You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI报错:视图更新内禁止发布变更,求消息日期分组修复方案

问题描述

我正在开发一款消息应用,希望为每组消息添加日期或星期标题。在ViewModel中向数组添加元素时,出现错误:“Publishing changes from within view updates is not allowed, this will cause undefined behavior”。尝试用Dispatch.main.async包裹操作但无效,寻求可行的修复方案。

View代码

import SwiftUI
import Firebase
struct MessagesView: View {
    let messages: [Message]
    @EnvironmentObject var viewModel: MessageViewModel
    var body: some View {
        VStack{
            ScrollView{
               LazyVStack(spacing: 3){ 
                   ForEach(messages){ message in
                       if let dateStr = viewModel.getDate(date: message.timestamp) {
                             Text(dateStr)
                       }
                       MessageBubble(message: message)
                   }                               
                }  
            }
        }
    }
}

ViewModel代码

import Foundation
class MessageViewModel: Identifiable, ObservableObject {
    @Published var dayArr = [String]()  

    func getDate(date: Timestamp) -> String? {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "MM/dd/yyyy"

        let val = date.dateValue()
        let dateString = dateFormatter.string(from: val)

        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date())
        let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
        let dateToCompare = calendar.startOfDay(for: val)

        if dateToCompare == today {
            if !dayArr.contains("Today"){
                dayArr.append("Today") //error here when trying to append
                return "Today"
            } else{
                return nil
            }
        } else if dateToCompare == yesterday {
            if !dayArr.contains("Yesterday"){
                dayArr.append("Yesterday") //error here when trying to append
                return "Yesterday"
            } else {
                return nil
            }
        } else {
            if !dayArr.contains(dateString){
                dayArr.append(dateString) //error here when trying to append
                return dateString 
            } else{
                return nil
            }
        }
    }      
}
解决方案

问题根源在于你在View的渲染流程中(ForEach遍历消息时调用getDate)修改了ViewModel的@Published属性dayArr,这会触发View重新渲染,形成循环更新,导致系统抛出错误。Dispatch.main.async无效是因为即使延迟执行,还是在当前渲染周期的后续阶段触发了状态变更,依然会被系统检测到。

正确的修复思路是提前预处理消息数据,将日期标题和消息分组,而不是在View渲染时动态修改状态,具体步骤如下:

1. 定义分组数据结构

创建一个包含日期标题和对应消息数组的结构体:

struct MessageSection: Identifiable {
    let id = UUID()
    let dateTitle: String
    let messages: [Message]
}

2. 修改ViewModel,预处理分组逻辑

在ViewModel中移除dayArr,添加预处理分组的方法,一次性将消息按日期分组:

class MessageViewModel: ObservableObject {
    @Published var messageSections: [MessageSection] = []
    
    // 接收原始消息数组,生成分组数据
    func processMessages(_ messages: [Message]) {
        guard !messages.isEmpty else {
            messageSections = []
            return
        }
        
        let calendar = Calendar.current
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "MM/dd/yyyy"
        
        // 按时间戳排序消息(确保顺序正确)
        let sortedMessages = messages.sorted { $0.timestamp.dateValue() < $1.timestamp.dateValue() }
        
        var sections: [MessageSection] = []
        var currentSectionDateTitle: String?
        var currentSectionMessages: [Message] = []
        
        for message in sortedMessages {
            let messageDate = message.timestamp.dateValue()
            let dateToCompare = calendar.startOfDay(for: messageDate)
            let today = calendar.startOfDay(for: Date())
            let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
            
            var dateTitle: String
            if dateToCompare == today {
                dateTitle = "Today"
            } else if dateToCompare == yesterday {
                dateTitle = "Yesterday"
            } else {
                dateTitle = dateFormatter.string(from: messageDate)
            }
            
            if dateTitle != currentSectionDateTitle {
                // 新建分组
                if !currentSectionMessages.isEmpty {
                    sections.append(MessageSection(dateTitle: currentSectionDateTitle!, messages: currentSectionMessages))
                    currentSectionMessages = []
                }
                currentSectionDateTitle = dateTitle
            }
            currentSectionMessages.append(message)
        }
        
        // 添加最后一个分组
        if !currentSectionMessages.isEmpty, let title = currentSectionDateTitle {
            sections.append(MessageSection(dateTitle: title, messages: currentSectionMessages))
        }
        
        messageSections = sections
    }
}

3. 更新View,遍历分组数据

修改MessagesView,直接遍历预处理好的分组,无需在渲染时调用状态修改方法:

struct MessagesView: View {
    let messages: [Message]
    @EnvironmentObject var viewModel: MessageViewModel
    
    var body: some View {
        VStack{
            ScrollView{
                LazyVStack(spacing: 3){
                    ForEach(viewModel.messageSections) { section in
                        Text(section.dateTitle)
                        ForEach(section.messages) { message in
                            MessageBubble(message: message)
                        }
                    }
                }
            }
            .onAppear {
                // 在View出现时预处理消息分组
                viewModel.processMessages(messages)
            }
            .onChange(of: messages) { newMessages in
                // 当原始消息数组变化时,重新分组
                viewModel.processMessages(newMessages)
            }
        }
    }
}

额外说明

  • 这种方式将状态修改从View渲染流程中剥离,放在onAppear和onChange的时机执行,完全符合SwiftUI的状态更新规则。
  • 预处理分组时先对消息按时间排序,确保日期标题和消息的顺序正确。
  • 移除了原有的dayArr,避免了不必要的状态维护,逻辑更清晰。

内容的提问来源于stack exchange,提问作者anon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 00:15:00