【问题标题】:ForEach not properly updating with dynamic content SwiftUIForEach 没有正确更新动态内容 SwiftUI
【发布时间】:2021-12-29 03:01:40
【问题描述】:

抱歉,这篇文章写了这么长,但事后看来,我应该向您展示这个问题的更简单的例子,以便您更好地理解问题所在。我假设 ForEach 的相同问题是这两个错误的根本原因,但我可能是错的。仍然包含第二个实例以提供上下文,但第一个实例应该是您完全理解问题所需的全部内容。

一审:

这里是该问题的视频:https://imgur.com/a/EIg9TSm。如您所见,有 4 个时间码,其中 2 个是收藏夹,2 个不是收藏夹(由黄色星标显示)。此外,顶部的文本表示时间码数组,显示为收藏夹 (F) 或不收藏夹 (N) 列表。我单击最后一个时间代码(更改为收藏)并按下切换按钮以取消收藏。当我点击保存时,时间码数组会更新,但如您所见,这并没有在列表中表示。但是,您会看到缩减数组的 Text 立即更新为 FNFF,表明它已正确更新为 ObservedObject 的收藏夹。

当我点击导航返回页面时,UI 已正确更新,并且有 3 个黄色星。这让我假设问题出在 ForEach 上,因为 Text() 显示数组已更新,但 ForEach 没有。据推测,点击离开页面会重新加载 ForEach,这就是它在退出页面后更新的原因。 EditCodeView() 处理 CoreData 中 TimeCodeVieModel 的保存,我 99% 确定它通过我自己的测试和 ObservedObject 按预期更新的事实正常工作。我很确定我使用的是 ForEach 的动态版本(因为 TimeCodeViewModel 是可识别的),所以我不知道如何在保存后立即更新行为。任何帮助将不胜感激。

这是视图的代码:

struct ListTimeCodeView: View {
    
    @ObservedObject var timeCodeListVM: TimeCodeListViewModel
    @State var presentEditTimeCode: Bool = false
    @State var timeCodeEdit: TimeCodeViewModel?

    init() {
        self.timeCodeListVM = TimeCodeListViewModel()
    }

    var body: some View {
        VStack {
            HStack {
                Text("TimeCodes Reduced by Favorite:")
                Text("\(self.timeCodeListVM.timeCodes.reduce(into: "") {$0 += $1.isFavorite ? "F" : "N"})")
            }

            List {
                ForEach(self.timeCodeListVM.timeCodes) { timeCode in
                        
                   TimeCodeDetailsCell(fullName: timeCode.fullName, abbreviation: timeCode.abbreviation, color: timeCode.color, isFavorite: timeCode.isFavorite, presentEditTimeCode: $presentEditTimeCode)
                        .contentShape(Rectangle())
                        .onTapGesture {
                            timeCodeEdit = timeCode
                                
                        }
                        .sheet(item: $timeCodeEdit, onDismiss: didDismiss) { detail in
                            EditCodeView(timeCodeEdit: detail)   
                        }
                }    
            }
        }
    }      
}

这里是视图模型的代码(不应该与问题相关,但为了理解而包含在内):

class TimeCodeListViewModel: ObservableObject {
    
    @Published var timeCodes = [TimeCodeViewModel]()
    
    init() {
        fetchAllTimeCodes()
    }

    func fetchAllTimeCodes() {
        self.timeCodes = CoreDataManager.shared.getAllTimeCodes().map(TimeCodeViewModel.init)
    } 
}


class TimeCodeViewModel: Identifiable {
    var id: String = ""
    var fullName = ""
    var abbreviation = ""
    var color = ""
    var isFavorite = false
    var tags = ""

    
    init(timeCode: TimeCode) {
        self.id = timeCode.id!.uuidString
        self.fullName = timeCode.fullName!
        self.abbreviation = timeCode.abbreviation!
        self.color = timeCode.color!
        self.isFavorite = timeCode.isFavorite
        self.tags = timeCode.tags!
    }
}

二审:

