Question 9
Modals: ownership, boundaries, and passing data back
You need to show a second screen on top of the current one and hand information back when it finishes (or cancel without changing anything). How do you decide where state lives, how the two screens should talk to each other, and what usually goes wrong if that contract is vague?
Answer outline
Two roles: the presenting screen owns whether the modal is showing and what happens when it closes. The presented screen is where the user edits a draft, picks an option, or confirms.
Keep that split explicit. If the presented screen silently mutates the parent’s model while open with no agreed rule, you get subtle bugs and unclear reviews.
Data in: pass an initial value or a reference to shared state, whatever the child needs to start. If both sides should reflect edits immediately, they should share the same draft or store, not two copies.
Data out: agree an explicit contract: callback or delegate with a result (value vs cancel), apply once on dismiss, or a binding / shared observable when there is truly one source of truth.
What goes wrong: two copies of the same field (drift); retain cycles between presenter and modal; saving after the user chose cancel; async work that finishes after dismiss and still writes; stacked or overlapping presentations so identity ('which modal is this?') gets confused.
Principles
- Commit vs live: does the parent change only when the modal completes, or during the presentation? That choice drives callback-style vs shared state.
- One return path per piece of data: delegate, closure, or one shared model, not every pattern at once.
Child doesn’t have to own the parent’s model forever. Finish with one outcome (saved value vs cancelled) so the parent applies updates in one place.
struct Parent: View {
@State private var item = Item.default
@State private var editorPresented = false
var body: some View {
ContentView()
.sheet(isPresented: $editorPresented) {
ItemEditor(
initial: item,
onFinish: { updated in
item = updated
editorPresented = false
},
onCancel: { editorPresented = false }
)
}
}
}
When the modal is a live editor for shared data, one object (observable, view model, or binding) avoids copying fields that can get out of sync.
struct ItemEditor: View {
@Binding var item: Item
// edits write directly into parent's item while sheet is visible
}