Last active
June 1, 2022 21:09
-
-
Save chriseidhof/603354ee7d52df77f7aec52ead538f94 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
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)
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I also think most of the
Store
I used can be changed to bindings, which would make things a lot simpler.