编辑:我意识到可能很难理解代码在做什么,所以我包含了一个演示问题的 gif(不幸的是,我的声誉不够高,无法自动显示)。如您所见,我选择了要更改的单元格,然后按下按钮将 TimeCode 分配给它。 TimeCodeCellViewModels 数组在后台发生变化,但在我按下主页按钮然后重新打开应用程序之前,您实际上看不到这种变化,这会触发 ForEach 的刷新。 Gif of issue。如果GIF太快还有这个视频:https://imgur.com/a/Y5xtLJ3

我正在尝试使用 HStack 的 VStack 来显示网格视图,并且遇到了一个问题,即当传入的数组发生更改时,我用来显示内容的 ForEach 不刷新。我知道数组本身正在发生变化,因为如果我将其缩减为字符串并使用 Text() 显示内容,它会在发生更改后立即正确更新。但是,仅当我关闭并重新打开应用程序时,ForEach 循环才会更新,从而强制 ForEach 重新加载。我知道有一个专门为动态内容设计的特殊版本的 ForEach,但我很确定我正在使用这个版本,因为我传入了 '''id: .self'''。这里是主要代码sn -p:

var hoursTimeCode: [[TimeCodeCellViewModel]] = []

// initialize hoursTimeCode

VStack(spacing: 3) {
   ForEach(self.hoursTimeCode, id: \.self) {row in
      HStack(spacing: 3){
         HourTimeCodeCell(date: row[0].date) // cell view for hour
            .frame(minWidth: 50)
         ForEach(row.indices, id: \.self) {cell in
            // TimeCodeBlockCell displays minutes normally. If it is selected, and a button is pressed, it is assigned a TimeCode which it will then display
            TimeCodeBlockCell(timeCodeCellVM: row[cell], selectedArray: $selectedTimeCodeCells)
               .frame(maxWidth: .infinity)
               .aspectRatio(1.0, contentMode: .fill)
         }
      }                              
   }                          
}

我很确定它不会改变任何东西,但我确实必须为 TimeCodeCellViewModel 定义一个自定义散列函数,这可能会改变 ForEach 的行为(被更改的属性包含在散列函数中)。但是,我注意到在我的项目的另一部分使用不同的视图模型的相同 ForEach 行为,所以我非常怀疑这是问题所在。

class TimeCodeCellViewModel:Identifiable, Hashable {
    static func == (lhs: TimeCodeCellViewModel, rhs: TimeCodeCellViewModel) -> Bool {
        if lhs.id == rhs.id {
            return true
        }
        else {
            return false
        }
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(isSet)
        hasher.combine(timeCode)
        hasher.combine(date)
    }

    var id: String = ""
    var date = Date()
    var isSet = false
    var timeCode: TimeCode
    
    var frame: CGRect = .zero
    
    init(timeCodeCell: TimeCodeCell) {
        self.id = timeCodeCell.id!.uuidString
        self.date = timeCodeCell.date!
        self.isSet = timeCodeCell.isSet
        self.timeCode = timeCodeCell.toTimeCode!
    }
}

【问题讨论】:

  • 您好,请提供完整的示例代码,以便我们轻松重现您的问题
  • 将您的类更改为结构或使其符合 ObservanleObject 并通过包装其用法来观察它。你的观点没有被告知这些变化。
  • @lorem-ipsum 你能看看我添加的第一个实例吗?我认为这个问题更清楚,更容易向您展示 ViewModel 已经符合 Identifiable 和 ObservedObject。非常感谢您花时间帮助我!
  • 您的代码中缺少太多内容,因此无法帮助您这些只是观察查看thisthis 这两个问题中的元素都适用于您正在尝试的内容去做。

标签: swift swiftui swiftui-foreach


【解决方案1】:

这是使代码正常工作所需的 sn-p。

查看 cmets 了解一些基本原因

