SwiftUI Tips:如何强制触发View刷新(reload)

5,184 阅读2分钟

按照 SwiftUI 的设计原则,View 绑定的数据源变化会触发 View 刷新。但是在某些情况下我们需要在数据源没变化的时候主动刷新 View。系统没有给 View 提供一个统一的 refresh 或者 reload 方法。于是我们只能用一些非主流的方式来实现。本文介绍两种方式实现手动触发 View 刷新。

自定义ObservedObject

SwiftUI 状态管理 @ObservedObject 的生命周期和 View 保持一致。当 View 被重新求值时,关联的ObservedObject 也会被重新初始化。同理 ObservedObject 变化时也会触发 View 重新求值。因此我们可以自定义一个 ObservedObject 实例,通过触发 ObservedObject 状态变化促使 View 刷新。

代码实现

AnimatedImage.gif

定义一个 ObservableObject:

class TriggerViewModel: ObservableObject {
    func updateView() {
        self.objectWillChange.send()
    }
}

在 View 中声明成 @ObservedObject 属性:

struct ContentView: View {
    @ObservedObject var refreshTrigger = TriggerViewModel()
    
    var body: some View {
        VStack {
            RandomView()
                .frame(width: 150, height: 150)
            Button("刷新") {
                updateViewModel()
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
        }
    }
    
    private func updateViewModel() {
        refreshTrigger.updateView()
    }
}

有了 refreshTrigger 后,我们调用内部的 updateView 方法就可以让 View 刷新。

Tips:因为每次 View 更新都会对 @ObservedObject 重新求值,但是通常情况下 View 的更新并不需要关联的属性重新初始化。因此 SwiftUI 后面推出了 @StateObject。

Demo 说明:RandomView 每次刷新就会变换背景色。

struct RandomView: View {
    let randomH = Double.random(in: 0...1.0)
    let randomS = Double.random(in: 0...1.0)
    let randomL = Double.random(in: 0...1.0)

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 12)
                .foregroundColor(Color(hue: randomH, saturation: randomS, brightness: randomL))
            VStack {
                Text("H: \(randomH)")
                Text("S: \(randomS)")
                Text("L: \(randomL)")
            }
            .fontWeight(.medium)
            .foregroundColor(.white)
        }
    }
}

修改 View 的标识:id

虽然 View 没有提供 reload 方法,但是 View 提供了 id 方法绑定 View 的身份标识。

extension View {

    /// Binds a view's identity to the given proxy value.
    ///
    /// When the proxy value specified by the `id` parameter changes, the
    /// identity of the view — for example, its state — is reset.
    @inlinable public func id<ID>(_ id: ID) -> some View where ID : Hashable

}

因此我们可以通过修改一个 View 的 id 的值来告诉系统这个 View 已经变化,需要重新刷新。

我们可以把要刷新的 View 的 id 绑定到一个时间戳上面。每次要更新的时候对 ViewId 重新赋值就可以了。

沿用上一节的代码,实现是这样的:

struct ContentView: View {
    @State private var refreshViewId = Date().timeIntervalSince1970
    
    var body: some View {
        VStack {
            RandomView()
                .frame(width: 150, height: 150)
                .id(refreshViewId)
            Button("刷新") {
                updateViewModel()
            }
        }
    }
    
    private func updateViewModel() {
        refreshViewId = Date().timeIntervalSince1970
    }
}