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

SwiftUI视图更新时Image状态异常 点赞按钮图标闪烁Bug

问题根因

点赞图标闪回未点赞状态的核心原因有三个:

  1. @ObservedObject使用错误:你在CommentCell的初始化方法里自行创建CommentRowViewModel实例,但@ObservedObject不会持有实例、也不会保证实例生命周期,每当父视图刷新、CommentCell被重新构造时,就会生成全新的ViewModel实例。新实例初始化时会触发异步的checkIfUserLikedComment网络请求,在请求返回的约1秒空窗期内,comment.didLike为初始值,会直接渲染未点赞的空心图标,等请求返回后才切回实心,就是你看到的闪屏现象。
  2. 状态更新不规范:Firebase的网络回调默认不在主线程执行,直接在回调里修改@Published属性会导致UI更新延迟、状态不一致;同时checkIfUserLikedComment只处理了点赞状态为true的赋值逻辑,未点赞时不会更新状态,进一步加剧了初始渲染的状态不确定性。
  3. 视图实现冗余:用ZStack叠两个图标改透明度的写法会增加视图层级,在父视图有隐式动画时容易出现非预期的过渡效果。
修复方案

按以下步骤修改即可解决问题:

  • 将内部创建的ViewModel的声明从@ObservedObject替换为@StateObject,保证ViewModel实例在Cell的整个生命周期内只初始化一次,不会随视图重载被重复创建。
  • 补全点赞状态检查逻辑,无论接口返回是否点赞,都明确给didLike赋值,消除状态不确定的空窗期。
  • 所有修改@Published属性的操作都切换到主线程执行,符合SwiftUI的UI更新规则。
  • 简化点赞按钮实现,用单个Image动态切换图标和颜色,去掉冗余的ZStack叠层写法。
  • 增加乐观更新逻辑:点击点赞/取消时立刻更新UI状态,不用等网络请求返回,交互更跟手。
修正后核心代码
struct CommentCell: View {
    // 内部初始化的ViewModel用@StateObject托管生命周期
    @StateObject private var commentRowViewModel: CommentRowViewModel
    
    init(post: Post, comment: Comment) {
        // 该闭包仅在Cell首次创建时执行一次,后续重载不会重新生成VM
        _commentRowViewModel = StateObject(wrappedValue: CommentRowViewModel(comment: comment, post: post))
    }
    
    var body: some View {
        VStack {
            Button {
                commentRowViewModel.comment.didLike ?? false ?
                commentRowViewModel.unlikeComment() :
                commentRowViewModel.likeComment()
            } label: {
                let didLike = commentRowViewModel.comment.didLike ?? false
                // 单图标动态切换,无需叠放两个视图
                Image(systemName: didLike ? "heart.fill" : "heart")
                    .foregroundColor(didLike ? Color(r: 221, g: 102, b: 91) : .black)
            }
            Text("\(commentRowViewModel.comment.likes)")
                .font(.system(size: 14))
        }
    }
}

class CommentRowViewModel: ObservableObject {
    @Published var comment: Comment
    let service = CommentService()
    let post: Post
    
    init(comment: Comment, post: Post) {
        self.comment = comment
        self.post = post
        checkIfUserLikedComment()
    }
    
    func likeComment() {
        // 乐观更新:点击后立刻改UI状态,无需等网络返回
        comment.didLike = true
        comment.likes += 1
        
        service.likeComment(post: post, comment: comment) { [weak self] likes in
            DispatchQueue.main.async {
                guard let self = self else { return }
                // 若需要处理网络请求失败,可在此处回滚状态
                self.comment.likes = likes
            }
        }
    }
    
    func checkIfUserLikedComment() {
        service.checkIfUserLikedComment(comment: comment) { [weak self] didLike in
            DispatchQueue.main.async {
                // 无论是否点赞都明确赋值
                self?.comment.didLike = didLike
            }
        }
    }
    
    func unlikeComment() {
        // 乐观更新
        comment.didLike = false
        comment.likes = max(0, comment.likes - 1)
        
        service.unlikeComment(post: post, comment: comment) { [weak self] likes in
            DispatchQueue.main.async {
                guard let self = self else { return }
                // 若需要处理网络请求失败,可在此处回滚状态
                self.comment.likes = likes
            }
        }
    }
}
额外注意

如果CommentCell是放在List/ForEach中渲染,确保ForEach使用评论的唯一稳定标识(如comment.id)作为id,不要用数组索引作为遍历标识,避免SwiftUI错误复用、销毁重建Cell导致的状态异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 18:31:04