2

I want a dynamic array of mutable strings to be presented by a mother view with a list of child views, each presenting one of the strings, editable. Also, the mother view will show a concatenation of the strings which will update whenever one of the strings are updated in the child views.

Can't use (1) ForEach(self.model.strings.indices) since set of indices may change and can't use (2) ForEach(self.model.strings) { string in since the sub views wants to edit the strings but string will be immutable.

The only way I have found to make this work is to make use of an @EnvironmentObject that is passed around along with the parameter. This is really clunky and borders on offensive.

However, I am new to swiftui and I am sure there a much better way to go about this, please let know!

Here's what I have right now:

import SwiftUI

struct SimpleModel : Identifiable { var id = UUID(); var name: String }
let simpleData: [SimpleModel] = [SimpleModel(name: "text0"), SimpleModel(name: "text1")]
final class UserData: ObservableObject { @Published var simple = simpleData }

struct SimpleRowView: View {
  @EnvironmentObject private var userData: UserData
  var simple: SimpleModel
  var simpleIndex: Int { userData.simple.firstIndex(where: { $0.id == simple.id })! }
  var body: some View {
    TextField("title", text: self.$userData.simple[simpleIndex].name)
  }
}

struct SimpleView: View {
  @EnvironmentObject private var userData: UserData
  
  var body: some View {
    let summary_binding = Binding<String>(
      get: {
        var arr: String = ""
        self.userData.simple.forEach { sim in arr += sim.name }
        return arr;
      },
      set: { _ = $0 }
    )
    
    return VStack() {
      TextField("summary", text: summary_binding)
      ForEach(userData.simple) { tmp in
        SimpleRowView(simple: tmp).environmentObject(self.userData)
      }
      Button(action: { self.userData.simple.append(SimpleModel(name: "new text"))}) {
        Text("Add text")
      }
    }
  }
}

Where the EnironmentObject is created and passed as SimpleView().environmentObject(UserData()) from AppDelegate.

EDIT:

For reference, should someone find this, below is the full solution as suggested by @pawello2222, using ObservedObject instead of EnvironmentObject:

import SwiftUI

class SimpleModel : ObservableObject, Identifiable {
  let id = UUID(); @Published var name: String
  init(name: String) { self.name = name }
}

class SimpleArrayModel : ObservableObject, Identifiable {
  let id = UUID(); @Published var simpleArray: [SimpleModel]
  init(simpleArray: [SimpleModel]) { self.simpleArray = simpleArray }
}

let simpleArrayData: SimpleArrayModel = SimpleArrayModel(simpleArray: [SimpleModel(name: "text0"), SimpleModel(name: "text1")])

struct SimpleRowView: View {
  @ObservedObject var simple: SimpleModel
  var body: some View {
      TextField("title", text: $simple.name)
  }
}

struct SimpleView: View {
  @ObservedObject var simpleArrayModel: SimpleArrayModel
  
  var body: some View {
    let summary_binding = Binding<String>(
      get: { return self.simpleArrayModel.simpleArray.reduce("") { $0 + $1.name } },
      set: { _ = $0 }
    )
    
    return VStack() {
      TextField("summary", text: summary_binding)
      ForEach(simpleArrayModel.simpleArray) { simple in
        SimpleRowView(simple: simple).onReceive(simple.objectWillChange) {_ in self.simpleArrayModel.objectWillChange.send()}
      }
      Button(action: { self.simpleArrayModel.simpleArray.append(SimpleModel(name: "new text"))}) {
        Text("Add text")
      }
    }
  }
}
MrTeapot
  • 23
  • 3
  • What do you mean by "set of indices may change"? – New Dev Jun 29 '20 at 22:30
  • @NewDev When adding a new text to the array, the array's index set will change and a ForEach on indices is only allowed if the set of indices is constant. – MrTeapot Jun 30 '20 at 10:03

1 Answers1

0

You don't actually need an @EnvironmentObject (it will be available globally for all views in your environment).

You may want to use @ObservedObject instead (or @StateObject if using SwiftUI 2.0):

...
return VStack {
    TextField("summary", text: summary_binding)
    ForEach(userData.simple, id:\.id) { tmp in
        SimpleRowView(userData: self.userData, simple: tmp) // <- pass userData to child views
    }
    Button(action: { self.userData.simple.append(SimpleModel(name: "new text")) }) {
        Text("Add text")
    }
}
struct SimpleRowView: View {
    @ObservedObject var userData: UserData
    var simple: SimpleModel
    ...
}

Note that if your data is not constant you should use a dynamic ForEach loop (with an explicit id parameter):

ForEach(userData.simple, id:\.id) { ...

However, the best results you can achieve when you make your SimpleModel a class and ObservableObject. Here is a better solution how do do it properly:

SwiftUI update data for parent NavigationView

Also, you can simplify your summary_binding using reduce:

let summary_binding = Binding<String>(
    get: { self.userData.simple.reduce("") { $0 + $1.name } },
    set: { _ = $0 }
)
pawello2222
  • 46,897
  • 22
  • 145
  • 209