1

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...

struct Group: Identifiable, Codable {
    var id = UUID()
    var name: String
    var number: Int
    var spaces: Bool
    var businesses: [Business]
}

struct Business: Identifiable, Codable {
    var id = UUID()
    var name: String
    var address: String
    var space: Int
    var enabled: Bool
    
}

These are used in a class with an Observable var that stored in User Defaults

class GroupSettings: ObservableObject {
    
    @Published var groups = [Group]() {
        didSet {
            UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
        }
    }

    init() {
        if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
           if let userDefaultConfig = try?
              PropertyListDecoder().decode(Array<Group>.self, from: configData){
                  groups = userDefaultConfig
              }
        }
     }
}

Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...

I pass a Group from the first view which has the array of Groups in to the detail screen with the @Binding property ...

@Binding var group: Group

var body: some View {

       TextField("", text: $group.name)     <---- WORKS

       List {
           ForEach(self.group.businesses){ business in
           
              if business.enabled {

                 Text(business.name)               <---- WORKS
                 TextField("", business.address)   <---- FAILS
                 TextField("", value: business.space, formatter: NumberFormatter())   <---- FAILS

              } else {

                 Text("\(business.name) is disabled"
              }
           }
       }
}

Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.

Thanks in advance!

Plasma
  • 2,622
  • 2
  • 20
  • 35
  • Try reading https://stackoverflow.com/questions/63839270/swiftui-getting-textfield-to-array-in-foreach it shows you how to creating a binding with an array. What I can see is that business.address is NOT a binding variable – NotAPhoenix Jan 24 '21 at 15:03
  • @NotAPhoenix I've had a look at the link you posted, but as I said in the question I'm new to Swift and I'm struggling to understand how that link helps my use case. There aren't any embedded arrays of structs, and I can't understand how to apply that solution to my problem. If you have time could you you maybe add an answer with a little more context or some pointer to help me in the right direction. – Plasma Jan 24 '21 at 19:38

2 Answers2

2

You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...

List {
    ForEach(group.businesses.indices) { index in
        TextField("", text: $group.businesses[index].address)
    }
}
davidev
  • 7,694
  • 5
  • 21
  • 56
  • Hi thanks for the answer, it works great for the TextField however, it breaks the Text option which then says 'Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'' , also when I try to use business[index].enabled I once again get the 'cannot convert to Binding'. – Plasma Jan 24 '21 at 12:13
  • I've updated the question with the Bool use case. – Plasma Jan 24 '21 at 12:35
  • @Plasma You need to use `group.businesses[index]` like in: `Text("\(group.businesses[index].name) is disabled")` and `if group.businesses[index].enabled { ... }` – pawello2222 Jan 25 '21 at 16:02
  • @pawello2222 thanks, it looks like doing that has resolved most of my issues, however it seems like a very long winded way of doing it. I thought Swift was meant to simply the syntax. :'( – Plasma Jan 25 '21 at 19:29
1

An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:

struct TestView: View {
    @Binding var group: Group

    var body: some View {
        TextField("", text: $group.name)
        List {
            let items = Array(zip(group.businesses.indices, group.businesses))
            ForEach(items, id: \.1.id) { index, business in
                if business.enabled {
                    Text(business.name)
                    TextField("", text: $group.businesses[index].address)
                } else {
                    Text("\(business.name) is disabled")
                }
            }
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • So I've used a mixture of both answers, using @davidev .indices solution and then doing a let business = group.businesses[index] inside the ForEach. I can use the long hand $group.businesses[index].address for the TextFields and the shorter business.enabled for the others. It's still not as pretty as I might want it, but it works which is the main thing. Thank you both for your answers! – Plasma Jan 25 '21 at 20:28