DEV Community

Maarek
Maarek

Posted on

Property Wrappers in SwiftUI

With Swift 5 and SwiftUI, Apple introduced property wrappers. You might have saw one of them on some SwiftUI articles or tutorials : @State, @BindableObject, @EnvironmentObject.
In this post, we’ll try to explain what are these wrappers and when to use them.

Everything in SwiftUI is struct based. And structs are immutable, those are fixed values.
When a property has an @State property wrapper, it just tells your struct that the memory management won’t be handled by the struct itself but by another memory manager entity : the SwiftUI Framework.
Let’s picture a practical use case :

struct ContentView : View {
    var buttonTapped: Bool = false
    var body: some View {
        Button(action: {
            self.buttonTapped.toggle()
        }) {
            Text("Tap the button")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we want the buttonTapped value to toggle on the tap of the button.
Xcode won’t let me do that saying :

Cannot use mutating member on immutable value: 'self' is immutable

Of course, we’re in a struct!
That’s where you want to use the @State wrapper. The memory, the creation and the storage of the property won’t be managed by the struct itself.
What it actually does is it will be notified that the value has changed and it will re-create the struct with the new value, therefore, the view will be invalidate and be refreshed with the updated value.

A $ sign before a value means a two way binding. Passing a $value to a View will enable to read the value and update it. Might be useful for TextFields for example, which will read and write the value you passed. Just like that :

struct ContentView : View {
    @State var input: String = ""
    var body: some View {
        TextField($input)
    }
}
Enter fullscreen mode Exit fullscreen mode

@State is great for these simple cases. When your view has simple properties like String, Int or Bool value. But what about when you have more complicated objects you want to use ?
Let’s say you have this registration form, that owns 3 fields : email, username, password.
First thing would be to make a struct with these Strings attributes, right ?

struct Registration {
var email: String = ""
var username: String = ""
var password: String = ""
}

And just store it in your view with a @State wrapper.

struct ContentView : View {
    @State var registration: Registration = Registration()
    var body: some View {
        Text(“Hello”)
    }
}
Enter fullscreen mode Exit fullscreen mode

It would actually works great as long as you’re using this struct in only one view, but let’s imagine you would have one view, say InfoView, with the email and username fields and another, that would be PasswordView, for the password field. That is where you would be stucked. Both view would need the same Registration model.
The Registration object will be passed to these two views, but remember, structs have one owner, because structs works as value and not reference. The Registration is a struct. You can’t have an instance of a struct like an object instance. If multiple views (or any kind of objects) owns a struct, every view would have it's own version of it.

So to make our registration form complete, we would need to transform the struct into a class. So we can work with references. Pass it to every view we would need to. But the thing is : with the @State wrapper, the the view (struct) gets re-created with updated values, but you can’t do that with a class. The class won’t be “listened” like an @State property would be.
It’s time to import Combine !
In the Getting started with SwiftUI and Combine post, we discovered the Bindable protocol. This is what we need to use in this situation. We’ll make our Registration class Bindable.

class Registration: Bindable {
    var email: String = ""
    var username: String = ""
    var password: String = ""
}
Enter fullscreen mode Exit fullscreen mode

As any Bindable object, you will need a didChange property and call it on any value updated.
We don’t need to pass a Registration object or an error. We just need to notify that the object’s values has changed.

class Registration: Bindable {
    var didChange = PassthroughSubject<Void, Never>()
        var email: String = "" {
        didSet {
            didChange.send()
        }
    }
    // do the same for each property
}
Enter fullscreen mode Exit fullscreen mode

Now we need to tell SwiftUI to watch for these changes and, juste like with the @State, update the UI by recreating the view.
So just replace @State in your view by @ObjectBinding :

struct ContentView : View {
    @ObjectBinding var registration: Registration = Registration()
    var body: some View {
        Text(“Hello”)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we solve the problem of having a complex, referenced type, object to be listened by SwiftUI to update our view. But what about passing this object to another view, just like in our practical use case with the two pages registration form ?
Well, Apple made something really cool for passing object through views : Environment. This is a way to share objects to make them available where needed.

Here is how it works :
You just add your Registration object with @EnvironmentObject :

struct ContentView : View {
    @EnvironmentObject var registration: Registration
    var body: some View {
        Text(“Hello”)
    }
}
Enter fullscreen mode Exit fullscreen mode

You don’t need to initialize it, as your view will assume it's value will be available in the Environment. To add a value into the Environment it is quite simple :

ContentView.environmentObject(Registration())
Enter fullscreen mode Exit fullscreen mode

You can do so in you sceneDelegate or anywhere you call your view.
Every view that gets to use the Registration object need to have a Registration property wrapped with @EnvironmentObject.

I hope this post helped you to understand the mechanics of SwiftUI and how to use the property wrappers.
I you have any questions, don’t hesitate to leave a comment.

Happy coding! :)

Top comments (0)