Property wrappers in SwiftUI

February 3, 2020

What is Property Wrapper?

Property wrapper is a simple generic class/struct that has some logic that gets trigger whenever the value of property gets modified. It was introduced during WWDC 2019 and comes with Swift 5.

SwiftUI provides us many property wrappers like @State, @Binding, @ObservedObject, @EnvironmentObject, @Environment. Let's discuss each of them and understand when to use and how to use it.

@State

@State is a property wrapper that allows us to modify value inside a struct. When the state value changes, the view invalidate its appearance and rebuild the view. Because of the local nature of @State property, Apple recommends us to mark them as private. Here is the example,

struct ContentView: View {
    @State private var showAlert: Bool = false    
    var body: some View {
        Button(action: {
            self.showAlert = true
        }) {
            Text("Show me alert")
        }
        .alert(isPresented: $showAlert) {
            Alert(title: Text("Hey!"), message: Text("Have a good day :)"), dismissButton: .default(Text("OK")))
        }
    }
}

In the above example, we have a simple screen with the button "Show me alert" in the center. When you hit the button it will change the state property showAlert and SwiftUI recreates view and display the alert.

@Binding

@Binding property wrapper allows you to pass the data between two or more views without ownership. It provides reference like access for a value type. @Binding property has its value passed in from parent view as binding. It does not have ownership since it copies a value from parent @State value. Here is the example,

struct ContentView: View {
    @State private var isPlaying: Bool = false
    
    var body: some View {
        VStack {
            Text("Test audio!")
            PlayButtonView(isPlaying: $isPlaying)
        }
    }
}

struct PlayButtonView: View {
    @Binding var isPlaying: Bool
    var body: some View {
        Button(action: {
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
        }
    }
}

In the above example, we have a simple view with a label and play/pause button in the center. We have state property isPlaying and passing that reference to PlayButtonView using $. As soon as PlayButtonView changes the value of isPlaying property, SwiftUI recreates ContentView and PlayButtonView as its child view.

@ObservedObject

@ObservedObject is similar to @State property wrapper but it is for external type and track the changes using @Published property wrapper. The key difference between @State and @ObservedObject is that @ObservedObject shares the object between two or more individual views. Let's look at the example,

class Cart: ObservableObject {
    @Published var cartItems = [String]()
}

struct ContentView: View {
    @ObservedObject var cart = Cart()
    
    var body: some View {
        VStack {
            Text("Number of items in cart is: \(cart.cartItems.count)")
        }
    }
}

In the above example, we have Cart class that we are using in multiple views of our application. Whenever any changes made to cartItems, It will automatically notify all the views that use the Cart. Any type you mark as @ObservedObject should conform to the protocol ObservableObject and it should be a class type.

@EnvironmentObject

@EnvironmentObject is very similar to @ObservedObject but the main difference is that @EnvironmentObject allows us to create a property that shared across the entire app without created by view or passed in from another view. In simple words, we can add a property in the Environment of the view hierarchy and all the child view in the hierarchy can access that property. Here is the example,

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

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

struct ContentView: View {
    @EnvironmentObject var cart: Cart
    
    var body: some View {
        VStack {
            Text("Number of items in cart is: \(cart.cartItems.count)")
        }
    }
}

Above example is similar to example of @ObservedObject but the difference is we don't need to pass the default value by uisng @EnvironmentObject. It means it will be provided by SwiftUI environment. You can see in the example that we created the cart object in SceneDelegate and passing it to ContentView's environment object.

@Environment

@Environment property wrapper allows us to use system default settings like whether the device is in dark mode or light mode, device's current timezone, etc. Here is the example,

struct ContentView: View {
    @Environment(\.timeZone) var timeZone: TimeZone
    
    var body: some View {
        VStack {
            Text("Your Timezone: \(self.timeZone.abbreviation() ?? "N/A")")        }
    }
}

In above example, we have simple screen that display device's default currency code in the center of screen.

Conclusion

Property wrappers are powerful and playing a huge role while working with SwiftUI. You need to choose wisely which property wrapper you need to use and how to use it. If you have any questions feel free to follow me on Twitter and ask me your questions.

Thank you for reading! 😀 Peace ✌️