Presenting Sheets: Item, or a Boolean Binding?
This post is brought to you by Emerge Tools, the best way to build on mobile.
Quite often in SwiftUI, you’ve got to present another view in response to tapping some domain-specific data. Consider a simple model representing people:
struct Person: Identifiable {
let id = UUID()
var name: String
var age: Int
static var empty: Person {
get {
Person(name: "", age: 0)
}
}
}
And with it, the canonical list showing them:
struct PeopleView: View {
@State private var people: [Person]
var body: some View {
List(people) { person in
HStack {
VStack(alignment: .leading) {
Text(person.name)
.padding(.bottom, 2)
Text("\(person.age)")
.font(.caption)
}
}
}
.onAppear {
// Fetch `people` from an API, db, etc.
}
}
}
Now inevitably, there comes a time to edit a person. So, you show a modally presented view to do just that:
struct PersonEditor: View {
let person: Person
var body: some View {
// Omitted for brevity,
// But any sort of editing UI here would exist.
}
}
Here comes the crux of this little blurb. Previously, I had done something like this:
struct DirectoryView: View {
@State private var people: [Person]
// One bool to present the editor.
// One property to track the tapped person.
@State private var presentEditor: Bool = false
@State private var selectedPerson: Person = .empty
var body: some View {
List(people) { person in
Button {
selectedPerson = person
presentEditor.toggle()
} label: {
VStack(alignment: .leading) {
Text(person.name)
.padding(.bottom, 2)
Text("\(person.age)")
.font(.caption)
}
}
}
.sheet(isPresented: $presentEditor) {
PersonEditor(person: selectedPerson)
}
.onAppear {
// Fetch `people` from an API, db, etc.
}
}
}
And, while it works most of the time - it certainly has caused me several frustrating issues, and I don’t use it anymore for this particular scenario. The most prominent of these would be that the passed in @State
variable would be incorrect when the sheet was presented. I stepped through LLDB, wondering where I might’ve gone wrong.
I’d become even more flummoxed when I saw that my local @State
variable would appear correct before I toggled the boolean to present the sheet. In our example, that would be selectedPerson
.
Further still, I noticed that other times, it might be nil. What gives?
If your presentation requires a local data variable to control whether or not the view should present, use Sheet’s item
modifier instead of the above route. Here’s what that looks like:
struct DirectoryView: View {
@State private var people: [Person] = Person.peeps()
@State private var selectedPerson: Person? = nil
var body: some View {
List(people) { person in
Button {
selectedPerson = person
} label: {
VStack(alignment: .leading) {
Text(person.name)
.padding(.bottom, 2)
Text("\(person.age)")
.font(.caption)
}
}
}
.sheet(item: $selectedPerson) { person in
PersonEditor(person: person)
}
}
}
Bingo. Now, the relevant variable is always in the state that it should be, no surprises - and we get to lose the extra boolean @State
variable to control presentation. The “item” approach inherently controls presentation and dependency injection at the same time based off of whether or not its relevant binding is nil.
let concatenatedThoughts = """
It's also funny that I use this approach all the time with `enum` cases. For whatever reason, it didn't stick in my head to use it with data models.
"""
There are multiple explanations floating around about why this works and the other way doesn’t, long threads over SwiftUI’s internals and what not - but all I know is this, it does work.
Sure enough, if you R.T.F.M.1 - that’s exactly what it says on the tin, too:
Use this method when you need to present a modal view with content from a custom data source.
As soon as I realized this, I went back and ameliorated swathes of code. Maybe this is common knowledge in the community, but it took a good hour from my development life. I hope this post gives you one back.
Until next time ✌️
-
Read the fancy manual, of course. ↩