struct EditCodeView:View{
    @EnvironmentObject var timeCodeListVM: TimeCodeListViewModel
    //This will observe changes to the view model
    @ObservedObject var timeCodeViewModel: TimeCodeViewModel
    var body: some View{
        EditTimeCodeView(timeCode: timeCodeViewModel.timeCode)
            .onDisappear(perform: {
                //*********TO SEE CHANGES WHEN YOU EDIT
                //uncomment this line***********
                //_ = timeCodeListVM.update(timeCodeVM: timeCodeViewModel)
            })
    }
}
struct EditTimeCodeView: View{
    //This will observe changes to the core data entity
    @ObservedObject var timeCode: TimeCode
    var body: some View{
        Form{
            TextField("name", text: $timeCode.fullName.bound)
            TextField("appreviation", text: $timeCode.abbreviation.bound)
            Toggle("favorite", isOn: $timeCode.isFavorite)
        }
    }
}
class TimeCodeListViewModel: ObservableObject {
    //Replacing this whole thing with a @FetchRequest would be way more efficient than these extra view models
    //IF you dont want to use @FetchRequest the only other way to observe the persistent store for changes is with NSFetchedResultsController
    //https://stackoverflow.com/questions/67526427/swift-fetchrequest-custom-sorting-function/67527134#67527134
    //This array will not see changes to the variables of the ObservableObjects
    @Published var timeCodeVMs = [TimeCodeViewModel]()
    private var persistenceManager = TimeCodePersistenceManager()
    init() {
        fetchAllTimeCodes()
    }
    
    func fetchAllTimeCodes() {
        //This method does not observe for new and or deleted timecodes. It is a one time thing
        self.timeCodeVMs = persistenceManager.retrieveObjects(sortDescriptors: nil, predicate: nil).map({
            //Pass the whole object there isnt a point to just passing the variables
            //But the way you had it broke the connection
            TimeCodeViewModel(timeCode: $0)
        })
    }
    
    func addNew() -> TimeCodeViewModel{
        let item = TimeCodeViewModel(timeCode: persistenceManager.addSample())
        timeCodeVMs.append(item)
        //will refresh view because there is a change in count
        return item
    }
    ///Call this to save changes
    func update(timeCodeVM: TimeCodeViewModel) -> Bool{
        let result = persistenceManager.updateObject(object: timeCodeVM.timeCode)
        //You have to call this to see changes at the list level
        objectWillChange.send()
        return result
    }
}

//DO you have special code that you aren't including? If not what is the point of this view model?
class TimeCodeViewModel: Identifiable, ObservableObject {
    //Simplify this
    //This is a CoreData object therefore an ObservableObject it needs an @ObservedObject in a View so changes can be seem
    @Published var timeCode: TimeCode
    init(timeCode: TimeCode) {
        self.timeCode = timeCode
    }
}

【讨论】:

    【解决方案2】:

    您的第一个ForEach 可能无法检查Array<TimeCodeCellViewModel> 的身份是否已更改。 也许您想使用一个单独的struct,它在内部保存一个TimeCodeCellViewModel 数组并符合Identifiable,从而有效地实现这样的协议。

    stuct TCCViewModels: Identifiable {
        let models: Array<TimeCodeCellViewModel>
        var id: Int {
            models.hashValue
        }
    
    }
    

    您也可以将其设为通用,以便在您的应用中为不同的视图模型重复使用:

    struct ViewModelsContainer<V: Identifiable> where V.ID: Hashable {
        let viewModels: Array<V>
        let id: Int
        init(viewModels: Array<V>) {
            self.viewModels = viewModels
            var hasher = Hasher()
            hasher.combine(viewModels.count)
            viewModels.forEach { hasher.combine($0.id) }
            self.id = hasher.finalize
        }
        
    }
    

    【讨论】:

    • 你能看看我添加的第一个实例吗?我认为这个问题更清楚,更容易向您展示 ViewModel 已经符合 Identifiable 和 ObservedObject。非常感谢您花时间帮助我!
    猜你喜欢
    • 1970-01-01
    • 2023-03-17
    • 2020-04-06
    • 2016-03-19
    • 1970-01-01
    • 2021-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多