19

I'm using an ObservableObject 'DataStore', which contains an array ('exampleList') of objects ('exampleObject').

@Published exampleList = [exampleObject]()

I'm calling the DataStore via @EnvironmentObject ('dataStore').

@EnvironmentObject var dataStore = DataStore()

Then I iterate the list with

ForEach(0..<dataStore.exampleList.count) { index in ....

To bind element of item to a detail view, I'm doing like this:

DetailView(itemBinding: $dataStore.exampleList[index])

Until Xcode11 beta 4, it worked perfectly. Since XCode11 beta 5, it still works but Xcode gives me this alert:

'subscript(_:)' is deprecated: See Release Notes for a migration path

I tried with simpler stuff, with a simple @State var containing an array of strings, and it's the same issue: when calling an element of this array, and trying to use the value into a TextField:

TextField("test", text: $test[0])

I get the same alert.

I don't understand how to fix it. Does that mean that we no longer can bind values inside an array? Then, how can we iterate an array and bind a specific item?

This is my first question on Stack Overflow, I apologize if my question is clumsy... Thanks a lot for your answers, I'm using Stack Overflow for years, it's amazing, I always find existing and helpful answers, but it is the first time I can't find any, that's why I'm asking.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
hasselfred
  • 191
  • 1
  • 7
  • First, good question. I upvoted this because I'm getting a similar warning with a `Slider`. I found the same thing as you - if you bind to an array in your `ObservableObject` something is wrong (but it still works) , but if you bind to a local state variable everything is fine (but then how do you update the model?). I have a sample project if you wish to check it out (ignore the name, that was an old issue). https://github.com/justdfd/ListBug If I find an answer, I'll post it. –  Aug 02 '19 at 13:22
  • @dfd, Thanks for your comment, I'm pleased to know that I'm not alone in the world to have this issue. I still don't understand why, because using arrays seems to be a common situation for a model with complex data. For me, the interest of ObservableObject is to refresh all the parent views when modifying an element of an array. If I can't do that way anymore, what is the good way ? – hasselfred Aug 02 '19 at 14:03
  • This wasn't what I wanted to work on today, but after spending an hour (of my life that I'll never get back) I couldn't find the answer. I listen to a few Apple-related podcasts, and yesterday one was talking about how "the software" is way behind "the hardware" and they were pondering what Apple may do come September. My take? My app broke BIG time in both beta 4 and 5 because of changes that make no sense to me. Right now I'm looking at (a) something that works with a warning and (b) no idea of what changes are coming with beta 6 in 10 days. I'll monitor your question in the meantime. –  Aug 02 '19 at 14:14
  • From the release notes in beta 5: **The Binding structure’s conditional conformance to the Collection protocol is removed. (51624798)**. To be precise, it wasn't removed, it was deprecated. That is why it still works. If they follow the same pattern as with other beta deprecations, they will probably remove it for good in beta 6, or 7. I haven't got the time to analyze the alternatives. The release notes put some examples, but they seem too much work to achieve something that was so easy until now. My bet is (cont'd) – kontiki Aug 02 '19 at 14:45
  • My bet is things will change again in beta 6 or 7. Maybe the conformance to the Collection won't come back, but something probably will be offered. While it still works, I do not want to waste too much time investigating it. It may all be in vain if there's another change soon. For the time being, I think I'll leave it be and worry when they finally remove it. – kontiki Aug 02 '19 at 14:48
  • @kontiki : thanks for your comment, so let's wait and see... – hasselfred Aug 02 '19 at 14:55
  • @kontiki, I worked through what they noted and wasn't able to see where it makes a difference in `ObservableObject`. I removed the issue with a local `State` array, but that's about it. It's been frustrating - worse than Swift 1.0 beta if you ask me. I've decided to put my "new" SwiftUI app away - I'm now at a point that I'd be further along (UI included) doing it in `UIKit`. I'll try to keep up with a `SwiftUI` version, but this "every two weeks my app breaks" isn't working for me. I'll wait until Apple and Swift OSS (or whatever it's called) decide on a stable base. –  Aug 02 '19 at 17:21
  • @dfd I understand your feeling.If your curious, I posted an answer with what I think, is the proper way of getting rid of the warnings. – kontiki Aug 02 '19 at 20:43

2 Answers2

16

Xcode 11, beta 6 UPDATE:

Good news! Just as I suspected, in beta 6, the Binding conformance to MutableCollection has been been replaced with something else. Instead of conforming to MutableCollection, it now let your access the elements via @dynamicMemberLookup. The result is you now can keep doing $text[3] and no longer get a warning! It seems this question can be closed now.

