Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active June 1, 2022 21:09
Show Gist options
  • Save chriseidhof/603354ee7d52df77f7aec52ead538f94 to your computer and use it in GitHub Desktop.
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
@mannd
Copy link

mannd commented Jun 8, 2020

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.

@potmo
Copy link

potmo commented Jun 13, 2020

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