Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
229 views
in Technique[技术] by (71.8m points)

swift - Deleting list elements from SwiftUI's List

SwiftUI seems to have a rather annoying limitation that makes it hard to create a List or a ForEach while getting a binding to each element to pass to child views.

The most often suggested approach I've seen is to iterate over indices, and get the binding with $arr[index] (in fact, something similar was suggested by Apple when they removed Binding's conformance to Collection):

@State var arr: [Bool] = [true, true, false]

var body: some View {
   List(arr.indices, id: .self) { index in
      Toggle(isOn: self.$arr[index], label: { Text("(idx)") } )
   }
}

That works until the array changes in size, and then it crashes with index out of range run-time error.

Here's an example that will crash:

class ViewModel: ObservableObject {
   @Published var arr: [Bool] = [true, true, false]
    
   init() {
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
         self.arr = []
      }
   }
}

struct ContentView: View {
   @ObservedObject var vm: ViewModel = .init()

   var body: some View {
      List(vm.arr.indices, id: .self) { idx in
         Toggle(isOn: self.$vm.arr[idx], label: { Text("(idx)") } )
      }
  }
}

What's the right way to handle deletion from a List, while still maintaining the ability to modify elements of it with a Binding?

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Using insights from @pawello2222 and @Asperi, I came up with an approach that I think works well, without being overly nasty (still kinda hacky).

I wanted to make the approach more general than just for the simplified example in the question, and also not one that breaks separation of concerns.

So, I created a new wrapper view that creates a binding to an array element inside itself (which seems to fix the state invalidation/update ordering as per @pawello2222's observation), and passes the binding as a parameter to the content closure.

I initially expected to be needing to do safety checks on the index, but turns out it wasn't required for this problem.

struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
   
   typealias BoundElement = Binding<T.Element>
   private let binding: BoundElement
   private let content: (BoundElement) -> C

   init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) {
      self.content = content
      self.binding = .init(get: { binding.wrappedValue[index] }, 
                           set: { binding.wrappedValue[index] = $0 })
   }
   
   var body: some View { 
      content(binding)
   }
}

Usage is:

@ObservedObject var vm: ViewModel = .init()

var body: some View {
   List(vm.arr.indices, id: .self) { index in
      Safe(self.$vm.arr, index: index) { binding in
         Toggle("", isOn: binding)
         Divider()
         Text(binding.wrappedValue ? "on" : "off")
      }
   }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...