Managing Data Flow in SwiftUI
Last week we talked about Animations and Transitions in SwiftUI. But it’s time to touch the crucial aspect of every app, and it is data. All the apps have data to present or mutate. Data plays a vital role in apps using SwiftUI. Every view in SwiftUI is just a function of some state, where the state is our data.
Fetching data from local/remote storage
Today we will build a small app which uses core SwiftUI concepts like Binding and BindableObject. Assume that you work on the app, which has two primary responsibilities:
- Fetch and show the list of employees from local or remote storage
- Edit personal information about selected employee
Let’s start with describing our model layer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import SwiftUI
import Combine
struct Person: Identifiable {
let id: UUID
var name: String
var age: Int
}
final class PersonStore: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var persons: [Person] = [] {
willSet {
DispatchQueue.main.async {
self.willChange.send()
}
}
}
func fetch() {
// Fetch your data from real storage here
persons = [
.init(id: .init(), name: "Majid", age: 27),
.init(id: .init(), name: "John", age: 31),
.init(id: .init(), name: "Fred", age: 25)
]
}
}
Here we have simple Person struct which conforms Identifiable protocol. The single requirement of Identifiable is Hashable id field. We implement it by defining id as UUID. We also can use Int instead of UUID.
Next, we can implement PersonStore class, which is providing data for our view. PersonStore type conforms to BindableObject it will allow SwiftUI to refresh the view as soon as we notify it by using willChange Subject. We send a Void value to willChange Subject before every mutation on persons array.
Now let’s take a look at PersonListView.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct PersonsView : View {
@ObjectBinding var store: PersonStore
var body: some View {
NavigationView {
List(store.persons) { person in
VStack(alignment: .leading) {
Text(person.name)
.font(.headline)
Text("Age: \(person.age)")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.onAppear(perform: { self.store.fetch() })
.navigationBarTitle(Text("Persons"))
}
}
}
We use List component to present an array of Person structs. Every row in List contains VStack with two Text components representing the name and age of a Person. We call fetch method on store object as soon as List appears. As you remember, our PersonStore object notifies SwiftUI about data change by using Subject, and SwiftUI rebuilds the view to present newly fetched data.
Editing
Next step is creating a new view which allows us to edit personal information of selected Person. We will use Form component to show nice form for data entry. You can check my previous post to learn more about Form component and its advantages. Let’s dive into code which represents editing view.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct EditingView: View {
@Environment(\.isPresented) var isPresented: Binding<Bool>
@Binding var person: Person
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal information")) {
TextField($person.name)
Stepper(value: $person.age) {
Text("Age: \(person.age)")
}
}
Section {
Button(action: { self.isPresented?.value.toggle() }) {
Text("Save")
}
}
}.navigationBarTitle(Text(person.name))
}
}
}
Here we use Binding for selected person item. Binding Property Wrapper allows passing a reference to a value type. By using Binding property, EditingView can read and write to the Person struct, but it doesn’t store a copy of it. We use this Binding to mutate value inside PersonsStore and as soon as we do that SwiftUI will update the view with the updated list of Persons. If you want to learn more about Property Wrappers available in SwiftUI like @Binding, @Environment, @EnvironmentObject, @ObjectBinding, please take a look at the dedicated post.
Now let’s refactor our PersonsView to support editing by passing Binding to a selected Person inside EditingView. For that, we will use PresentationButton to present a view in a new beautiful cart interface available in iOS 13.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct PersonsView : View {
@ObjectBinding var store: PersonStore
var body: some View {
NavigationView {
List {
ForEach(0..<store.persons.count) { index in
PresentationButton(destination: EditingView(person: self.$store.persons[index])) {
VStack(alignment: .leading) {
Text(self.store.persons[index].name)
.font(.headline)
Text("Age: \(self.store.persons[index].age)")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
.onAppear(perform: { self.store.fetch() })
.navigationBarTitle(Text("Persons"))
}
}
}
And here is the screenshot of our app, you can see how it looks.
Conclusion
Today we built simple Master-Detail flow in SwiftUI. I’ve tried to show the power of Bindings in SwiftUI. You don’t need to post notifications or observe key-value to indicate changes in your User Interface, all you need is using correct Property Wrapper provided by SwiftUI. Again, if you want to learn when and which one should be used, check out my post about Property Wrappers in SwiftUI.