How to use published optional properties correctly for SwiftUI
Asked Answered
L

1

6

To provide some context, Im writing an order tracking section of our app, which reloads the order status from the server every so-often. The UI on-screen is developed in SwiftUI. I require an optional image on screen that changes as the order progresses through the statuses.

When I try the following everything works...

My viewModel is an ObservableObject: internal class MyAccountOrderViewModel: ObservableObject {

This has a published property: @Published internal var graphicURL: URL = Bundle.main.url(forResource: "tracking_STAGEONE", withExtension: "gif")!

In SwiftUI use the property as follows: GIFViewer(imageURL: $viewModel.graphicURL)


My issue is that the graphicURL property has a potentially incorrect placeholder value, and my requirements were that it was optional. Changing the published property to: @Published internal var graphicURL: URL? causes an issue for my GIFViewer which rightly does not accept an optional URL:

Cannot convert value of type 'Binding<URL?>' to expected argument type 'Binding<URL>'

Attempting the obvious unwrapping of graphicURL produces this error:

Cannot force unwrap value of non-optional type 'Binding<URL?>'


What is the right way to make this work? I don't want to have to put a value in the property, and check if the property equals placeholder value (Ie treat that as if it was nil), or assume the property is always non-nil and unsafely force unwrap it somehow.

Lamoree answered 2/8, 2021 at 15:11 Comment(3)
What would you like to if the URL is nil? You've got to handle it somehowArabelle
Issue is it doesn't work if its optional, if url is nil I would bypass that part of SwiftUI definition.Lamoree
So you almost want like an if let x = ... kind of thing with the Binding?Arabelle
A
8

Below is an extension of Binding you can use to convert a type like Binding<Int?> to Binding<Int>?. In your case, it would be URL instead of Int, but this extension is generic so will work with any Binding:

extension Binding {
    func optionalBinding<T>() -> Binding<T>? where T? == Value {
        if let wrappedValue = wrappedValue {
            return Binding<T>(
                get: { wrappedValue },
                set: { self.wrappedValue = $0 }
            )
        } else {
            return nil
        }
    }
}

With example view:

struct ContentView: View {
    @StateObject private var model = MyModel()

    var body: some View {
        VStack(spacing: 30) {
            Button("Toggle if nil") {
                if model.counter == nil {
                    model.counter = 0
                } else {
                    model.counter = nil
                }
            }

            if let binding = $model.counter.optionalBinding() {
                Stepper(String(binding.wrappedValue), value: binding)
            } else {
                Text("Counter is nil")
            }
        }
    }
}

class MyModel: ObservableObject {
    @Published var counter: Int?
}

Result:

Result

Arabelle answered 2/8, 2021 at 16:23 Comment(2)
Thanks for a well written and helpful response!Lamoree
This one is great, but it gives a "Publishing changes from within view updates is not allowed, this will cause undefined behavior." warning on Xcode 14 :(Volution

© 2022 - 2024 — McMap. All rights reserved.