Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active June 1, 2022 21:09
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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
@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