-
-
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 |
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)
}
}
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.