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
@mannd
Copy link

mannd commented Jun 1, 2020

Have you found a solution to the onDelete crash? I'm assuming this is a SwiftUI bug and will get fixed someday.

@chriseidhof
Copy link
Author

Hm, maybe try iterating over the indices instead of the range? So ForEach(list.items.indices) instead of ForEach(0..<list.items.count)? This code is ancient! 😅

@mannd
Copy link

mannd commented Jun 2, 2020

I've tried that and it is still a static list; indices is not updated with remove() in onDelete() and the code crashes. On the other hand, using ForEach(list.items, id: .id) { item in ...} makes a dynamic list and allows remove() without crashing. I just can't figure out how to make each item a binding in that situation because I want to be able to use TextFields in my list that alter members of item. I can use $list[index] with the static list, but I can't use $item with the dynamic list. Maybe I'm missing something. Or maybe Apple needs to make lists using indices dynamic.

@potmo
Copy link

potmo commented Jun 6, 2020

Im having the same issue and been working two days without finding any solution. Would be great if anyone knew how to remove items from a ForEach without getting Index out of range crashes

@mannd
Copy link

mannd commented Jun 6, 2020

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.

@chriseidhof
Copy link
Author

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

@chriseidhof
Copy link
Author

I also think most of the Store I used can be changed to bindings, which would make things a lot simpler.

@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