Reputation: 401
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
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