Data in SwiftUI, Part 3: Tools

⋅ 9 min read ⋅ SwiftUI iOS Data Property Wrapper

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.

  1. Data flow in SwiftUI, Part 1: Data
  2. Data flow in SwiftUI, Part 2: Views as a function of data
  3. 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.

Sponsor sarunw.com and reach thousands of iOS developers.

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.

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.

Sponsor sarunw.com and reach thousands of iOS developers.

References


  1. Publisher https://developer.apple.com/documentation/combine/publisher ↩︎ ↩︎

  2. Combine https://developer.apple.com/documentation/combine/ ↩︎

  3. onReceive(_:perform:) https://developer.apple.com/documentation/swiftui/modifiedcontent/3364555-onreceive ↩︎

  4. 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 Share
Previous
Data 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.

Next
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.

← Home