binding with @ObjectBinding and @EnvironmentObject

2019-08-21 11:08发布

问题:

28-07-2019. I still have a question about the code below. I would like to separate the data model out of the ContentView. So I made a separate file and added the class, like this:

import SwiftUI
import Combine

class User: BindableObject {
    let willChange = PassthroughSubject<Void, Never>()
    var username : String = "Jan" { willSet { willChange.send() }}
    var password : String = "123456" { willSet { willChange.send() } }
    var emailAddress : String = "jan@mail.nl" { willSet { willChange.send() } }
}

#if DEBUG
struct User_Previews: PreviewProvider {
    static var previews: some View {
        User()
            .environmentObject(User())
    }
}
#endif

This doesn't work however, I'm getting an error:

Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols

Error occurs on the .environmentObject(User()) line in # if DEBUG.


After watching some video's I made the following code (including changes for Xcode 11 beta 4). Tips from both answers from dfd and MScottWaller are already included in the code.

import Combine
import SwiftUI

class User: BindableObject {
    let willChange = PassthroughSubject<Void, Never>()
    var username = "Jan" { willSet { willChange.send() } }
    var password = "123456" { willSet { willChange.send() } }
    var emailAddress = "jan@mail.nl" { willSet { willChange.send() } }
}

struct ContentView: View {
    @EnvironmentObject var user: User

    private func buttonPressed() {
        print(user.username) // in Simulator
    }

    var body: some View {
        VStack {
            TextField("Username", text: $user.username)
            TextField("Password", text: $user.password)
            TextField("Emailaddress", text: $user.emailAddress)
            Button(action: buttonPressed) {
                Text("Press me!")
            }

        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(User())
    }
}
#endif

But now onto the next part. If I have another view... how can I reference the data then? Since the source of truth is in the above ViewContent() view. The answer is:

import SwiftUI

struct DetailView: View {
    @EnvironmentObject var user: User

    var body: some View {
        VStack {
            TextField("Username", text: $user.username)
            TextField("Password", text: $user.password)
            TextField("Email", text: $user.emailAddress)
        }
    }
}

#if DEBUG
struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView()
            .environmentObject(User())
    }
}
#endif

Don't forget to edit the SceneDelegate (answer from dfd):

var user = User()

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: ContentView()
            .environmentObject(user)
        )
        self.window = window
        window.makeKeyAndVisible()
    }
} 

回答1:

If the "source of truth" is User, and you've made it a BindableObject you just need to expose it best to make it available to the various views you want. I suggest @EnvironmentObject.

In your SceneDelegate, do this:

var user = User()

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: ContentView()
            .environmentObject(user)
        )
        self.window = window
        window.makeKeyAndVisible()
    }
}

Now that a "stateful" instance of User is available to any View, you simply need to add:

@EnvironmentObject var user: User

To any/all vies that need to know about User.

BindableObject (for the most part) reserve memory for what you've denied. @ObjectBinding merely binds a view to what is in that part of memory (again, for the most part). And yes, you can do this for User in all views - but since you are instantiating it in ContentView? Nope.)! @EnvironmentObject makes it available to any views that need to access it.

Absolutely, you can use @ObjectBinding instead of an @EnvironmentObject, but so far,? I've never heard of a reason to do that.



回答2:

In your DetailView preview, don't for get to attach the environmentObject. See how I've added it in the PreviewProvider below. When you run the actual app, you'll want to do the same to you ContentView in the SceneDelegate

import SwiftUI

struct DetailView: View {
    @EnvironmentObject var user: User

    var body: some View {
        HStack {
            TextField("Username", text: $user.username)
            Text("Hello world!")
        }
    }
}

#if DEBUG
struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView()
            .environmentObject(User())
    }
}
#endif


标签: swiftui