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

Posted

技术标签:

【中文标题】如何以编程方式从 SwiftUI 列表中删除行并刷新列表视图?【英文标题】:How to programatically delete row from SwiftUI List and have the List view refreshes as well? 【发布时间】:2020-09-13 15:18:49 【问题描述】:

我希望以编程方式从 SwiftUI 列表数据源中删除一条记录,当我检测到 onAppear 该列表有一条陈旧的记录时,该记录在用户离开列表屏幕时得到更新。

例如,用户在项目编辑屏幕上。此屏幕标记 Environment 对象中的一个属性,该属性与更新的记录有关,以便 List 屏幕可以使用此属性在 List 再次出现时从 List 数据源中删除过时的记录。希望这种刷新 List 的模式可以吗?

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

问题是我尝试了两个选项来专门从列表数据源中删除过时的记录,方法是找到它的索引(在选项#1中)并附加更新的实例,或者在选项#2中我试图用更新的替换过时的记录实例。

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

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

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

我还想知道,List 确实支持编辑 List 中的项目,就像我在 List 上实现了 onDelete 一样。在这里,用户可以将 List 置于 editMode 并删除单个项目。我确信 List 没有调用我的存储来提取所有数据。那么 List 是如何管理删除单个项目并刷新其显示的呢?

【问题讨论】:

只需从userData.items中删除元素 我已经在 OPTION#1 中这样做了,但它没有从列表显示中删除。列表继续显示项目 你会显示userData类型的代码吗? 添加了 userData 的代码和其他一些代码 您的 ItemModel 类型是否有唯一的 id 字段?喜欢UUID 【参考方案1】:

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

假设 SwiftUI 最喜欢 @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
    

【讨论】:

以上是关于如何以编程方式从 SwiftUI 列表中删除行并刷新列表视图?的主要内容,如果未能解决你的问题,请参考以下文章

如何以编程方式从 Android 的默认应用列表中删除应用?

如何从 WatchOS 的 SwiftUI 列表中删除分隔符?

从 SwiftUI 的列表中删除列表元素

从 SwiftUI 的列表中删除列表元素

如何从 SwiftUI 列表和领域中删除数据

如何从 Mac OS 中的 SwiftUI 列表中删除底部/顶部项目填充