bchards
bchards

Reputation: 401

SwiftUI: TextBox text field not showing bound value until clicked

I am trying to create a view where a user can edit an item inside of a list. The list is displayed using a ForEach loop from a bound list variable, where each item is wrapped in a NavigationLink to a View that can be used to edit the given item.

The first issue I ran into was that the edit view was binding directly to the element in the list, meaning that when I edited it, it forced a reload of the previous list View and threw you out of the edit view after each character typed. To get over this I though that I could duplicate the data inside of the bound variable, set this as a new @State variable, which would hold the updated information the user enters, and then once they are finished, update the bound variable in one go.

This is working for me, bar one small issue, which is that the variable in the TextField does not reflect the actual information of the bound variable, until you tap it, then it updates with the right value. Is there something I can do so that I do not have to tap for the correct value to show up? It seems odd behaviour to me, because I am also using a ColorPicker and this always has the correct value.

class AppData: ObservableObject {
    @Published var things: [Thing] = [
        Thing(name: "1", color: .red),
        Thing(name: "2", color: .blue)
    ]
}

@main
struct ThingApp: App {
    @ObservedObject private var appData = AppData()
    
    var body: some Scene {
        WindowGroup {
            ViewOne(things: $appData.things)
        }
    }
}


struct ViewOne: View {
    @Binding var things: [Thing]
    
    var body: some View {
        NavigationView {
            ViewTwo(things: $things)
        }
    }
}

struct ViewTwo: View {
    @Binding var things: [Thing]

    var body: some View {
        NavigationLink(destination: ThingListView(things: $things)) {
            Image(systemName: "list.bullet")
                .font(.system(size: 20))
        }
    }
}

struct ThingListView: View {
    @Binding var things: [Thing]
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach($things) { $thing in
                    NavigationLink(destination: ThingEditView(thingData: $thing.data)) {
                        Text(thing.name)
                    }
                }
            }
        }
    }
}

struct ThingEditView: View {
    @Binding var thingData: Thing.Data
    @State private var newData: Thing.Data = Thing.Data()
    
    var body: some View {
        List {
            Section(header: Text("Details")) {
                TextField("Name", text: $newData.name)
                ColorPicker("Color", selection: $newData.color)
            }
        }
        .onAppear {
            newData.name = thingData.name
            newData.color = thingData.color
        }
        .onDisappear{
            thingData = newData
        }
    }
}

struct Thing: Identifiable {
    let id: UUID
    var name: String
    var color: Color
    
    struct Data {
        var name: String = ""
        var color: Color = .green
    }
    
    var data: Data {
        get { return Data(name: self.name, color: self.color)}
        set {
            name = newValue.name
            color = newValue.color
        }
    }
    
    internal init (data: Data) {
        self.id = UUID()
        self.name = data.name
        self.color = data.color
    }
    
    internal init (
        id: UUID = UUID(),
        name: String,
        color: Color
    ) {
        self.id = id
        self.name = name
        self.color = color
    }
}

Upvotes: 0

Views: 833

Answers (1)

ChrisR
ChrisR

Reputation: 12165

IMHO you don't need the whole data stuff in your Thing. You can just pass down the thing from the NavigationLink like this in ThingListView:

   NavigationLink(destination: ThingEditView(thing: $thing)) {

and do the following in ThingEditView:

struct ThingEditView: View {
    @Binding var thing: Thing
    
    @State private var newData: Thing  // can be of type Thing too
    
    init(thing: Binding<Thing>) {  // the init puts the initial values into newData
        self._thing = thing
        self._newData = State(initialValue: thing.wrappedValue)
    }
    
    var body: some View {
        List {
            Section(header: Text("Details")) {
                TextField("Name", text: $newData.name)
                ColorPicker("Color", selection: $newData.color)
            }
        }
        .onDisappear{
            thing = newData
        }
    }
}

On a general note you might use .environmentObject instead of passing down thing from view to view.

Upvotes: 1

Related Questions