Using .sheet to produce different screens
[SwiftUI
swift
iOS
swift
]
Frequently, we use .sheet
to deterministically produce a given view, and we link the process to a binding boolean. But, we can bind an optional source of truth for the sheet. That way, when the binding optional is set to a non-nil value, the system calls the responsible view, passing the value, and asking for a view to populate the modal. And if the value is set to nil, then the system dismisses the modal.
Let’s say we have a Home Screen, and we want to give our users access to a settings screen with a bar button. That would be easy, we can bind such control with a boolean, and link our settings sheet to that same boolean.
struct ContentView: View {
@State var showingSettings = false
var body: some View {
Button(action: {
self.showingSettings.toggle()
}) {
Text("Show Settings")
}.sheet(isPresented: $showingSettings) {
SettingsView()
}
}
}
That’s fine. But now, we want to show a list of, let’s say, news stories, and we want to give our users the chance to annotate them. In each cell of our list, we have a button that should trigger a popup linked to the story.
We can pass the selected story using environmental variables, and toggle a bool to produce the popup, and that should be fine, a little messy, but fine.
But, what if we want to be able to produce more than one type of popups from the same root view? Well, we can let .sheet
do the heavy lifting for us.
First, we need a type that can adopt different values and convey different meanings.
enum ActiveSheet: Identifiable {
case settings
case selectedStory(Story)
var id: Int {
switch self {
case .settings:
return 1
case .selectedStory(_):
return 2
}
}
}
That’s going to be the type of our source of truth.
@State var activeSheet: ActiveSheet?
If activeSheet
is nil, no popup is shown, if we have a concrete value, the system will call the closure defined as content in sheet(item:onDismiss:content:)
. The system will call the closure with the corresponding value:
.sheet(item: $activeSheet, content: { (item: ActiveSheet) -> AnyView in
switch item {
case .settings:
return AnyView(SettingsView())
case .selectedStory(let story):
return AnyView(AnnotateStory(story))
}
})
We can use the same technique to pass the story, even if we don’t need to differentiate popup types, but just to have the story linked without environmental variables.
One more thing, if the binding optional is changed while the system is showing a modal corresponding to a given value (we have on-screen the modal associated with type A, and the source of truth is changed to B), then the system will dismiss the first modal, and produce a new modal corresponding to the appropriate value. How cools is that?