Data in SwiftUI, Part 3: Tools
Table of Contents
The last part in a series on understanding data in SwiftUI. See all tools SwiftUI provided to declare different types of data and dependency.
- Data flow in SwiftUI, Part 1: Data
- Data flow in SwiftUI, Part 2: Views as a function of data
- Data flow in SwiftUI, Part 3: Tools
Old tools
Before jump right into new tools, SwiftUI provided. Let have a visit to our old friend Swift property.
Property
SwiftUI builds around the concept of view as a function of data. To be able to do that, it needs special kind of data which framework know how to manage, e.g., keep track of the change and re-render.
But Swift property is just a plain old Swift. It doesn't acquire such ability. You can only use it when you want immutable data.
Usage
Since it is immutable, it only suite for read-only, static data. You can use it where you want to store hard code values or read-only data from the network.
In the following example, we use Swift property to inject menu data for each menu item.
struct MenuItem: Identifiable {
let id = UUID()
let image: UIImage
let title: String
}
let menuItems: [MenuItem] = [
MenuItem(image: UIImage(systemName: "person.circle")!, title: "Account"),
MenuItem(image: UIImage(systemName: "power")!, title: "Sign Out")
]
var body: some View {
VStack {
ForEach(menuItems) { item in
MenuView(image: item.image, title: item.title)
}
}
}
You can easily support sarunw.com by checking out this sponsor.
Offline Transcription: Fast, privacy-focus way to transcribe audio, video, and podcast files. No data leaves your Mac.
Internal changes
We can categorize changes in SwiftUI into two categories, internal and external changes. We will start with the internal one.
@State
@State
is a source of truth designed for use locally in a view. Since it means to use internally in the view, Apple recommends marking it as private
to reinforce the idea that @State
is own and manage by that view only.
Declare @State
variable is a way to tell the framework to allocate persistence storage for variable and tracks it as a dependency. You get this for free by using @State
. All of this happens internally in the framework. @State
is one of many property wrappers SwiftUI provided for you as a tool in this declarative world.
Property Wrapper
Throughout the rest of this article, you will see a lot of new attributes prefixing with @
symbol, so it worth mentioning here. All of this new custom attribute is Property Wrapper.
Property Wrapper SE-0258 is one of many Swift evolution proposals which got implemented in Swift 5.1. In short, it is a mechanism to allow us to add additional behavior on a property when it is read or written (as we used to have with lazy
and @NSCopying
).
This property wrapper is where the magic happens. SwiftUI uses this extensively to facilitate the declaration of data and dependency. Each property wrappers have a different implementation detail, but all of them try to fulfill all the principles you have read in part 1 and 2. SwiftUI put property wrapper in use and make all of this possible.
Usage
@State
is designed for local/private changes inside a view, such as a button highlight or any internal view state.
struct Contact: Identifiable {
let id = UUID()
let number: String
let date: Date
let missCall: Bool
}
let contactItems: [Contact] = [
Contact(number: "089-xxx-xxxx", date: Date(), missCall: true),
Contact(number: "089-yyy-yyyy", date: Date(), missCall: false),
Contact(number: "089-zzz-zzzz", date: Date(), missCall: false)
]
struct ContactView: View {
let number: String
let date: Date
let missCall: Bool
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}()
var body: some View {
HStack {
Text(number).foregroundColor(missCall ? Color.red: Color.primary)
Spacer()
Text("\(date, formatter: Self.dateFormatter)")
}
}
}
struct ContactListView: View {
@State var isMissCall: Bool = false
var body: some View {
NavigationView {
List {
ForEach(contactItems) { contact in
if (self.isMissCall && contact.missCall) || !self.isMissCall {
ContactView(number: contact.number, date: contact.date, missCall: contact.missCall)
}
}
}.navigationBarItems(trailing: Button(action: {
self.isMissCall.toggle()
}, label: { Text("Miss call")}))
}
}
}
In this example, we use @State
to keep filter state, isMissCall
, to determine whether to show miss call only or not.
@Binding
We use @Binding
property wrapper to define an explicit dependency to a source of truth without owning it. With @Binding
, you can read and write to any data that bind to your @Binding
variable. The framework will make sure its always in sync.
Usage
@Binding
is a suitable tool for any view mean to be reusable, since the view doesn't care where that data comes from; it just knows how to render according to that data. Most standard SwiftUI components using this, e.g. Toggle
, TextField
, and Slider
.
public struct Toggle<Label>: View {
public init(
isOn: Binding<Bool>,
label: () -> Label
)
}
public struct TextField: View {
init(
_ text: Binding<String>
)
}
To put this in use, you initialize your view like this.
@State var bar: Bool = false
var body: some View {
Toggle("Toggle", isOn: $bar)
}
We use $
sign to get Binding
from @State
, this also come from a help of property wrapper.
Everything we saw so far is considered internal data and event since it happens within a view. @State
means to be local/private change within a view. @Binding
declares a dependency on the @State
. And actions we see so far are originated from a user interact directly with the view.
External changes
Publisher is a single abstraction for representing external changes to SwiftUI
External changes can refer to both interactions, e.g., Timer, Notification, and external source of truth, like your model object. Publisher[1] is a single abstraction for representing external changes to SwiftUI.
Publisher
Publisher[1:1] comes from Combine
framework[2] and acts as a single abstraction representation of external changes. To establish a dependency between Publisher and View, View has a method onReceive(_:perform:)
[3] to react to the incoming event.
Usage
The Publisher is a tool to bridge between the old world and the new world.
Several Foundation
types expose their functionality through publishers, including Timer
, NotificationCenter
, and URLSession
. Combine also provides a built-in publisher for any property that’s compliant with Key-Value Observing.
struct ListenerView: View {
@State var text: String = "Placeholder"
var body: some View {
TextField("Listener", text: $text)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { (output) in
self.text = "Keyboard will show"
}
}
}
ObservableObject protocol
ObjservableObject is a protocol that SwiftUI provided to expose your object to the SwiftUI as a source of truth. Think of it as a tool to equip your object with a goodness @State
get, but this time you manage the persistence storage yourself.
Usage
You use this when you want a reference type source of truth, which is great for the model you already have.
class Foo: ObservableObject {
@Published var show = false
}
You conform your class to ObservableObject
protocol and put @Published
property wrapper on a variable that you want to keep track of the change. That's all you need to do to make your existing class working in SwiftUI.
Behind the scene, ObservableObject
also use Publisher to emit change to interested parties. Like mentioned before, "Publisher is a single abstraction for representing external changes to SwiftUI".
// The above example translate to something like this.
class Bar: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var show = false {
willSet {
objectWillChange.send()
}
}
}
@ObservedObject
Just like we declare a dependency on @State
with @Binding
, we use @ObservedObject
to declare a dependency on ObservableObject
.
Usage
You use it just like @Binding
. The only difference is you use @ObservedObject
with an ObservableObject
. Which is suitable for the view that depends on a model object.
struct MyView: View {
@ObservedObject var model: MyModelObject
...
}
MyView(model: modelInstance)
You can also get Binding
from an individual property of ObservableObject with the following syntax.
$model.property
Which you can use with @Binding
.
var body: some View {
Toggle("Toggle", isOn: $model.booleanProperty)
}
@EnvironmentObject
@EnvironmentObject
is just another way of declaring a dependency on ObservableObject, but this time indirectly. With @ObservedObject
you have to pass your data around hop by hop, which might feel cumbersome in some cases where that model might need to be consumed in many places. With@EnvironmentObject
, you can inject that data from any ancestor view.
The downside of this is it won't be obvious on what ObservableObject
needs to be set. To figure it out, you might need to go through the view hierarchy to see which object is needed for @EnvironmentObject
. Failing to do this might cause run time error where @EnvironmentObject
is not correctly set.
Usage
contentView.environmentObject(foo)
The entire hierarchy of contentView can access foo data by declare @EnvironmentObject
struct SomeViewDownTheHeirarchy: View {
@EnvironmentObject var foo: Foo
...
}
@Environment
SwiftUI also provided many environment values[4] you use, e.g., colorScheme
, locale
, sizeCategory
. There might be a time when you need to adjust your view based on these values. When you want to do that, you can use @Environment
property wrapper to reads a value from the view’s environment.
Usage
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
...
}
You can override this environment value by injecting it to any view just like @EnvironmentObject
, and the entire view hierarchy will get that effect.
// An example to force view to be dark mode
contentView.environment(\.colorScheme, .dark)
Conclusion
SwiftUI changes the way we handle view and data. Many techniques we have learned all these years won't fit into this new paradigm. What I struggle the most when I first saw SwiftUI is the missing of a view controller and all the tools, e.g., delegate pattern, target-action. I hope these three parts series will give you a good foundation for more advanced topics in SwiftUI.
Related Resources
Data flow in SwiftUI, Part 1: The Data
Data flow in SwiftUI, Part 2: Views as a function of data
Data Flow Through SwiftUI
Property Wrapper SE-0258
You can easily support sarunw.com by checking out this sponsor.
Offline Transcription: Fast, privacy-focus way to transcribe audio, video, and podcast files. No data leaves your Mac.
References
Publisher https://developer.apple.com/documentation/combine/publisher ↩︎ ↩︎
Combine https://developer.apple.com/documentation/combine/ ↩︎
onReceive(_:perform:)
https://developer.apple.com/documentation/swiftui/modifiedcontent/3364555-onreceive ↩︎EnvironmentValues https://developer.apple.com/documentation/swiftui/environmentvalues ↩︎
Read more article about SwiftUI, iOS, Data, Property Wrapper, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareData in SwiftUI, Part 2: Views as a function of data
Part 2 in a series on understanding data in SwiftUI. We will talk about the key that makes principles in part 1 possible in SwiftUI. And how this resulting in a reduction of the complexity of UI development.
UINavigationBar changes in iOS13, Part2: UISearchController
Revisit of navigation bar appearance — this time with a UISearchController. If you have a search bar in your navigation bar, you might need to recheck when you build your app against iOS13.