ForEach 未正确更新动态内容 SwiftUI
ForEach not properly updating with dynamic content SwiftUI
很抱歉让这个 post 这么长,但事后看来,我应该向您展示问题的更简单实例,以便您更好地理解问题所在。我假设 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: "") {[=10=] += .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 演示问题(不幸的是我的声誉不够高,无法自动显示)。如您所见,我 select 我想要更改的单元格,然后按下按钮将时间码分配给它。 TimeCodeCellViewModels 数组在后台发生变化,但您实际上看不到该变化,直到我按下主页按钮然后重新打开应用程序,这会触发 ForEach 的刷新。 Gif of issue. There is also this video if the GIF is too fast: https://imgur.com/a/Y5xtLJ3
我正在尝试使用 HStacks 的 VStack 显示网格视图,并且 运行 遇到一个问题,当传入的数组发生变化时,我用来显示内容的 ForEach 没有刷新。我知道数组本身正在发生变化,因为如果我将它缩减为一个字符串并使用 Text() 显示内容,它会在进行更改后立即正确更新。但是,ForEach 循环只会在我关闭并重新打开应用程序时更新,从而强制 ForEach 重新加载。我知道 ForEach 有一个专门为动态内容设计的特殊版本,但我很确定我正在使用这个版本,因为我传入了 '''id: .self'''。这是主要代码片段:
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!
}
}
您的第一个 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([=11=].id) }
self.id = hasher.finalize
}
}
这是使代码正常工作所需的片段。
查看评论了解一些基本的原因
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
//
//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: [=10=])
})
}
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
}
}
很抱歉让这个 post 这么长,但事后看来,我应该向您展示问题的更简单实例,以便您更好地理解问题所在。我假设 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: "") {[=10=] += .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 演示问题(不幸的是我的声誉不够高,无法自动显示)。如您所见,我 select 我想要更改的单元格,然后按下按钮将时间码分配给它。 TimeCodeCellViewModels 数组在后台发生变化,但您实际上看不到该变化,直到我按下主页按钮然后重新打开应用程序,这会触发 ForEach 的刷新。 Gif of issue. There is also this video if the GIF is too fast: https://imgur.com/a/Y5xtLJ3
我正在尝试使用 HStacks 的 VStack 显示网格视图,并且 运行 遇到一个问题,当传入的数组发生变化时,我用来显示内容的 ForEach 没有刷新。我知道数组本身正在发生变化,因为如果我将它缩减为一个字符串并使用 Text() 显示内容,它会在进行更改后立即正确更新。但是,ForEach 循环只会在我关闭并重新打开应用程序时更新,从而强制 ForEach 重新加载。我知道 ForEach 有一个专门为动态内容设计的特殊版本,但我很确定我正在使用这个版本,因为我传入了 '''id: .self'''。这是主要代码片段:
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!
}
}
您的第一个 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([=11=].id) }
self.id = hasher.finalize
}
}
这是使代码正常工作所需的片段。
查看评论了解一些基本的原因
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
//
//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: [=10=])
})
}
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
}
}