Xcode 11, beta 5. Old answer:

I finally got some time to investigate this a little. As I mentioned in the comments, I think it would be wise to wait until the Collection conformance is completely removed (or replaced with something else). But just to satisfy our curiosity, I have created an extension on Binding, that I think does what the current Collection conformance does. The only difference is that, instead of accessing through a subscript, I implemented a function called element(_ idx: Int) to get a Binding<T> to the element.

If one day the conformance is completely removed, I may change the implementation, and conform to Collection myself. I cannot do it now, because it would conflict with the existent (and deprecated) implementation. For the time being, I think this demonstrate how to handle the warnings if you absolutely want to get rid of them.

Just to be clear. I am not using this code. As long as I can still access the elements through the subscript, I will still do it and ignore the warnings. This is just for academic purposes.

The extension is:

extension Binding where Value: MutableCollection, Value.Index == Int {
    func element(_ idx: Int) -> Binding<Value.Element> {
        return Binding<Value.Element>(
            get: {
                return self.wrappedValue[idx]
        }, set: { (value: Value.Element) -> () in
            self.wrappedValue[idx] = value
        })
    }
}

And it can be used like this:

struct MainView: View {
    @Binding var text: [String]

    var body: some View {
        TextField("", text: $text.element(0))
        TextField("", text: $text.element(1))
        TextField("", text: $text.element(2))
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • I have the issue of `dyld: Symbol not found: _$s7SwiftUI7BindingVyxGAA0C11ConvertibleAAMc` using this solution on a real device using iOS 13 beta 6 or 7. Can someone please confirm that this solution is currently working with beta 6 or 7? – AnErd Aug 19 '19 at 18:03
  • Thanks for your update ! Anyway, since I had this issue, I thought about other ways to design my code, and I think it’s better now. I saw that using arrays into binding could cause many risk of index out of range, if the view containing the texfield for exemple was still somewhere in the background or behind navigation. Maybe now it works better in beta 6 ? I’m still using arrays in my observable object, but only to copy a state value when user validates the form, and not directly into a textfield. And never again index out of range like that. (Sorry for my bad english) – hasselfred Aug 23 '19 at 20:55
  • Hi Kontiki, You seem to have a great grasp of the internals at work here. Do you know a way of taking a Binding Array in a view and using it with ForEach to create multiple child views that bind properly to their parent data? Using this method works to a point, but the indices don't seem to get updated so I get an "index out of bounds" failure as the data in my app changes. `ForEach(myArray.indices, id: \.self){ index in MyChildView(data: self.$myArray[index]) }` – Tom Millard Oct 14 '19 at 09:29
  • @TomMillard it's hard to say without looking the whole context of your code. I suggest you post a separate question... with a little more detail... Ideally, a minimal example that can be pasted into Xcode and reproduce your problem. – kontiki Oct 14 '19 at 14:44
0

I had to bind the array of an observable object recently, didn't get any warnings on stable XCode11. I did it like this

struct ScheduleTimer: Identifiable {
    var id: Int
    var name: String
    var start: Date
    var end: Date
    var isActive: Bool
}

struct ScheduleView: View {
    @ObservedObject var scheduleController = ScheduleController()
    var body: some View {
        NavigationView {
            Form {
                ForEach(scheduleController.timers) { timer in
                    ScheduleForm(scheduleController: self.scheduleController, timer: timer)
                }
            }
        }
    }
}


struct ScheduleForm: View {
    @ObservedObject var scheduleController: ScheduleController
    var timer: ScheduleTimer
    var scheduleIndex: Int {
        scheduleController.timers.firstIndex(where: { $0.id == timer.id })!
    }
    @State var start = Date()
    var body: some View {
        Section(header: Text(self.scheduleController.timers[scheduleIndex].name)){
            DatePicker("From", selection: self.$scheduleController.timers[scheduleIndex].start, displayedComponents: .hourAndMinute)
            DatePicker("To", selection: self.$scheduleController.timers[scheduleIndex].end, displayedComponents: .hourAndMinute)
            Toggle(isOn: self.$scheduleController.timers[scheduleIndex].isActive) {
                Text("")
            }.toggleStyle(DefaultToggleStyle())
        }
    }
}

class ScheduleController: ObservableObject {
    @Published var timers = [ScheduleTimer]()
...
Watermamal
  • 357
  • 3
  • 12