SwiftUI Patterns

Passing Data Back from Modals

June 18, 2026
4 min read
Featured image for blog post: 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

PatternUse Case
Closure callbackCreating new items
BindingEditing existing items
EnvironmentObjectShared state across many views

Interview Tip

This demonstrates understanding of data flow in SwiftUI. The closure pattern keeps the modal independent and reusable.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch