如何以编程方式从SwiftUI列表中删除行并让列表视图也刷新?

问题描述

当我检测到onAppear上的列表包含一个过时的记录时,我正在以编程方式从SwiftUI列表数据源中删除一条记录。当用户离开列表屏幕时,该记录已更新。

例如,用户位于项目编辑屏幕上。该屏幕在环境对象中标记一个有关记录更新的属性,以便“列表”屏幕可以使用该属性在“列表”再次出现时从“列表”数据源中删除陈旧记录。希望这种刷新列表的方式还可以吗?

            List{
                Section{
                    Toggle(isOn: self.$showArchivedItems.onChange(showOptionsChanged)) {
                        Text("Show Archived Records).font(.headline)
                    }
                }
                
                Section {
                    ForEach(userData.items) { dataItem in
                        NavigationLink(destination: ItemDetailView(item: dataItem)){
                            ListRowItemView(item: dataItem)
                        }
                    }.onDelete(perform: self.onDeletingItems)
                        .onAppear() {
                            if let updatedRecord = self.userData.lastUpdatedRecord {
                                if let updatedRecordId = self.userData.items.firstIndex(where: {return $0.id == updatedRecord.id}) {

                                    //OPTION #1 - Doesn’t work
                                    //self.userData.items.remove(at: updatedRecordId)
                                    //self.userData.items.append(updatedRecord)

                                    //OPTION #2 - Doesn’t work
                                    //self.userData.items.replaceSubrange(updatedRecordId... updatedRecordId,with: [updatedRecord])

                                    //OPTION #3 - works
                                        self.userData.items = self.storage.load()

                                }
                            }
                    }
                }
            }.listStyle(GroupedListStyle())

  private func onDeletingItems(indexset: IndexSet) {
    var itemsToDeactivate: [Itemmodel] = []
    
    for index in indexset {
        userData.items[index].isArchived = true
        itemsToDeactivate.append(userData.items[index])
    }
    
    for item in itemsToDeactivate {
        storage.persist(dataItem: item)
    }
    
    userData.items.remove(atOffsets: indexset)
}

附加代码

UserData.swift

import Foundation
final class UserData: ObservableObject {
    @Published var items: [Itemmodel]
    
    var lastUpdatedRecord: Itemmodel? = nil
        
    init(data: [Itemmodel] = []) {
        self.items = data
    }
    
    func clearLastUpdatedRecord() {
        self.lastUpdatedRecord = nil
    }
}

列表视图

struct ListView: View {
    @EnvironmentObject var userData: UserData

var body: some View {
        NavigationView {
            vstack{
                List{ 
                    //ForEach on userData.items
                }
            }
        }
}

SceneDelegate.swift

func scene(_ scene: UIScene,willConnectTo session: UIScenesession,options connectionoptions: UIScene.Connectionoptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard,the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingScenesession` instead).

        // Create the SwiftUI view that provides the window contents.
        let storage = ItemLocalStorage()
        let userData = UserData(data: storage.load())
        let userSettings = SettingsModel()
        let contentView = ContentView().environmentObject(storage).environmentObject(userData).environmentObject(userSettings)

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

问题是,我尝试了两种选择,专门是通过在List数据源中查找过时记录的索引(在OPTION#1中)并追加更新的实例,或者在OPTION#2中追加过时的实例,尝试将过时的记录替换为update实例。

在两个选项中,更新的记录的确添加到列表中,但过时的记录不会从列表中删除,尽管陈旧的记录具有更新的数据,但我看到的是更新记录的双重实例。

当我从存储中完全刷新数据源(OPTION#3)时,列表将显示正确的项目,即仅更新记录的一个实例。

任何人都可以建议如何以编程方式从List数据源中删除记录,以便List也刷新其视图。我的意思是我不必从存储中再次加载所有记录来更新列表,因为单个记录已更新。 (在我的应用程序中,更新是用户触发的,根据当前设计,一次只能更新一条记录)

我还想知道,List是否支持从List编辑项目,就像我在List上实现onDelete一样。用户可以在此处将List置于editMode并删除单个项目。我确定List不会调用我的存储来提取所有数据。那么如何管理列表以删除单个项目并刷新其显示

解决方法

我通过捕获List的数据源作为显式状态变量设法找到了解决该问题的方法。如果我现在对此状态变量进行更改,即删除并添加行,则列表显示将反映更新的记录。

假设是,当基础值发生变化时,SwiftUI会比@State更喜欢@State来自动呈现UI。因此,我没有处理在EnvironmentObject中保存的Array副本,而是将工作传递给专用的@State变量。在此之后,我得到了想要的行为。

我的关键更改是

ListView.swift

    @State private var items: [ItemModel] = []

    init(items: [ItemModel]) {
        _items = State(initialValue: items)
    }

    List{
                    Section{
                        Toggle(isOn: self.$showArchivedItems.onChange(showOptionsChanged)) {
                            Text("Show Archived”).font(.headline)
                        }
                    }
                    
                    Section {
                        ForEach(self.items) { item in
                            NavigationLink(destination: ItemDetailView(item: item)){
                                ListRowItemView(item: item)
                            }
                        }.onDelete(perform: self.onDeletingItems)
                            .onAppear() {
                                if let updatedRow = self.userData.lastUpdatedRecord {
                                    self.updateListDataSource(updatedRow)
                                }
                            }
                        }
                }.listStyle(GroupedListStyle())


    fileprivate func updateListDataSource(_ updatedRow: ItemModel) {
        if let updatedRecordId = self.items.firstIndex(where: {return $0.id == updatedRow.id}) {
            self.items.remove(at: updatedRecordId)
            self.items.append(updatedRow)
            self.userData.clearLastUpdatedRecord()
        }
    }

  private func onDeletingItems(indexset: IndexSet) {
        var itemsToDeactivate: [ItemModel] = []
        
        for index in indexset {
            userData.items[index].isArchived = true
            userData.items[index].lastModified = Date()
            itemsToDeactivate.append(userData.items[index])
        }
        
        for item in itemsToDeactivate {
            storage.persist(item: item)
            self.userData.items.removeAll { $0.id == item.id}
        }
        
        self.items.remove(atOffsets: indexset)
    }

UserData.swift

import Foundation
final class UserData: ObservableObject {
    @Published var items: [ItemModel]
    
     var lastUpdatedRecord: ItemModel? = nil {
        didSet {
            if let updatedInstance = lastUpdatedRecord {
                if let updatedInstanceIndex = self.items.firstIndex(where: {$0.id == updatedInstance.id}) {
                    self.items.remove(at: updatedInstanceIndex)
                    self.items.append(updatedInstance)
                }
            }
        }
    }
        
    init(data: [ItemModel] = []) {
        self.items = data
    }
    
    func clearLastUpdatedRecord() {
        self.lastUpdatedRecord = nil
    }
}