Data in SwiftUI, Part 3: Tools

The last part in a series on understanding data in SwiftUI. See all tools SwiftUI provided to declare different types of data and dependency.

Before jump right into new tools, SwiftUI provided. Let have a visit to our old friend Swift 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.

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 )

}

}

}

Internal changes #

We can categorize changes in SwiftUI into two categories, internal and external changs. We will start with the internal one.

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

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

Since @State is the easiest form to declare a mutable state and source of truth, you might see a lot of tutorials using this. You can use it to keep your development going, but don't forget its real purpose and replace it with a more appropriate data type in the later phase of development.

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.

@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 is a single abstraction for representing external changes to SwiftUI.

Publisher comes from Combine framework and acts as a single abstraction representation of external changes. To establish a dependency between Publisher and View, View has a method onReceive(_:perform:) to react to the incoming event.

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.

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



class Bar : ObservableObject {

let objectWillChange = PassthroughSubject < Void , Never > ( )



var show = false {

willSet {

objectWillChange . send ( )

}

}

}

Just like we declare a dependency on @State with @Binding , we use @ObservedObject to declare a dependency on ObservableObject .

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

contentView . environmentObject ( foo )

The entire hierarchy of contentView can access foo data by declare @EnvironmentObject

struct SomeViewDownTheHeirarchy : View {

@ EnvironmentObject var foo : Foo

. . .

}

SwiftUI also provided many environment values 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.

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.



contentView . environment ( \ . colorScheme , . dark )

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

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

Tweet

Share

← Home