-
-
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 |
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)
}
}
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 gettingIndex out of range
crashes