Optional @ObservableObject in SwiftUI
Asked Answered
W

4

19

I want to have an optional @ObservedObject in SwiftUI but I keep getting a compile time error of.

Property type 'AModel?' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'

Here is some minimum reproducible code.

import SwiftUI

public struct AView: View {
    
    //MARK: View Model
    //Error thrown here.
    @ObservedObject var model: AModel?
    
    //MARK: Body
    public var body: some View {
        Text("\(model?.value ?? 0)")
    }
    
    //MARK: Init
    public init() {
        
    }
    
}

class AModel: ObservableObject {
    let value: Int = 0
}
Weihs answered 12/4, 2021 at 18:33 Comment(4)
Why not making value optional in AModel, that would be more doable thing!Narcoanalysis
I'm currently refactoring that way but I'd still like to know why this does not seem to work?Weihs
I know what you trying to do, And I guess you want have multi option on your custom View wether you have a model or not(nil), if is that what you want, we can solve the issue in other way.Narcoanalysis
Optional is an enum - not a class, so it cannot conform to ObservableObject.Polynesian
P
21

The technical reason is that Optional is not a class, so it cannot be conformed to ObservableObject.

But if you want to avoid refactoring the object itself - for example, if it's not in your control - you could do something like this:

struct AView: View {

    private struct Content: View {
       @ObservedObject var model: AModel
       var body: some View {
          Text("\(model.value)")
       }
    }

    var model: AModel?
    
    var body: some View {
       if let model = model {
          Content(model: model)
       } else {
          Text("0")
       }
    }    
}
Polynesian answered 12/4, 2021 at 20:31 Comment(0)
A
4

You actually want your init argument to be optional, not the struct property. In your case I would simply do:

import SwiftUI

public struct AView: View {
    
    //MARK: View Model
    @ObservedObject var model: AModel
    
    //MARK: Body
    public var body: some View {
        Text("\(model.value)")
    }
    
    //MARK: Init
    public init(model: AModel? = nil) {
        self.model = model ?? AModel()
    }
    
}

class AModel: ObservableObject {
    let value: Int = 0
}

Anywise answered 19/7, 2021 at 1:10 Comment(0)
V
1

This is new in iOS 18 (and won't work on older OSes) but you can conform an ObservableObject to the Observable protocol and get a free conformance. Any @Published property is automatically tracked. This lets you use @State in a SwiftUI view which supports optional. (NOTE it does NOT work if something is not @Published like with manually sending changes to the didChange publisher).

Here's a working example:

struct ContentView: View {

    @State
    var myObservableObject: MyObservableObject? = nil

    var stateString: String {
        if let myObservableObject {
            return "\(myObservableObject.i)"
        }
        return "nil"
    }

    var body: some View {
        VStack {
            Text("State: \(stateString)")
            Button("Inc") {
                myObservableObject?.inc()
            }
            .disabled(myObservableObject == nil)
            Button("Toggle state") {
                if myObservableObject != nil {
                    self.myObservableObject = nil
                } else {
                    myObservableObject = MyObservableObject()
                }
            }
        }
        .buttonStyle(.bordered)
        .padding()
    }
}

class MyObservableObject: ObservableObject, Observable {

    @Published
    var i = 0

    func inc() {
        i += 1
    }
}
Vichyssoise answered 2/8 at 17:51 Comment(0)
A
0

Environment now supports Observable, while retaining the ability to remain nil-able. Still, having a nil-value here at run-time is still considered an incorrect state in most cases, so you're just trading in a run-time crash for a non-crashing bug.

Adumbral answered 21/3 at 16:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.