• When Should I Use @state, @binding, @observedobject, @environmentobject, or @environment?
  • Jared Sinclair
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: LoneyIsError
  • Proofread: Zilin Zhu,PassionPenguin

SwiftUI introduces a new set of property wrappers that allow your code to bridge the gap between application state and view:

 @State
 @Binding
 @ObservedObject
 @EnvironmentObject
 @Environment
Copy the code

That’s just part of it. There are other property wrappers for Core Data to retrieve requests and for recognizing gestures. But these wrappers are irrelevant to this article. Unlike the listed wrappers, the usage scenarios for @fetchRequest and @geSTUreState are clear. However, when to use @state versus @Binding, or @observedobject versus @environmentobject, These can often be confusing.

This article tries to define when each wrapper should be used in simple, repetitive terms. I may be a little prescriptive here, but I’ve learned a lot from these rules. Also, there is a long tradition of programmers blogging about over-specification, so while it can be annoying at times, I think following the specification should help you.

All of the code examples below can be found in the GitHub repository.

Note: This article was written on iOS 13 using Swift 5.

Quick table

  1. Use this when your view needs to change its own properties@State.
  2. Use this function when your view needs to change properties owned by the ancestor view or properties owned by the observable referenced by the ancestor view@Binding
  3. Use this when your view depends on observables@ObservedObjectThat object can be created by itself or passed as a parameter to the view’s constructor.
  4. Use this when passing an observable through the constructor of the view’s all ancestors is too cumbersome@EnvironmentObject.
  5. Use this parameter when the view depends on a type that does not conform to ObserveObject@Environment.
  6. This can also be used when a view depends on multiple instances of the same type, as long as the type does not need to be used as an observable@Environment.
  7. If your view requires multiple instances of the same observable class, you are out of luck. You cannot use@EnvironmentObject@EnvironmentTo solve the problem. (There is, however, a simple workaround at the bottom of this article).

Your view needs one@StateProperty if…

… When it needs read/write access to one of its properties for private use.

Like the Ishu Light-up property on UIButton. Other objects do not need to know when the button is highlighted, nor do they need to assign to this property. If you are implementing a button from scratch in SwiftUI, decorating your Ishu Lighted property with the @State wrapper would be a good choice.

struct CustomButton<Label> :View where Label : View {
    let action: () -> Void
    let label: () -> Label

    /// it needs read/write access to one of 
    /// its own properties for private use
    @State private var isHighlighted = false
}
Copy the code

… It needs to give the subview read/write access to one of its properties.

