-
-
Save chriseidhof/603354ee7d52df77f7aec52ead538f94 to your computer and use it in GitHub Desktop.
// | |
// ContentView.swift | |
// TestingMoreSwiftUI | |
// | |
// Created by Chris Eidhof on 04.06.19. | |
// Copyright © 2019 Chris Eidhof. All rights reserved. | |
// | |
import SwiftUI | |
import Combine | |
var newListCounter = 1 | |
extension Array { | |
mutating func remove(atOffsets indices: IndexSet) { | |
for i in indices.reversed() { | |
remove(at: i) | |
} | |
} | |
subscript(safe index: Int) -> Element? { | |
get { | |
guard (startIndex..<endIndex).contains(index) else { return nil } | |
return self[index] | |
} | |
set { | |
guard (startIndex..<endIndex).contains(index) else { return } | |
if let v = newValue { | |
self[index] = v | |
} | |
} | |
} | |
} | |
/// Similar to a `Binding`, but this is also observable/dynamic. | |
@propertyDelegate | |
@dynamicMemberLookup | |
final class Derived<A>: BindableObject { | |
let didChange = PassthroughSubject<A, Never>() | |
fileprivate var cancellables: [AnyCancellable] = [] | |
private let get: () -> (A) | |
private let mutate: ((inout A) -> ()) -> () | |
init(get: @escaping () -> A, mutate: @escaping ((inout A) -> ()) -> ()) { | |
self.get = get | |
self.mutate = mutate | |
} | |
var value: A { | |
get { get() } | |
set { mutate { $0 = newValue } } | |
} | |
subscript<U>(dynamicMember keyPath: WritableKeyPath<A, U>) -> Derived<U> { | |
let result = Derived<U>(get: { | |
let value = self.get()[keyPath: keyPath] | |
return value | |
}, mutate: { f in | |
self.mutate { (a: inout A) in | |
f(&a[keyPath: keyPath]) | |
} | |
}) | |
var c: AnyCancellable! = nil | |
c = AnyCancellable(didChange.sink { [weak result] in | |
// todo cancel the subscription as well | |
result?.didChange.send($0[keyPath: keyPath]) | |
}) | |
cancellables.append(c) | |
return result | |
} | |
var binding: Binding<A> { | |
return Binding<A>(getValue: { self.value }, setValue: { self.value = $0 }) | |
} | |
deinit { | |
for c in cancellables { | |
c.cancel() | |
} | |
} | |
} | |
final class SimpleStore<A>: BindableObject { | |
let didChange = | |
PassthroughSubject<A, Never>() | |
init(_ value: A) { self.value = value } | |
var value: A { | |
didSet { | |
didChange.send(value) | |
} | |
} | |
var bindable: Derived<A> { | |
let result = Derived<A>(get: { | |
self.value | |
}, mutate: { f in | |
f(&self.value) | |
}) | |
let c = self.didChange.sink { [weak result] value in | |
result?.didChange.send(value) | |
} | |
result.cancellables.append(AnyCancellable(c)) | |
return result | |
} | |
} | |
struct TodoList: Codable, Equatable, Hashable { | |
var items: [Todo] = [] | |
var name = "Todos" | |
} | |
struct Todo: Codable, Equatable, Hashable { | |
var text: String | |
var done: Bool = false | |
} | |
struct MyState: Codable, Equatable, Hashable { | |
var lists: [TodoList] = [ | |
TodoList(items: [ | |
Todo(text: "Buy Milk"), | |
Todo(text: "Clean") | |
]) | |
] | |
} | |
struct ItemRow: View { | |
@Binding var item: Todo? | |
var body: some View { | |
return Button(action: { self.item!.done.toggle() }) { | |
HStack { | |
Text(item!.text) | |
Spacer() | |
if item!.done { | |
Image(systemName: "checkmark") | |
} | |
} | |
} | |
} | |
} | |
struct ListRow: View { | |
@ObjectBinding var item: Derived<TodoList> | |
var body: some View { | |
NavigationButton(destination: TodoListView(list: item)) { | |
HStack { | |
Text(item.value.name) | |
Spacer() | |
} | |
} | |
} | |
} | |
struct TodoListView: View { | |
@ObjectBinding var list: Derived<TodoList> | |
var body: some View { | |
List { | |
ForEach((0..<list.value.items.count)) { index in | |
ItemRow(item: self.list.items[safe: index].binding) | |
}.onDelete { indices in | |
// this crashes... | |
self.list.value.items.remove(atOffsets: indices) | |
} | |
} | |
.navigationBarTitle(Text("\(list.value.name) - \(list.value.items.count) items")) | |
.navigationBarItems(leading: | |
EditButton(), | |
trailing: Button(action: { self.list.value.items.append(Todo(text: "New Todo")) }) { Image(systemName: "plus.circle")} | |
) | |
} | |
} | |
struct AllListsView: View { | |
@ObjectBinding var theState: Derived<MyState> | |
var body: some View { | |
List { | |
ForEach(0..<theState.value.lists.count) { (index: Int) in | |
ListRow(item: self.theState.lists[index]) | |
} | |
} | |
.navigationBarTitle(Text("All Lists")) | |
.navigationBarItems(trailing: | |
Button(action: { | |
newListCounter += 1 | |
self.theState.value.lists.append(TodoList(items: [], name: "New List \(newListCounter)")) | |
}) { Image(systemName: "plus.circle")} | |
) | |
} | |
} | |
struct ContentView : View { | |
@ObjectBinding var store: Derived<MyState> | |
var body: some View { | |
NavigationView { | |
AllListsView(theState: store) | |
} | |
} | |
} | |
#if DEBUG | |
struct ContentView_Previews : PreviewProvider { | |
static var previews: some View { | |
ContentView(store: SimpleStore(MyState()).bindable) | |
} | |
} | |
#endif |
Hm, maybe try iterating over the indices instead of the range? So ForEach(list.items.indices) instead of ForEach(0..<list.items.count)? This code is ancient! 😅
I've tried that and it is still a static list; indices is not updated with remove() in onDelete() and the code crashes. On the other hand, using ForEach(list.items, id: .id) { item in ...} makes a dynamic list and allows remove() without crashing. I just can't figure out how to make each item a binding in that situation because I want to be able to use TextFields in my list that alter members of item. I can use $list[index] with the static list, but I can't use $item with the dynamic list. Maybe I'm missing something. Or maybe Apple needs to make lists using indices dynamic.
Im having the same issue and been working two days without finding any solution. Would be great if anyone knew how to remove items from a ForEach
without getting Index out of range
crashes
I don't think it's possible with current iteration of SwiftUI. My workaround is to have deletion set a flag in an item so that the item appears deleted (red background), and then with a Save button I filter out the deleted items from the array and close the view. It may not be the best UI design, but it is better than nothing.
This gist is very old and was just about me trying out some things...
I don't have the time to completely revise it, and I don't know if it helps, but here's what I'd currently would do to delete items from a list:
struct ContentView: View {
@State var items: [String] = [
"One",
"Two",
"Three",
"Four"
]
var body: some View {
List {
ForEach(items.indices, id: \.self) { ix in
Text(self.items[ix])
}.onDelete(perform: { ixs in
for ix in ixs.sorted().reversed() {
self.items.remove(at: ix)
}
})
}
}
}
I also think most of the Store
I used can be changed to bindings, which would make things a lot simpler.
The above works, however my problem is doing the same when the List includes bindings. For example, this causes the crash during onDelete():
struct Person {
var name: String
}
struct ContentView: View {
@State var items: [Person] = [
Person(name: "Fred"),
Person(name: "Charles"),
Person(name: "Amy")]
var body: some View {
List {
ForEach(items.indices, id: \.self) { ix in
TextField("Name: ", text: self.$items[ix].name)
}.onDelete(perform: { ixs in
for ix in ixs.sorted().reversed() {
self.items.remove(at: ix)
}
})
}
}
}
I'm probably missing something, but I haven't found a way to do this type of thing successfully.
This is a little more code and used ObservableObject but it handles adding and removing elements nicely
import Foundation
import SwiftUI
class DataItem: ObservableObject, Identifiable, Equatable {
var id: UUID
@Published var text: String
init(id: UUID, text: String) {
self.id = id
self.text = text
}
static func == (lhs: DataItem, rhs: DataItem) -> Bool {
return lhs.id == rhs.id
}
}
class DataStore: ObservableObject {
@Published var items: [DataItem]
init(items: [DataItem]) {
self.items = items
}
}
struct DataView: View {
@Binding var data: DataItem
var body: some View {
Text("\(data.text)")
}
}
struct DataListView: View {
@ObservedObject var data = DataStore(items: [
DataItem(id: UUID(), text: "One"),
DataItem(id: UUID(), text: "Two"),
DataItem(id: UUID(), text: "Three"),
DataItem(id: UUID(), text: "Four")
])
var body: some View {
List(self.data.items){ item in
DataView(data: self.bind(item))
.contextMenu {
Button("Delete"){
self.data.items.removeAll{$0 == item}
}
}
}.frame(minWidth: 200,
idealWidth: 200,
maxWidth: 500,
minHeight: 400,
idealHeight: 400,
maxHeight: 10000,
alignment: .topLeading)
}
func bind(_ item: DataItem) -> Binding<DataItem> {
let get = {item}
let set = { (item: DataItem) in
guard let index = self.data.items.firstIndex(of: item) else {
fatalError("Trying to set a binding to an item that no longer exist")
}
self.data.items[index] = item
}
return Binding(get: get, set: set)
}
}
Have you found a solution to the onDelete crash? I'm assuming this is a SwiftUI bug and will get fixed someday.