Passing Data Back from Modals

Sometimes you need data created in a modal to flow back to the parent. Here's how to handle that with closures.
Follow along with the code: iOS-Practice on GitHub
The Problem
A sheet presents a form. When submitted, the parent needs the result. How do you pass data back?
Closure Callback Pattern
struct AddProductSheet: View {
@Environment(\.dismiss) private var dismiss
@State private var productName = ""
let onAdd: (String) -> Void
var body: some View {
NavigationStack {
Form {
Section("Product Details") {
TextField("Product Name", text: $productName)
}
}
.navigationTitle("Add Product")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
onAdd(productName)
dismiss()
}
.disabled(productName.isEmpty)
}
}
}
}
}
Parent View Usage
struct ProductListView: View {
@State private var showAddSheet = false
@State private var products: [Product] = []
@State private var alertMessage = ""
@State private var showAlert = false
var body: some View {
List {
Button {
showAddSheet = true
} label: {
Label("Add Product", systemImage: "plus.circle.fill")
}
ForEach(products) { product in
Text(product.name)
}
}
.sheet(isPresented: $showAddSheet) {
AddProductSheet { newProductName in
// Handle the new product
alertMessage = "Added: \(newProductName)"
showAlert = true
}
}
.alert("Success", isPresented: $showAlert) {
Button("OK") {}
} message: {
Text(alertMessage)
}
}
}
Full Data Return
For complex data, define a struct:
struct NewProduct {
let name: String
let price: Double
let category: String
}
struct AddProductSheet: View {
@Environment(\.dismiss) private var dismiss
@State private var name = ""
@State private var price = ""
@State private var category = "Electronics"
let onAdd: (NewProduct) -> Void
var body: some View {
NavigationStack {
Form {
TextField("Name", text: $name)
TextField("Price", text: $price)
.keyboardType(.decimalPad)
Picker("Category", selection: $category) {
// ...
}
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
let product = NewProduct(
name: name,
price: Double(price) ?? 0,
category: category
)
onAdd(product)
dismiss()
}
}
}
}
}
}
Binding Alternative
For editing existing data, use a binding:
struct EditProductSheet: View {
@Binding var product: Product
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
TextField("Name", text: $product.name)
// Changes reflect immediately in parent
}
}
}
// Parent
.sheet(item: $productToEdit) { $product in
EditProductSheet(product: $product)
}
When to Use Each
| Pattern | Use Case |
|---|---|
| Closure callback | Creating new items |
| Binding | Editing existing items |
| EnvironmentObject | Shared state across many views |
Interview Tip
This demonstrates understanding of data flow in SwiftUI. The closure pattern keeps the modal independent and reusable.