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