Search code examples
swiftuibindingoption-type

Pass optional @State to non optional @Binding view variable


I'm trying pass optional @State in View with non optional @Binding for edit it there. I faced with problem Xcode is crushing with Fatal error: Unexpectedly found nil while unwrapping an Optional value. But when I check optional value before call that view it is not nil: Editing car is set: Optional("Audi A8"). I checked other SO advices how to solve that problem but nothing helps me to understand what is going wrong... How to pass @State correctly for edit it in SheetView?

enter image description here:

import SwiftUI

struct Car: Identifiable {
    let id = UUID().uuidString
    var model: String
    var color: String
}

class CarModelView: ObservableObject {
    @Published var cars: [Car] = [
        .init(model: "Audi A8", color: "Red"),
        .init(model: "Honda Civic", color: "Blue"),
        .init(model: "BMW M3", color: "Black"),
        .init(model: "Toyota Supra", color: "Orange")
    ]
}

struct CarListView: View {
    
    @StateObject var vm = CarModelView()
    @State var editingCar: Car?
    
    var body: some View {
        List {
            ForEach(vm.cars) { car in
                VStack(alignment: .leading) {
                    Text(car.model)
                        .font(.headline)
                    Text(car.color)
                        .font(.callout)
                }
                .swipeActions(edge: .trailing) {
                    Button {
                        setEditing(car: car)
                    } label: {
                        Label("Edit", systemImage: "pencil.circle")
                    }
                }
            }
            .sheet(item: $editingCar) {
                resetEditingCar()
            } content: { _ in
                SheetView(editingCar: Binding($editingCar)!) // Crash!
            }

        }
        .environmentObject(vm)
    }
    
    func setEditing(car: Car) {
        editingCar = car
        print("Editing car is set: \(String(describing: editingCar?.model))")
    }
    
    func resetEditingCar() {
        editingCar = nil
        print("Editing car should be nil: \(String(describing: editingCar?.model))")
    }
}

struct SheetView: View {
    
    @Binding var editingCar: Car
        
    var body: some View {
        VStack {
            Text("Edit Car Data")
            TextField("Model", text: $editingCar.model)
            TextField("Color", text: $editingCar.color)
        }
    }
}

Solution

  • Actually we don't need binding to state here, because it will edit nothing (after sheet close - state will be dropped, so all changes will be lost). Instead we need to transfer a binding to view model item into sheet.

    A possible solution is to iterate over view model bindings and use state of binding as activator to inject it as sheet's item into content.

    Tested with Xcode 13.4 / iOS 15.5

    demo

    Here is main part:

    @State var editingCar: Binding<Car>?    // << here !!
    
    var body: some View {
        List {
            ForEach($vm.cars) { car in     // << binding !!
                VStack(alignment: .leading) {
                    Text(car.wrappedValue.model)
                        .font(.headline)
                    Text(car.wrappedValue.color)
                        .font(.callout)
                }
                .swipeActions(edge: .trailing) {
                    Button {
                        setEditing(car: car)  // << binding !!
                    } label: {
                        Label("Edit", systemImage: "pencil.circle")
                    }
                }
            }
    
        }
        .sheet(item: $editingCar) {   // << sheet is here !!
            resetEditingCar()
        } content: {
            SheetView(editingCar: $0)  // non-optional binding !!!
        }
    

    Test module on GitHub