Каморка сурового программиста, дубль 2

Динамическое удаление элементов из списков в SwiftUI, проблемы и решения.

Для начала поговорим о списках в SwiftUI. Если конкретнее - о том как передавать из родительской View данные в дочерние экраны. Вот ссылка на проект, в котором можно найти исходный код из статьи: Исходники. Должен предупредить, что компилируется только в Xcode 13, чтобы можно было запустить в 12м надо дорабатывать напильником, т.к. новая SwiftUI конструкция для обхода массива с поддержкой Binding в Xcode 12 не поддерживается.

Начнем с базового вопроса, зачем нужны списки, какие аналоги в UIKit? Списком в SwiftUI является List, аналогом в UIKit будет UITableView, более того изначальная реализация List под капотом явно была построена поверх UITableView.

Что можно сказать хорошого про списки в SwiftUI? Они прям очень простые, ими очень приятно пользоваться.. Когда это работает. Что можно сказать плохого? Работает это далеко не всегда, и что хуже всего поведение меняется от версии к версии iOS, что совсем не хорошо.

Следующие примеры можно найти в ListsPlayground внутри проекта

  • Простейший список "1. ConstantList":

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        List {
            Text("First")
            Text("Second")
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

И все, простейшая статическая табличка. Можно помещать внутрь любые кастомные View

Но статика это скучно, а если хочется динамически строить список?

  • Тоже не сложно, "2. ListFromVar":

import SwiftUI
import PlaygroundSupport

struct Element {
    var name: String
}

struct ListFromVar: View {
    @State private var elements: [Element] = [
        .init(name: "First"),
        .init(name: "Second")
    ]
    var body: some View {
        List(elements, id: \.name) { element in
            Text(element.name)
        }
    }
}

PlaygroundPage.current.setLiveView(ListFromVar())

Списку надо указать по какому признаку элемент будет уникальным, в нашем случае это name. Конечно обычно используется уникальный id (если в нашем примере сделать два "First" - будут проблемы). И чтобы упростить жизнь, можно указать для нашей модели, что она реализует протокол Identifiable, требованием протокола является наличие переменной id: Hashable. UUID удовлетворяет этому условию. Это позволяет не указывать id: .id в параметрах для List

  • "3. ListFromVarId":

import SwiftUI
import PlaygroundSupport

struct Element: Identifiable {
    var id: UUID = UUID()
    var name: String
}

struct ListFromVar: View {
    @State private var elements: [Element] = [
        .init(name: "First"),
        .init(name: "Second"),
        .init(name: "First")
    ]
    var body: some View {
        List(elements) { element in
            Text(element.name)
        }
    }
}

PlaygroundPage.current.setLiveView(ListFromVar())

А что если мы захотим свайпом удалять элементы из списка? С этим нам поможет ForEach внутри List, к которому можно добавлять модификаторы. В нашем случае нужен модификатор onDelete

  • "4. ListForEach":

import SwiftUI
import PlaygroundSupport

struct Element: Identifiable {
    var id: UUID = UUID()
    var name: String
}

struct ListForEach: View {
    @State private var elements: [Element] = [
        .init(name: "First"),
        .init(name: "Second"),
        .init(name: "First")
    ]
    var body: some View {
        List {
            ForEach(elements) { element in
                Text(element.name)
            }
            .onDelete { indexSet in
                elements.remove(atOffsets: indexSet)
            }
        }
    }
}

PlaygroundPage.current.setLiveView(ListForEach())

И последний кубик знаний, которого нам не хватает. Как сделать, чтобы по нажатию на элемент списка происходила навигация внутрь нового экрана? С этим поможет NavigationLink, Ну и обязательно на самом верхнем уровне должна быть NavigationView.

  • "5. ListNavigation":

import SwiftUI
import PlaygroundSupport

struct Element: Identifiable {
    var id: UUID = UUID()
    var name: String
}

struct ListElement: View {
    let element: Element

    var body: some View {
        Text(element.name)
            .foregroundColor(.red)
    }
}

struct ListForEach: View {
    @State private var elements: [Element] = [
        .init(name: "First"),
        .init(name: "Second"),
        .init(name: "First")
    ]
    var body: some View {
        NavigationView {
            List {
                ForEach(elements) { element in
                    NavigationLink(destination: ListElement(element: element)) {
                        Text(element.name)
                    }
                }
                .onDelete { indexSet in
                    elements.remove(atOffsets: indexSet)
                }
            }
        }
    }
}

PlaygroundPage.current.setLiveView(ListForEach())

По сути мы быстро пробежались по основам списка и готовы начать разбираться с более интересными вопросами, а что к примеру произойдет, если в тот момент как мы перешли из списка на дочерний экран - элемент будет удален из списка? Или будет ли обновляться дочерний экране, если поменялось значение текущего элемента?

Дальнейшие примеры будут уже не в плейграунде, а внутри проекта. Вкратце пробегусь по структуре проекта.

  • SwiftUIListDataFlowApp - точка входа
  • ViewModifiers/NavigationButtons, хелпер позволяющий добавлять кнопки обновления записи и ее удаления в одну строчку (withEditNavigationButtons)
  • Helpers/ForEach+Binding - хелпер позволяющий на Xcode 12 использовать биндинги с ForEach. Но сразу предупреждаю, использование этого кода может приводить к крешам, он представлен скорее в виде исторической справки как приходилось ухищряться до введения Apple'ом нативного решения.
  • папка Models - здесь находится наша структура данных User, и manager позволяющий работать со списком пользователей - UsersManager: ObservableObject
  • папка Views - как не сложно догадаться содержит все View
    • UsersView - основная View, сделана в виде статического списка позволяющего открывать рассматриваемые нами примеры

import SwiftUI

struct UsersView: View {
    private static var firstRun = true
    @EnvironmentObject var usersManager: UsersManager
    var body: some View {
        List {
            Section(header: Text("Read only")) {
                NavigationLink(destination: ListViewConstants(checkDeletion: false)) {
                    Text("Simple list without bindings")
                }
                NavigationLink(destination: ListViewConstants(checkDeletion: true)) {
                    Text("Simple list without bindings, check deletion")
                }
                NavigationLink(destination: UserViewCheckDeletion(user: usersManager.users.first ?? UsersAPI.dumpUsers[1])) {
                    Text("Just user: \((usersManager.users.first ?? UsersAPI.dumpUsers[1]).name)")
                }
                NavigationLink(destination: ListViewIndices()) {
                    Text("Simple list without bindings, indices")
                }
            }
            Section(header: Text("With bindings, Readonly")) {
                NavigationLink(destination: ListViewBinding()) {
                    Text("List with custom bindings")
                }
                NavigationLink(destination: ListWithBindingsXcode13(userViewKind: .readonly)) {
                    Text("List with Xcode 13 bindings, no check deletion")
                }
                NavigationLink(destination: ListWithBindingsXcode13(userViewKind: .checkDeletion)) {
                    Text("List with Xcode 13 bindings, check deletion")
                }
                NavigationLink(destination: ListWithBindingsXcode13(userViewKind: .useCache)) {
                    Text("List with Xcode 13 bindings, check deletion, cache users")
                }
            }
            Section(header: Text("With bindings, Editable")) {
                NavigationLink(destination: ListWithBindingsXcode13(userViewKind: .editable)) {
                    Text("List with Xcode 13 bindings, editable")
                }
            }
        }
        .navigationBarTitle("Lists of lists =)", displayMode: .inline)
    }
}

struct UsersView_Previews: PreviewProvider {
    static var previews: some View {
        UsersView()
    }
}

  • UserInfoView - простая вьюшка отображающая свойства модели, используется другими вьюшками, чтобы не повторяться

    struct UserInfoView: View {
        let user: User
        var body: some View {
            VStack {
                Text("Name: \(user.name)")
                Text("Surname: \(user.surname)")
                Text("Views: \(user.views)")
            }
        }
    }
    
    • Lists, папка внутри которой указаны различные подходы к тому как делать списки
      • UserViewVariations, папка внутри которой помещены всевозможные вариации View для показа и манипулирования данными выбранного User

    Итак, начнем с простейшего случая, когда у нас список проходится по всем элементам массива, и передает элемент как копию внутрь дочерней View Запускаем приложение и выбираем пункт "Simple list without bindings"

    Список строится этой View


    struct ListViewConstants: View {
        let checkDeletion: Bool
        @EnvironmentObject var usersManager: UsersManager
    
        var body: some View {
            List {
                ForEach(usersManager.users) { user in
                    let _ = print(user)
                    if checkDeletion {
                        NavigationLink(
                            destination: UserViewCheckDeletion(user: user),
                            label: { Text(user.name) }
                        )
                    } else {
                        NavigationLink(
                            destination: UserView(user: user),
                            label: { Text(user.name) }
                        )
                    }
    
                }
                .onDelete { indexSet in
                    usersManager.removeUsers(indexSet: indexSet)
                }
            }
            .navigationBarTitle("Constants list", displayMode: .inline)
        }
    }
    

    Вызов будет как ListViewConstants(checkDeletion: false), поэтому отработает ветка else { и будет показана простейшая UserView


    struct UserView: View {
        let user: User
        @EnvironmentObject var usersManager: UsersManager
    
        var body: some View {
            UserInfoView(user: user)
                .withEditNavigationButtons(user: user, usersManager: usersManager)
                .onAppear {
                    print("UserView appear with user: \(user)")
                }
        }
    }
    

    Если зайти внутрь какого либо элемента, мы увидим экран:



    У нас есть в навигации две кнопки: Update и Delete, первая просит менеджер проставить текущему User имя равное "New name", вторая удаляет текущий User. Т.к. внутри View передается user обычной переменной, не через @Binding, нам по хорошему надо извлечь id от User и по этому id искать - есть ли в списке пользователь с таким id. Чтобы не делать это в каждой View, UsersManager имеет метод, у которого это все производится под капотом


    func set(name: String, to user: User) throws {
        DispatchQueue.main.async { [self] in
            print("try to set name \(name) for \(user)")
            guard let index = users.firstIndex(where: { $0.id == user.id }) else {
                print("can't find user: \(user)")
                return
            }
            users[index].name = name
        }
    }
    

    Проверяем работу кнопки Update, после нажатия мы видим, что хоть сама ссылка на UsersManager была в родительской View, обновление данных происходит и в дочерней View. Теперь нажимаем Delete, и тут важный момент, все сильно зависит от условий запуска приложения.

    • если таргетом является устройство на основе iOS 15 то при нажатии Delete
      • если выбран первый элемент в списке, - нас выкинет на сам список система
      • если не первый - ничего не произойдет, мы останемся на дочернем экране
    • если таргетом является устройство на основе iOS 14 то при нажатии Delete ничего не произойдет, не зависимо от индекса выбранного элемента.
      И это тот самый момент, о котором я говорил в начале статьи. Не очень весело наблюдать за тем, как идентичный, я бы сказал простейший код отрабатывает настолько по разному на разных версиях iOS.

    Вот и попробуем как то обойти эту проблему, чтобы при удалении элемента на основе которого показан дочерний экран из менеджера мы могли покинуть дочерний экран прописав необходимую реакцию (P.S. в нашем случае удаление вызвано нами же, естественно не проблема сделать любую реакцию по нажатию на кнопку Delete, речь идет о том, если удаление произошло по какому то событию вне нашего контроля, вернулся результат какого то сетевого запроса или еще что случилось)


    onChange


    import SwiftUI
    
    struct UserViewCheckDeletion: View {
        let user: User
        @EnvironmentObject var usersManager: UsersManager
    
        @State private var userWasDeleted: Bool = false
        @Environment(\.presentationMode) var presentationMode
    
    //    init(user: User) {
    //        print("init: UserViewCheckDeletion with user \(user)")
    //        self.user = user
    //    }
    
        var body: some View {
            UserInfoView(user: user)
                .withEditNavigationButtons(user: user, usersManager: usersManager)
    //            .onAppear {
    //                print("UserViewCheckDeletion appear with user: \(user)")
    //            }
                .alert(isPresented: $userWasDeleted, content: {
                    Alert(
                        title: Text("User was deleted"),
                        dismissButton: Alert.Button.cancel(Text("OK"), action: {
                            presentationMode.wrappedValue.dismiss()
                        })
                    )
                })
                .onChange(of: usersManager.users) { users in
                    print("UserViewCheckDeletion (onChange) users update: \(users)")
                    if !users.map(\.id).contains(user.id) {
                        userWasDeleted = true
                    }
                }
    //            .onReceive(usersManager.$users) { users in
    //                print("UserViewCheckDeletion (onReceive) users update: \(users)")
    //                if !users.map(\.id).contains(user.id) {
    //                    userWasDeleted = true
    //                }
    //            }
        }
    }
    

    Идея была в следующем, есть такой метод, onChange, он генерирует новые данные каждый раз когда они обновляются, то что нам надо.

    Запускаем программу на iOS 15, выбираем пункт "Simple list without bindings, check deletion, заходим на любого пользователя, жмем Delete и все отрабатывает как надо. В метод onChange прилетает обновленный список users,в нем уже отсутствует тот, которым был инициирован экран, соответственно мы понимаем, что элемент был удален, ну и реагируем, показываем алерт и выполняем навигацию на предыдущий экран.

    Хорошо, что все это отрабатывает даже для первого элемента (как мы помним в предыдущем примере в iOS 15 при удалении первого элемента система сама без предупреждения выкидывает на список обратно). Плохо то.. Что это не работает для iOS 14. Запускаем симулятор для iOS 14, повторяем все шаги, нажатие на Delete не приводит визуально ни к чему. Починить это можно двумя способами.

    1. раскомментировать строчку .navigationViewStyle(StackNavigationViewStyle())

    struct SwiftUIListDataFlowApp: App {
        let usersManager = UsersManager(usersApi: UsersAPI())
        var body: some Scene {
            WindowGroup {
                NavigationView {
                    UsersView()
                }
    //            .navigationViewStyle(StackNavigationViewStyle())
                .environmentObject(usersManager)
                .onAppear {
                    usersManager.getUsers()
                }
            }
        }
    }
    
    

    это поменяет тип навигации, по умолчанию используется DoubleColumnNavigationViewStyle, баг может быть из за двойного вложения списка в список. Но если код будет и на iPad - то потеряем возможность иметь две колонки (SplitView), нужно в общем выбирать надо или нет это.

    1. вместо как мне кажется более подходящего onChange использовать onReceive, минусом данного подхода будет то, что onReceive срабатывает чаще, при заходе на экран в том числе, так что следует быть аккуратней

    struct UserViewCheckDeletion: View {
        let user: User
        @EnvironmentObject var usersManager: UsersManager
    
        @State private var userWasDeleted: Bool = false
        @Environment(\.presentationMode) var presentationMode
    
        var body: some View {
            UserInfoView(user: user)
                .withEditNavigationButtons(user: user, usersManager: usersManager)
    //            .onAppear {
    //                print("UserViewCheckDeletion appear with user: \(user)")
    //            }
                .alert(isPresented: $userWasDeleted, content: {
                    Alert(
                        title: Text("User was deleted"),
                        dismissButton: Alert.Button.cancel(Text("OK"), action: {
                            presentationMode.wrappedValue.dismiss()
                        })
                    )
                })
    //            .onChange(of: usersManager.users) { users in
    //                print("UserViewCheckDeletion (onChange) users update: \(users)")
    //                if !users.map(\.id).contains(user.id) {
    //                    userWasDeleted = true
    //                }
    //            }
                .onReceive(usersManager.$users) { users in
                    print("UserViewCheckDeletion (onReceive) users update: \(users)")
                    if !users.map(\.id).contains(user.id) {
                        userWasDeleted = true
                    }
                }
        }
    }
    

    Теперь решение работает и на iOS 14 и на iOS 15

    Чтобы проверить свою теорию, что баг вызван вложенностью списков, следующий элемент ведет напрямую на дочернее вью, выбираем в основном списке элемент "Just user: Alexandr". Здесь кнопка Delete отработает как на iOS 14 так и на iOS 15, как с onChange, так и с onReceive.

    • Нередко я встречал совет, что надо перебирать в списке не сами элементы, а индексы, мол это даст индекс из коробки, по которому потом можно доставать элемент или Binding.

    import SwiftUI
    
    struct ListViewIndices: View {
        @EnvironmentObject var usersManager: UsersManager
        
        var body: some View {
            List {
                ForEach(usersManager.users.indices, id: \.self) { index in
                    NavigationLink(
                        destination: UserView(user: usersManager.users[index]),
                        label: { Text(usersManager.users[index].name) }
                    )
                }
                .onDelete { indexSet in
                    usersManager.removeUsers(indexSet: indexSet)
                }
            }
            .navigationBarTitle("Constants list", displayMode: .inline)
            .navigationBarItems(
                trailing: Button("Reload") {
                    usersManager.getUsers()
                }
            )
        }
    }
    

    Так вот, так делать не стоит. Таким образом мы говорим списку, что он должен различать элементы массива по их индексам, т.к. список теряет возможность по факту отличать элементы друг от друга. Выбираем в основном списке "Simple list without bindings, indices", заходим на первого юзера (Alexandr) и жмем Delete. В результате данные нашей View меняются на данные юзера Bob. В принципе это предсказуемо, т.к. как я уже говори идентификатором является сам индекс, число элементов изменилось, это вызвало перерисовку, элемент с индексом 0 был Alexandr стал Bob, SwiftUI посчитал, что элемент не удалился, а обновился. Более того XCode в зависимости от места использования может выдавать ворнинг про то, что мы используем ForEach не так как задумано


    ForEach(_:content:) should only be used for *constant* data. Instead conform
    data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
    

    На этом мы закрываем секцию с View, которые инициализируются обычными переменными и переходим к разделу, где в дочерних View ожидаются @Bindable.

    Начнем с истории. До Xcode 13 из коробки нельзя было получить для списка Binding значение. И люди шли на всевозможные ухищрения, от простейших типа


    ForEach(Array(array.enumerated()), id: \.offset) { index, element in
    

    или


    ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
    

    до крутых навороченных решений (как я уже упоминал, одно из них приведено в ForEach+Binding.swift) Это решение позволяет писать вот так


    struct ListViewBinding: View {
        @EnvironmentObject var usersManager: UsersManager
    
        var body: some View {
            List {
                ForEach($usersManager.users) { index, user in
                    NavigationLink(
                        destination: UserViewWithBindingReadonly(user: $usersManager.users[index]),
                        label: { Text(usersManager.users[index].name) }
                    )
                }
                .onDelete { indexSet in
                    usersManager.removeUsers(indexSet: indexSet)
                }
            }
        }
    }
    

    user внутри ForEach сразу будет типа Binding<User>

    Круто? Круто, вот только падает иногда..

    В секции "With bindings, readonly" выбираем пункт "List with custom bindings", выбираем последнего в списке юзера, заходим на него, жмем Delete. Crash. Ну и если удалять не последний элемент, а первый, то данные внутри открытой View заменятся данными следующего по списку элемента.
    К счастью теперь у нас есть нативный подход

    ForEach($usersManager.users) { $user in
    

    enum UserViewKind {
        case useCache
        case checkDeletion
        case readonly
        case editable
    }
    struct ListWithBindingsXcode13: View {
        let userViewKind: UserViewKind
        @EnvironmentObject var usersManager: UsersManager
    
        var body: some View {
            List {
                ForEach($usersManager.users) { $user in
                    let _ = print(user)
                    switch userViewKind {
                        case .useCache:
                            NavigationLink(
                                destination: UserViewWithBindingReadonlyCheckDeletionCached(user: $user),
                                label: { Text("\(user.name), \(user.surname)") }
                            )
                        case .checkDeletion:
                            NavigationLink(
                                destination: UserViewWithBindingReadonlyCheckDeletion(user: $user),
                                label: { Text("\(user.name), \(user.surname)") }
                            )
                        case .readonly:
                            NavigationLink(
                                destination: UserViewWithBindingReadonly(user: $user),
                                label: { Text("\(user.name), \(user.surname)") }
                            )
                        case .editable:
                            NavigationLink(
                                destination: EditableUserInfoView(user: $user),
                                label: { Text("\(user.name), \(user.surname)") }
                            )
                    }
                }
                .onDelete { indexSet in
                    usersManager.removeUsers(indexSet: indexSet)
                }
            }
        }
    }
    

    К сожалению это работает только в XCode 13. К счастью нет ограничения по iOS 15, есть обратная совместимость и это радует.
    Проверяем работу удаления для пункта "List with Xcode 13 bindings, no check deletion"

    struct UserViewWithBindingReadonly: View {
        @Binding var user: User
        @EnvironmentObject var usersManager: UsersManager
    
        var body: some View {
            UserInfoView(user: user)
                .withEditNavigationButtons(user: user, usersManager: usersManager)
        }
    }
    

    Та же проблема, что и для "Simple list without bindings", если на iOS 15 удалять первый элемент в списке - вылетает на список, если другие - визуально ничего не происходит. На iOS 14 вообще визуальной реакции нет на удаления, ничего нового.

    Проверяем работу удаления для пункта "List with Xcode 13 bindings, check deletion"

    struct UserViewWithBindingReadonlyCheckDeletion: View {
        @Binding var user: User
        @EnvironmentObject var usersManager: UsersManager
    
        @State private var userWasDeleted: Bool = false
        @Environment(\.presentationMode) var presentationMode
    
        var body: some View {
            UserInfoView(user: user)
                .withEditNavigationButtons(user: user, usersManager: usersManager)
                .alert(isPresented: $userWasDeleted, content: {
                    Alert(
                        title: Text("User was deleted"),
                        dismissButton: Alert.Button.cancel(Text("OK"), action: {
                            presentationMode.wrappedValue.dismiss()
                        })
                    )
                })
                .onChange(of: usersManager.users) { users in
    
                    // iOs 15 crash
    
                    print("UserViewWithBindingReadonlyCheckDeletion (onChange) users update: \(users), for user \(user)")
                    if !users.map(\.id).contains(user.id) {
                        userWasDeleted = true
                    }
                }
    //            .onReceive(usersManager.$users) { users in
    //                print("UserViewWithBindingReadonlyCheckDeletion (onReceive) users update: \(users)")
    //                if !users.map(\.id).contains(user.id) {
    //                    userWasDeleted = true
    //                }
    //            }
        }
    }
    

    iOS 14: onChange не отрабатывает, фикс идентичен - использовать onReceive. Если попробовать с onChange перейти на navigationViewStyle(StackNavigationViewStyle()) - при удалении последнего элемента будет Crash как в кастомном решении


    iOS 15: onChange - удаление первого выбрасывает в список, удаление среднего ни к чему не приводит, удаление последнего - крэш, собрали все баги, флеш рояль ) onReceive - корректно отрабатывает во всех случаях.


    Чтобы уйти от крэшей можно попробовать воспользоваться системой кеша данных (ну или draft, кому как удобней). Подобное решение применяла Apple можно увидеть здесь

    Основная идея - при показе View - копируем данные и работаем с ними, это позволит уйти от проблемы использования Binding для несуществующего элемента в массиве

    import SwiftUI
    
    struct UserViewWithBindingReadonlyCheckDeletionCached: View {
        @Binding var user: User
        @EnvironmentObject var usersManager: UsersManager
    
        @State private var userWasDeleted: Bool = false
        @Environment(\.presentationMode) var presentationMode
    
        @State private var cachedUser: User = User(id: .init(), name: "", surname: "", birthday: Date(), views: 0)
    
        var body: some View {
            UserInfoView(user: user)
                .withEditNavigationButtons(user: user, usersManager: usersManager)
                .onAppear {
                    cachedUser = user
                }
                .alert(isPresented: $userWasDeleted, content: {
                    Alert(
                        title: Text("User was deleted"),
                        dismissButton: Alert.Button.cancel(Text("OK"), action: {
                            presentationMode.wrappedValue.dismiss()
                        })
                    )
                })
                .onChange(of: usersManager.users) { users in
                    print("UserViewWithBindingReadonlyCheckDeletionCached (onChange) users update: \(users), for user \(cachedUser)")
                    if !users.map(\.id).contains(cachedUser.id) {
                        userWasDeleted = true
                    }
                }
    //            .onReceive(usersManager.$users) { users in
    //                print("UserViewCheckDeletion (onReceive) users update: \(users)")
    //                if !users.map(\.id).contains(cachedUser.id) {
    //                    userWasDeleted = true
    //                }
    //            }
        }
    }
    

    По сути главное что поменялось - добавилось

    .onAppear {
         cachedUser = user
    }
    

    Это решение прекрасно отрабатывает с onChange на iOS 15. Но не работает на iOS 14 для удаления любых элементов кроме последнего. Для последнего просто падает )
    Замена на onReceive выдает баг на обеих осях, связано с тем что мы инициализируем изначально


    @State private var cachedUser: User = User(id: .init(), name: "", surname: "", birthday: Date(), views: 0)
    

    А как я уже говорил, onReceive отрабатывает чаще, и на старте мы проверяем наличие этого дефолтного пустого юзера с usersManager.$users, и нам успевает показаться алерт что созданного на старте юзера с id: .init() не существует в массиве (что в принципе правда).


    Все примеры из секции "With bindings, readonly" передавали в дочерние view Binding<User>, хотя по факту не использовали возможностей по их обновлению напрямую, просто не хотел усложнять материал. Так что оставил это на сладкое


    Секция "With bindings, Editable", пункт "List with Xcode 13 bindings, editable"


    struct EditableUserInfoView: View {
        @Binding var user: User
        @EnvironmentObject var usersManager: UsersManager
        
        var body: some View {
            VStack {
                TextField("Name", text: $user.name)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("Surname", text: $user.surname)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }
            .padding()
            .withEditNavigationButtons(user: user, usersManager: usersManager)
        }
    }
    

    В плане редактирования все работает как часы при любом варианте биндингов как через текстовые поля, так и через кнопку Update. В проекте я добавил обвязку через onReceive на проверку удаления, как единственно работающую на всех осях и для любого индекса удаляемого элемента.


    Вот такое вот выдалось приключение "на пару минут" как же сделать список с редактируемыми и удаляемыми в фоне элементами работающий одновременно и в iOS 14 и в iOS 15.

    Tagged with: