Noveo

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

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

Senior iOS-разработчик Александр, наш постоянный автор, описывает нюансы работы с динамическим удалением элементов в SwiftUI.

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

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

static-list

У нас есть в навигации две кнопки: 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.

 

Вот и попробуем как-то обойти эту проблему, чтобы при удалении элемента, на основе которого показан дочерний экран, из менеджера мы могли покинуть дочерний экран, прописав необходимую реакцию (примечание: в нашем случае удаление вызвано нами же, естественно, не проблема сделать любую реакцию по нажатию на кнопку 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), нужно выбирать, нужно это или нет.

 

2. Вместо, как мне кажется, более подходящего 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.

 

Оригинал: https://sparklone.github.io/articles/dynamic-delete-elements-from-lists-in-swiftui/

 

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: