Can't use @ObservedObject on real iPhone
This isn't a problem with not conforming to ObservableObject
. The code you provided works in the canvas and in the simulator, and should also work on the device. I have already come across this issue with iOS 13 Beta 6 in my own project and have spent a lot of time troubleshooting.
Other things (such as calling self.presentationMode.value.dismiss()
to dismiss a modal view) are also currently broken when running projects built with Xcode Beta 5 on devices running iOS 13 Beta 6. There have been issues with previous betas of Xcode not working on newer betas of iOS, and this may be the same issue.
I would suggest that you wait until Xcode Beta 6 is released to make any significant structural changes to your code, as iOS 13 Beta 6 may have been developed in anticipation of handling changes that will be made in Xcode Beta 6.
That being said, if you absolutely must make changes to workaround this issue in the meantime, I've found that using @EnvironmentObject
instead of @ObservedObject
fixes this issue. In your example, that would mean declaring your property like this:
@EnvironmentObject private var networkManager: NetworkManager
Then, when you create your view, you can pass a NetworkManager
as an environment object like this:
ContentView()
.environmentObject(NetworkManager())
@StateObject vs @ObservedObject when passed externally but owned by the view
This is a really interesting question. There's some subtle behavior going on here.
First, notice that you can't just change @ObservedObject
to @StateObject
in NameView
. It won't compile:
struct NameView: View {
@StateObject var viewModel: ViewModel
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
// ^ Cannot assign to property: 'viewModel' is a get-only property
}
...
}
To make it compile, you have to initialize the underlying _viewModel
stored property of type StateObject<ViewModel>
:
struct NameView: View {
@StateObject var viewModel: ViewModel
init(_ viewModel: ViewModel) {
_viewModel = .init(wrappedValue: viewModel)
}
...
}
But there's something hidden there. StateObject.init(wrappedValue:)
is declared like this:
public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
So the expression given as an argument (just viewModel
above) is wrapped up in a closure, and is not evaluated right away. That closure is stored for later use, which is why it is @escaping
.
As you might guess from the hoops we have to jump through to make it compile, this is a weird way to use StateObject
. The normal use looks like this:
struct NormalView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.name)
}
}
And doing it the weird way has some drawbacks. To understand the drawbacks, we need to look at the context in which makeView()
or NormalView()
is evaluated. Let's say it looks like this:
struct ContentView: View {
@Binding var count: Int
var body: some View {
VStack {
Text("count: \(count)")
NormalView()
ViewFactory().makeView()
}
}
}
When count
's value changes, SwiftUI will ask ContentView
for its body
again, which will evaluate both NormalView()
and makeView()
again.
So body
calls NormalView()
during this second evaluation, which creates another instance of NormalView
. NormalView.init
creates a closure which calls ViewModel()
, and passes the closure to StateObject.init(wrappedValue:)
. But StateObject.init
does not evaluate this closure immediately. It stores it away for later use.
Then body
calls makeView()
, which does call ViewModel()
immediately. It passes the new ViewModel
to NameView.init
, which wraps the new ViewModel
in a closure and passes the closure to StateObject.init(wrappedValue:)
. This StateObject
also doesn't evaluate the closure immediately, but the new ViewModel
has been created regardless.
Some time after ContentView.body
returns, SwiftUI wants to call NormalView.body
. But before doing so, it has to make sure the StateObject
in this NormalView
has a ViewModel
. It notices that this NormalView
is replacing a prior NormalView
at the same position in the view hierarchy, so it retrieves the ViewModel
used by that prior NormalView
and puts it in the StateObject
of the new NormalView
. It does not execute the closure given to StateObject.init
, so it does not create a new ViewModel
.
Even later, SwiftUI wants to call NameView.body
. But before doing so, it has to make sure the StateObject
in this NameView
has a ViewModel
. It notices that this NameView
is replacing a prior NameView
at the same position in the view hierarchy, so it retrieves the ViewModel
used by that prior NameView
and puts it in the StateObject
of the new NameView
. It does not execute the closure given to StateObject.init
, and so it does not use the ViewModel
referenced by that closure. But the ViewModel
was created anyway.
So there are two drawbacks to the weird way in which you're using @StateObject
:
- You're creating a new
ViewModel
each time you callmakeView
, even though thatViewModel
may never be used. This may be expensive, depending on yourViewModel
. - You're creating the
ViewModel
while theContentView.body
getter is running. If creating theViewModel
has side effects, this may confuse SwiftUI. SwiftUI expects thebody
getter to be a pure function. In theNormalView
case, SwiftUI is calling theStateObject
's closure at a known time when it may be better prepared to handle side effects.
So, back to your original question:
Should it be
@StateObject
or@ObservedObject
?
Well, ha ha, that's difficult to answer without seeing an example that's less of a toy. But if you do need to use @StateObject
, you should probably try to initialize it in the ‘normal’ way.
ObservableObject is missing in Motherview
If you're not going to use it in parent view then just use locally as StateObject
struct RegistrationView: View {
@StateObject var progress = Progress() // << here !!
// ... other code
SwiftUI: How can I catch changing value from observed object when I execute function
It is used two different instances of SettingCodeTwo
- one in NetworkNamager
another in SettingsView
, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults
)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
@objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
@Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no @ObservedObject needed here
...
SwiftUI with Core Location as ObservableObject crashes
First off I want to thank Fabian and graycampbell for their help.
Secondly, as for as I can tell @ObservableObject still does not work in iOS 13 beta 8 using XCode 11 beta 6.
Here is what worked for me:
1. I changed
@ObservedObject var location: MYLocationManager = MYLocationManager()
to:
@EnvironmentObject var location: MYLocationManager
2. In the SceneDelegate I added:
let myLocationManager = MYLocationManager()
and:
window.rootViewController = UIHostingController(rootView: CoreLocationView_NeedsEnv()
.environmentObject(myLocationManager)
No more crash!!
P.S. I am using Fabian's updated code. Thanks again!
SwiftUI: ObservableObject does not persist its State over being redrawn
Finally, there is a Solution provided by Apple: @StateObject
.
By replacing @ObservedObject
with @StateObject
everything mentioned in my initial post is working.
Unfortunately, this is only available in ios 14+.
This is my Code from Xcode 12 Beta (Published June 23, 2020)
struct ContentView: View {
@State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
@ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
@Published var title = 0
}
struct TestView2: View {
@StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
As you can see, the StateObject
Keeps it value upon the redraw of the Parent View, while the ObservedObject
is being reset.
@Published property wrapper not working on subclass of ObservableObject
Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.
NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>()
on the subclass as that will again cause the state not to update properly.
I hope this is something that will be fixed in future releases as the objectWillChange.send()
is a lot of boilerplate to maintain.
Here is a fully working example:
import SwiftUI
class MyTestObject: ObservableObject {
@Published var aString: String = ""
}
class MyInheritedObject: MyTestObject {
// Using @Published doesn't work on a subclass
// @Published var anotherString: String = ""
// If you add the following to the subclass updating the state also doesn't work properly
// let objectWillChange = PassthroughSubject<Void, Never>()
// But if you update the value you want to maintain state
// of using the objectWillChange.send() method provided by the
// baseclass the state gets updated properly... Jaayy!
var anotherString: String = "" {
willSet { self.objectWillChange.send() }
}
}
struct MyTestView: View {
@ObservedObject var myTestObject = MyTestObject()
@ObservedObject var myInheritedObject = MyInheritedObject()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")
TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}
What is the difference between ObservedObject and StateObject in SwiftUI
@ObservedObject
When a view creates its own @ObservedObject
instance it is recreated every time a view is discarded and redrawn:
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
}
On the contrary a @State
variable will keep its value when a view is redrawn.
@StateObject
A @StateObject
is a combination of @ObservedObject
and @State
- the instance of the ViewModel
will be kept and reused even after a view is discarded and redrawn:
struct ContentView: View {
@StateObject var viewModel = ViewModel()
}
Performance
Although an @ObservedObject
can impact the performance if the View is forced to recreate a heavy-weight object often, it should not matter much when the @ObservedObject
is not complex.
When to use @ObservedObject
It might appear there is no reason now to use an @ObservedObject
, so when should it be used?
You should use @StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with @ObservedObject.
Note there are too many use-cases possible and sometimes it may be desired to recreate an observable property in your View. In that case it's better to use an @ObservedObject
.
Useful links:
- What’s the difference between @StateObject and @ObservedObject?
- What’s the difference between @ObservedObject, @State, and @EnvironmentObject?
- What is the @StateObject property wrapper?
Related Topics
Swift 3: Transfer Utility Enumeratetoassignblocks Method Signature
How to Check Bitfields (Scnetworkreachabilityflags in Particular) for Flags in Swift
How to Make a Nsurlsesssion Get Request with Cookies
Urlsession.Shared.Datatask VS Datataskpublisher, When to Use Which
Xcode Reference a Framework Instead of Link Binary with Libraries
Error Domain=Kclerrordomain Code=2 "The Operation Couldn't Be Completed. (Kclerrordomain Error 2.)"
Disabling Allowsbackgroundlocationupdates (Cllocationmanager) Doesn't Work After Is Was Enabled
How to Dismiss a Modal Segue Then Perform Push Segue to New View Controller
Draw Polyline Using Google Maps in Custom View with Swift 3
How to Set Requestserializer in Alamofire
Restkit Request Not Sending Parameters
Issue Comparing Uicolors in Swift
Detecting Double and Single Tap on Uitableviewcell to Perform Two Action
Native-Like Momentum-Scrolling on Body in iOS Webapp
How to Chain Filters in Metal for iOS
How to Fix Crash When Tap to Select Row After Scrolling The Tableview