When a user does an action, that causes a mutation of state, SwiftUI views re-render themselves. Data flows in one direction - always into the body of the view - and this is how data flows in SwiftUI.
When working with user interactions like these, we will often track these changes using value types:
- Property - Immutable property that never changes.
- @State - Transient data owned by the view.
- @Binding - For mutating data owned by another view.
Value types are instances where each type keeps a unique copy of its data (struct
, enum
, tuple). They are good for tracking local, simple state changes in views. And their states can be bound to SwiftUI controls.
- Simply pass in the read-only value type you want to be reflected locally in your view.
import SwiftUI
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct PlayerView: View {
var isPlaying: Bool = true // 1. Define your property
var body: some View {
Button(action: {
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
}
}
}
struct ContentView: View {
var body: some View {
PlayerView(isPlaying: false) // 2. Pass it into your view.
}
}
This works until your property wants to change.
struct PlayerView: View {
var isPlaying: Bool = true
var body: some View {
Button(action: {
self.isPlaying.toggle() // 💥 `self` is immutable
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
To handle this we need something that can synthesize and track state in a struct
. This is where we use @State
.
- A read-write property wrapper for keeping track of transient state owned by the view.
import SwiftUI
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct PlayerView: View {
@State private var isPlaying: Bool = false // 1. Define your state 🚀.
var body: some View {
Button(action: {
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
}
}
}
struct ContentView: View {
var body: some View {
PlayerView() // 2. No need to set down here.
}
}
- How we pass
@State
down to subviews, and bi-directionally bind it back.
import SwiftUI
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct PlayerView: View {
@State private var isPlaying: Bool = false // 1. Define State
var body: some View {
VStack {
PlayButton(isPlaying: $isPlaying) // 2. Pass with `$`
Toggle(isOn: $isPlaying) {
Text("Hello World")
}
}.padding()
}
}
struct PlayButton: View {
@Binding var isPlaying: Bool // 3. Define Binding
var body: some View {
Button(action: {
self.isPlaying.toggle() // 4. Bi-directional
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
}
}
}
struct ContentView: View {
var body: some View {
PlayerView()
}
}
SwiftUI controls take Binding
property wrappers in their initializers to bind to the external properties you define.
struct Toggle<Label>: View {
public init(
isOn: Binding<Bool>,
label: () -> Label
)
}
@State
and @Binding
can also be used with structs
you create.
import SwiftUI
// Define your struct
struct Book {
var title: String
var author: String
}
// Inject into parent view at the top
struct ContentView: View {
@State var book: Book // 1
var body: some View {
DetailView(book: $book)
}
}
// Then explicitly pass binding to each child
struct DetailView: View
{
@Binding var book: Book // 2
var body: some View {
VStack {
DetailHeader(book: $book)
}
}
}
// And subview (tighter coupling)
struct DetailHeader: View
{
@Binding var book: Book // 3
var body: some View {
VStack {
Text(book.title)
Text(book.author)
Button(action: {
book.author = "Jonathan"
}) {
Text("Tap me!")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let book = Book(title: "Lord of the Rings",
author: "Tolkien")
ContentView(book: book)
}
}
This will update the UI when the button is tapped because the views are bound to the Book
struct
.
Reference type - instances share a single copy of the data (class
).
- Sometimes data changes externally for our views. And we need a way of being notified.
- As good as
structs
are, because they can't share the same instance of data (their values are copied), reference types likeclass
are instead used to publish and share external data changes to SwiftUI views. - To facilitate this sharing of data, SwiftUI created a
ObservableObject
protocol, aclass
implementing that protocol, can be observed from any other view.
ObservableObject.swift
public protocol ObservableObject : AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher { get }
}
- A type of object with a publisher that emits before the object has changed.
- Enforces its implementers be classes via
AnyObject
extension. - Synthesizes an
objectWillChange
publisher that emits the changed value before any of its @Published properties changes.
SwitUI has three property wrappers for receiving updates from observerable objects:
- @ObservedObject
- @StateObject
- @EnvironmentObject
- Blows away and recreates the view entirely whenever there is a change in state.
Say we have a struct
struct Book {
var title: String
var author: String
}
that we want to turn into an @ObservableObject
:
class Book: ObservableObject {
@Published var title: String
@Published var author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
Once that struct
becomes a class
implementing @ObservableObject
we can observe it like this.
import SwiftUI
// Define your observable
class Book: ObservableObject {
@Published var title: String
@Published var author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
// Inject into parent view at the top
struct ContentView: View {
@ObservedObject var book: Book // 1
var body: some View {
DetailView(book: book)
}
}
// And then explicitly pass to each child
struct DetailView: View{
@ObservedObject var book: Book // 2
var body: some View {
VStack {
DetailHeader(book: book)
}
}
}
// And subview (tighter coupling)
struct DetailHeader: View{
@ObservedObject var book: Book // 3
var body: some View {
VStack {
Text(book.title)
Text(book.author)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let book = Book(title: "Lord of the Rings", author: "Tolkien")
ContentView(book: book)
}
}
- Mechanically the same as
@ObservedObject
only@StateObject
maintains its state. Doesn't invalidate when view recreated. - Standard usage is to define
@StateObject
at the top in the view that creates it, and then pass it down as@ObservedObject
through subviews to the bottom.
import SwiftUI
// Define your observable
class Book: ObservableObject {
@Published var title: String
@Published var author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
// Inject into as @StateObject in initializing view at the top
struct ContentView: View {
@StateObject var book: Book // 1
var body: some View {
DetailView(book: book)
}
}
// And then receive as ObservedObject in subviews
struct DetailView: View
{
@ObservedObject var book: Book // 2
var body: some View {
VStack {
DetailHeader(book: book)
}
}
}
// And subview (tighter coupling)
struct DetailHeader: View
{
@ObservedObject var book: Book // 3
var body: some View {
VStack {
Text(book.title)
Text(book.author)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let book = Book(title: "Lord of the Rings", author: "Tolkein")
ContentView(book: book)
}
}
@ObservedObject
doesn't maintain state when a new view is created. It recreates itself as new object every time.
@StateObject
maintains the state of the observable object and won't recreate a new one every time.
- Which one you use depends on your use-case. Sometimes you want a refreshed data source. Sometimes you don't.
import SwiftUI
class DataSource: ObservableObject {
@Published var counter = 0
}
struct Counter: View {
@StateObject var dataSource = DataSource() // view maintains state
var body: some View {
VStack {
Button("Increment counter") {
dataSource.counter += 1
}
Text("Count is \(dataSource.counter)")
}
}
}
struct ItemList: View {
@State private var items = ["hello", "world"]
var body: some View {
VStack {
Button("Append item to list") {
items.append("test")
}
List(items, id: \.self) { name in
Text(name)
}
Counter() // new create view
}
}
}
struct ContentView: View {
var body: some View {
VStack {
ItemList()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
- Like
@ObservedObject
and@StatObject
but without the coupling. - Here you inject
EnvironmentObject
at the top of your view, and it will magically be made available to all subviews without having to explicitly reference. - Handy for getting to those hard to reach places.
Set it up like this.
//
// ContentView.swift
// Demo
//
// Created by jrasmusson on 2021-06-16.
//
import SwiftUI
// Define your observable
class Book: ObservableObject {
@Published var title: String
@Published var author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
// Inject once at the top
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let book = Book(title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams")
ContentView()
.environmentObject(book) // 1
}
}
// Define once at the top
struct ContentView: View {
// No need to define
var body: some View {
DetailView()
}
}
// Automtically available
struct DetailView: View {
// No need to reference
var body: some View {
VStack {
DetailHeader()
}
}
}
// Pull out when needed
struct DetailHeader: View {
@EnvironmentObject var book: Book // 2
var body: some View {
VStack {
Text(book.title)
Text(book.author)
}
}
}
SwiftUI gives us both @Environment
and @EnvironmentObject
property wrappers, but they are subtly different: whereas @EnvironmentObject
allows us to inject arbitrary values into the environment, @Environment
is specifically there to work with SwiftUI’s own pre-defined keys.
For example, @Environment
is great for setting contextual information about your views that flows down the view hierarchy and changes different aspects of any contained views all at once.
For example we could change the size category of all our views to extra large and see what that looks like in our previews like this.
ContentView(store: RoomStore(rooms: testData))
.environment(\.sizeCategory, .extraExtraExtraLarge)
So Environment
for iOS type style things about the look of our controls. EnvironmentObject
for app data we control and want to share.