SwiftUI bug or limitation?

So I have a super-simple model, and a super-simple GUI as follows:

class SomeModel: BindableObject {
   var didChange = PassthroughSubject<Void, Never>()

   var n1: Int = 0 {
        didSet { didChange.send()
                 print("Did change was called on n1") }
   }
}

struct ContentView : View {
   @Binding var n1: Int

   // imagine some GUI which displays n1, mutates it on a button click, etc.
}

So far so good. But what I want to do now is to pass a binding to the model's n1 property into the view. I do that as follows: in the spot in the boilerplate SceneDelegate.swift file you get, I wrote this:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    @ObjectBinding var theModel = SomeModel()

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let rootView = ContentView(n1: $theModel.n1)
            window.rootViewController = UIHostingController(rootView: rootView)
           //etc.
      }
   }
}

Now when I click on my button, which is wired to mutate via the n1 variable in ContentView, the print statement fires, but the GUI never updates. No compiler warnings, etc. Why isn't SwiftUI tracking the dependency? I passed it in a binding...

Is this illegal? Do I have to instead always give the ContentView object an @BindableObject property that references the model, and then pull the value out of the model to use it? I'm basically trying to make global predicate/values that live in a model available to the UI without passing in an entire model.

(My use case for this is: imagine a global variable that indicates whether or not my app is in connection with my company's network. I use this all over the place, so it's useful to think of it just as a global bool variable "isCompanyNetworkReachable". So I'd like to pass just that single value around, use it as a bool in expressions, etc. and have the GUI be able to react to state changes on it. It would be annoying to always have to write:

 `Text(isCompanyNetworkReachable.value ? "" : "Working offline")`

when I want to write

` Text(isCompanyNetworkReachable ? "" : "Working offline")`

so i'm trying to pass into the view a binding object, and NOT the enclosing model itself, if that makes sense.

I'm no SwiftUI expert (few are at this point), and there isn't much (any?) low level documentation about how @Binding works, but my theory is that, in your example, because the reference to the @ObjectBinding exists outside of any SwiftUI view, the SwiftUI system doesn't detect changes. Move it one layer deeper and it works fine. So if, instead of passing a binding to a view, you passed the @ObjectBinding to a view that then passed the Binding, I think it would work.

Generally, though, I'm not sure @ObjectBinding is a good way to achieve what you want anyway. It sounds more like what @EnvironmentObject is for, which is just a BindableObject that is implicitly available in subviews. Of the SwiftUI apps I've seen so far, most use a single root object for global app state, and more explicit state passage for state that only goes between a few views.

As an aside, I'm not sure if you're really using an isCompanyNetworkReachable property, or how you calculate it, but I'd hope you aren't actually using a reachability check like this. Apple has recommended for years now that you don't use something like this, as reachability can never be completely accurate. Instead, deriving such a value from active network requests failing due to network failures, or URLSessionTasks waiting for connectivity, is much more accurate.

Thanks. Your theory matches what i was thinking myself. I’ve just started playing around, but I need to see if you can put individual @Bindings into the @EnvironmentObject rather than @ObjectBindings.

What I’d really love to be able to do is just write

MyCystomView(isWetworkReachableBinding: SessionWideModelController.shared.isNetworkReachable())

where isNetworkReachable() returns an @Binding to a bool. that would be really cool. I don’t want to pass the entire session-wide-model-controller down to my view. I want to pick and choose its state bits to hand out to my view. And why shouldn’t I be able to?

The thing is, that session-wide-model-controller sure ain’t a view. It could exist even if I had no GUI at all, it really tracks “back-end” sort of state.

As for the comment about isCompanyNetworkReachable, it’s actually computed by contacting a single dedicated server that is essentially a “lighthouse”, to judge the actual success of talking to our network. We know to recheck whenever Apple’s API’s tell us that “reachability” has changed due to any kind of network change (wifi switched off, VPN kicking in/out etc.) It works quite well.

I'm guessing such a thing would make it harder for SwiftUI to track state changes, since something like that would necessarily need to expose hooks into SwiftUI's state tracking so you could manually publish changes without actually being in the view hierarchy.

Personally, I see little difference between what you want and @EnvironmentObject. You can have more than one @EnvironmentObject if you want, so you could have a single value for network reachability. That doesn't really seem to improve anything to me though.

I’m missing something very basic here.

Suppose I declare these vars in a View struct:

@EnvironmentObject var leftModel: MyModel
@EnvironmentObject var rightModel: MyModel

Fine. Now how do I inject two different instances of MyModel here? It works with one model, mysteriously: I supplied it to the outermost view, and the innermost view found it. But nobody knew the internal variable names the inner view was using. Now, with two different instances, how do i get two different models bound to leftModel and rightModel, respectively?

I'm pretty sure you can't have multiple @EnvironmentObjects of the same type, as they seem to be bound by type and not name. I do wish we could explicitly grab things out of the environment explicitly like you can with @Environment, but I think the intention is to have separate sets of state in different types. I think I'll file some feedback regarding explicit binding for custom environment values.

Ick. I’m creating very small BindableObject’s, that are generic, that store a single value (Bool, Int, Double). I would make those available for the people who want to set/get the overall state. I don’t want a monolithic model holding lots of different properties.

On the other hand, I just discovered EnvironmentKey, so I suppose I can just use extensions and add to the global environment. maybe that’s what i should be doing.

but binding by type so you can only have one of a given type, and getting rid of the name sure feels weird.

thanks for filing some feedback.

Terms of Service

Privacy Policy

Cookie Policy