Swift includes properties within the language (see Properties), but also supports (what we’d call in C#) property or data binding mechanisms.
Before we get started, I’m writing this post as a learning exercise, I’d suggest you checkout the references at the bottom of the post for a much more in depth look at this subject.
If you’re still reading, then let’s get started…
To begin with we’ll look at how we declare properties in Swift first. For those coming from C# or similar languages, then the simplest stored property looks just like a field
Property Declarations
struct MyModel { var id: String; }
The above declaration is known as a stored property, in that we can store values with the specific instance of the MyModel type, this is really the equivalent of having a property with a default getter and setter.
We can also declare properties as readonly like this
struct MyModel { var id: String { return "SomeId" } }
Both of the above examples are basically properties using “shorthand syntax”, like C# we can also create properties with get and set keywords – of course these are useful if the setting of the property (for example) does more than just assign a value, hence we have a form (notice you can use return or in these example we’re using implicit return
struct MyModel { private var _id: String = "SomeId" var id: String { get { _id } set { _id = newValue } } }
Property Observer
Property observers allow us to monitor a property change. For example if you have a struct/class with the shorthand syntax for a property then we can willSet and/or didSet to our property, for example
struct MyModel { var id: String = "" { willSet(newTitle) { print("About to set value") } didSet { if title != oldValue { print("Set Value") } } } }
Now when the property is about to be set, the willSet code is called and when a value changes didSet will be called. Obviously you wouldn’t want to do anything within these methods that could affect performance.
Property Wrapper
A Property Wrapper allows us to (well as the name suggests) wrap a property within a type that we can separate the storage of the property from functionality that acts upon that property. For example if our title has a length constraint, we might create a property wrapper to implement this functionality
@propertyWrapper struct LengthConstraint { private var value = "" var wrappedValue: String { get { value } set { value = String(newValue.prefix(5)) } } }
Property Binding
So what’s this got to do with binding/data binding/property binding? Well we’ve now seen how we declare propeties and can add property wrappers to the property. Within SwiftUI we can use the @Published property wrapper to our property which gets observed by a UI element and when the value changes updates the UI or in the case of TextField, when changes are made to the text field it’s placed into our property.
There are several different property wrappers for this type of functionality
- @Published – this is used on property classes, such as ObservableObject subclasses.
- @StateObject – this is used within a view to store an instance of an ObservableObject subclassed type
- @State – this allows us to store bindable data within a view
- @Binding – this is used in views which need to mutate a property owned by an ancestor view. In essences it’s like connecting to a parent’s property
- @EnvironmentObject – this is a shared object stored within the environment and is used in situations where passing properties through a UI hierarchy or the likes becomes too cumbersome.
- @Environment – this is used to connect to actual environment properties, such as reading whether the application is in dark theme or other SwiftUI properties
MVVM
We can declares @State and @Binding property wrappers (for example) within our view, but of course this can pollute the UI with view model state. If we prefer to go with a MVVM approach, i.e. separate the view from the view model, then we will start off by subclassing ObservableObject, for example here’s a minimal view model
@MainActor class ViewModel: ObservableObject { @Published var title: String = "" } @StateObject private var viewModel = ViewModel()
In the above code we declare and subclass of the ObservableObject and then we declare the properties that we wish to expose. The @StateObject line is what you’ll have in your ContentView to create an instance of the ObservableObject.
The @MainActor is used to denote that this class runs code on the main queue – essentially if you’re used to Windows UI programming this can be seen as equivalent to running code on the UI thread.
This of course might seem simpler to just declare properties like this, but then our model ends up more tightly coupled to the view than we might want
@State var title: String = ""
References
All SwiftUI property wrappers explained and compared
How to use @MainActor to run code on the main queue
What is the @Environment property wrapper?