The view does this by passing projectedValue of the @state-wrapped property, which is a binding of that value [1]. A good example is Swiftui.Alert. The view is responsible for controlling the display of the Alert pop-up by changing the value of a property decorated with @state, such as a Boolean property of an isPresentingAlert. But the view cannot hide the Alert popup itself, because the Alert popup does not know which view it pops in. You can solve this conundrum by using the @state attribute. The view binds to the isPresentingAlert property of the Alert box by using the compiler-generated self.$isPresentingAlert property, which is syntax-sugar for the predicted value of the @state wrapper. The.alert(isPresented: Content 🙂 modifier accepts this binding, and the Alert popbox then sets its isPresentingAlert property to false, thereby automatically disappearing.

struct MyView: View {
    /// it needs to provide read/write access of 
    /// one of its properties to a descendant view
    @State var isPresentingAlert = false

    var body: some View {
        Button(action: {
            self.isPresentingAlert = true
        }, label: {
            Text("Present an Alert")
        })
        .alert(isPresented: $isPresentingAlert) {
            Alert(title: Text("Alert!"))}}}Copy the code

Your view needs one@BindingProperty if…

… It needs to be on the ancestor viewStateWrap properties for read/write access

Reverse the scenario of the Alert box above. If your view is similar to the Alert popup in that it depends on the value owned by the ancestor and, crucially, requires variable access to that value, then the view needs an @binding to that value.

struct MyView: View {
    @State var isPresentingAlert = false

    var body: some View {
        Button(action: {
            self.isPresentingAlert = true
        }, label: {
            Text("Present an Alert")
        })
        .customAlert(isPresented: $isPresentingAlert) {
            CustomAlertView(title: Text("Alert!"))}}}struct CustomAlertView: View {
    let title: Text
    
    /// it needs read/write access to a State-
    /// wrapped property of an ancestor view
    @Binding var isBeingPresented: Bool
}
Copy the code

… It needs to matchObservableObjectObject, but references to the object are owned by its ancestors.

God, that’s a mouthful. In this case, there are three things:

  1. An observable object
  2. Has an ancestor view of the @-something wrapper that references the object
  3. Your view is a descendant of #2. (A view is a descendant of an ancestor view that it inherits.)

Your view needs to have read/write access to some members of the observable, but it does not (and should not) have access to the observable. The view will then define an @Binding attribute for the value, which will be provided by its ancestor view when the view is initialized. A good example of this is any reusable input view, such as a selector or text box. A text box needs to be able to have read/write access to some string properties on another object, but a text box should not be tightly coupled to that particular object. In contrast, the @Binding property of the text box provides read/write access to the string property without exposing it directly to the text box.

struct MyView: View {
    @ObservedObject var person = Person(a)var body: some View {
        NamePicker(name: $person.name)
    }
}

struct NamePicker: View {

    /// it needs read/write access to a property 
    /// of an observable object owned by an ancestor
    @Binding var name: String

    var body: some View {
        CustomButton(action: {
            self.name = names.randomElement()!
        }, label: {
            Text(self.name)
        })
    }
}
Copy the code

Your view needs one@ObservedObjectProperty if…

… It depends on the observables that can be instantiated.

Suppose you have a view that shows a list of items retrieved from a web service. SwiftUI’s view is a temporary, throwaway value type. They are great for presenting content, but not for web service requests. In addition, you should not mix user interface code with other tasks, as that would violate the single responsibility principle. Instead, you might transfer these responsibilities to another object that coordinates the request, parses the response, and maps the response to the values needed for the user interface model values. The view will have a reference to this object through the @observedobject wrapper.

struct MyView: View {

    /// it is dependent on an object that it 
    /// can instantiate itself
    @ObservedObject var veggieFetcher = VegetableFetcher(a)var body: some View {
        List(veggieFetcher.veggies) {
            Text($0.name)
        }.onAppear {
            self.veggieFetcher.fetch()
        }
    }
}
Copy the code

… It relies on a reference type object that can be easily passed to the view’s constructor.

This scenario is almost identical to the previous one, except that there are other objects in addition to the view that initialize and configure the observable. This can happen if some UIKit code is responsible for rendering your SwiftUI view, especially if the observable is accessed without referring to other SwiftUI views without being able to (or should not) access dependencies.

struct MyView: View {

    /// it is dependent on an object that can
    /// easily be passed to its initializer
    @ObservedObject var dessertFetcher: DessertFetcher

    var body: some View {
        List(dessertFetcher.desserts) {
            Text($0.name)
        }.onAppear {
            self.dessertFetcher.fetch()
        }
    }
}

extension UIViewController {

    func observedObjectExampleTwo(a) -> UIViewController {
        let fetcher = DessertFetcher(preferences: .init(toleratesMint: false))
        let view = ObservedObjectExampleTwo(dessertFetcher: fetcher)
        let host = UIHostingController(rootView: view)
        return host
    }

}
Copy the code

Your view needs one@EnvironmentObjectProperty if…

… It is too cumbersome to pass the observed object to all the constructors of all the ancestors of the view

Let’s go back to the second example in the @observedobject section above, where you need an observable to perform some tasks on behalf of your view, but the view cannot initialize the object itself. But for now let’s assume that your view is not a root view, but a descendant view deeply nested in many ancestor views. If the observed object is not needed by all ancestor views, it would be awkward to require that every view in the view chain include the observed object in their constructor arguments, just so that some descendant view can access it. Instead, you can provide this value indirectly by putting it into the SwiftUI environment around the view. Views can access the current environment instance through the @environmentobject wrapper. Note that once the value of @environmentobject is resolved, this use case is functionally the same as using the object wrapped in @observedobject.

struct SomeChildView: View {

    /// it would be too cumbersome to pass that 
    /// observed object through all the initializers 
    /// of all your view's ancestors
    @EnvironmentObject var veggieFetcher: VegetableFetcher

    var body: some View {
        List(veggieFetcher.veggies) {
            Text($0.name)
        }.onAppear {
            self.veggieFetcher.fetch()
        }
    }
}

struct SomeParentView: View {
    var body: some View {
        SomeChildView()}}struct SomeGrandparentView: View {
    var body: some View {
        SomeParentView()}}Copy the code

Your view needs one@EnvironmentProperty if…

… This depends on compliance with the ObservableObject type.

Sometimes your view will depend on something that doesn’t comply with ObservableObject, but you want it to, because passing it as a constructor argument is too cumbersome. There are many reasons why a dependency might not comply with ObservableObject, such as:

  • A dependency is a value type (like structure, enumeration, etc.)
  • Dependencies are exposed to the view only as protocols, not concrete types
  • A dependency is a closure

In this case, the view will use the @Environment wrapper to get the required dependencies. It takes some template to get it right.

struct MyView: View {

    /// it is dependent on a type that cannot 
    /// conform to ObservableObject
    @Environment(\.theme) var theme: Theme

    var body: some View {
        Text("Me and my dad make models of clipper ships.")
            .foregroundColor(theme.foregroundColor)
            .background(theme.backgroundColor)
    }
}

// MARK: - Dependencies

protocol Theme {
    var foregroundColor: Color { get }
    var backgroundColor: Color { get}}struct PinkTheme: Theme {
    var foregroundColor: Color { .white }
    var backgroundColor: Color { .pink }
}

// MARK: - Environment Boilerplate

struct ThemeKey: EnvironmentKey {
    static var defaultValue: Theme {
        return PinkTheme()}}extension EnvironmentValues {
    var theme: Theme {
        get { return self[ThemeKey.self]}set { self[ThemeKey.self] = newValue }
    }
}
Copy the code

… Your view depends on multiple instances of the same type, as long as that type doesn’t need to be used as an observable.

Since @environmentobject supports only one instance of each type, this idea doesn’t work. Conversely, if you need to register multiple instances of a given type with the key path for each instance, you need to use @Environment so that the view’s properties can specify the key path they want.

struct MyView: View {
    @Environment(\.positiveTheme) var positiveTheme: Theme
    @Environment(\.negativeTheme) var negativeTheme: Theme

    var body: some View {
        VStack {
            Text("Positive")
                .foregroundColor(positiveTheme.foregroundColor)
                .background(positiveTheme.backgroundColor)
            Text("Negative")
                .foregroundColor(negativeTheme.foregroundColor)
                .background(negativeTheme.backgroundColor)
        }
    }
}

// MARK: - Dependencies

struct PositiveTheme: Theme {
    var foregroundColor: Color { .white }
    var backgroundColor: Color { .green }
}

struct NegativeTheme: Theme {
    var foregroundColor: Color { .white }
    var backgroundColor: Color { .red }
}

// MARK: - Environment Boilerplate

struct PositiveThemeKey: EnvironmentKey {
    static var defaultValue: Theme {
        return PositiveTheme()}}struct NegativeThemeKey: EnvironmentKey {
    static var defaultValue: Theme {
        return NegativeTheme()}}extension EnvironmentValues {
    var positiveTheme: Theme {
        get { return self[PositiveThemeKey.self]}set { self[PositiveThemeKey.self] = newValue }
    }

    var negativeTheme: Theme {
        get { return self[NegativeThemeKey.self]}set { self[NegativeThemeKey.self] = newValue }
    }
}
Copy the code

A multi-instance solution to EnvironmentObject

Although it is technically possible to register an observable using the.environment() modifier, changes to the @published property of the object do not trigger invalidation or updating of the view. This can only be done using @environmentobject and @observedobject. Unless the upcoming iOS 14 API changes, I can only find one way to solve this problem: use custom property wrappers.

  • You should use modifiers.environment()Register each instance, whilenotUse modifiers.environmentObject()Registration.
  • You need a matchDynamicPropertyThe custom property wrapper for the., which has a private@ObservedObjectProperty whose value is passed during initialization from aEnvironment<T>A single instance of the structure (used as an instance, not as a property wrapper).

In this way, the view can observe multiple objects of the same class:

struct MyView: View {

    @DistinctEnvironmentObject(\.posts) var postsService: Microservice
    @DistinctEnvironmentObject(\.users) var usersService: Microservice
    @DistinctEnvironmentObject(\.channels) var channelsService: Microservice

    var body: some View {
        Form {
            Section(header: Text("Posts")) {
                List(postsService.content, id: \.self) {
                    Text($0)}}Section(header: Text("Users")) {
                List(usersService.content, id: \.self) {
                    Text($0)}}Section(header: Text("Channels")) {
                List(channelsService.content, id: \.self) {
                    Text($0)
                }
            }
        }.onAppear(perform: fetchContent)
    }
}

// MARK: - Property Wrapper To Make This All Work

@propertyWrapper
struct DistinctEnvironmentObject<Wrapped> :DynamicProperty where Wrapped : ObservableObject {
    var wrappedValue: Wrapped { _wrapped }
    @ObservedObject private var _wrapped: Wrapped

    init(_ keypath: KeyPath<EnvironmentValues.Wrapped>) {
        _wrapped = Environment<Wrapped>(keypath).wrappedValue
    }
}

// MARK: - Wherever You Create Your View Hierarchy

MyView()
    .environment(\.posts, Microservice.posts)
    .environment(\.users, Microservice.users)
    .environment(\.channels, Microservice.channels)
    // each of these has a dedicated EnvironmentKey
Copy the code

The sample code

All of the above examples can be downloaded and executed here.

reference

[1] Each @propertyWrapper — consistency type will provide options for projectedValue properties. The type of the value is determined by the implementation. The expected value for State

in this example is Binding

. Every time you use a new property wrapper, you should jump to the interface it generates to discover its expected values more deeply.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.