无法停用 SwiftUI View 更新

Cannot disable SwiftUI View updates

提问人:Reinhard Männer 提问时间:9/13/2023 最后编辑:Reinhard Männer 更新时间:9/15/2023 访问量:163

问:

我的 App 有两种模式:“
运行”模式执行速度非常快,无需任何 SwiftUI 视图更新,另一种模式是“单步”模式,用户手动推进 App 状态,并在每一步都需要 View 更新。
在单步模式下,SwiftUI 会像往常一样更新我的视图。对于运行模式,我想使用这篇文章中的建议禁用自动视图更新。
为了测试这种方法,我编写了以下最小项目。
如果我确实理解了这个建议,则不应更新,因为相等函数总是返回。
但是视图已更新,我不明白为什么。
CustomViewtrue

编辑:
我的应用程序需要在运行时在运行模式和单步模式之间切换。因此,无法为一种或另一种模式重新生成应用程序。

@Observable
final class Model {
    
    var counter = 0
    var timer = Timer()

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.counter += 1
        }
    }   
}

@main
struct ViewEquatableApp: App {
    
    let model = Model()
    
    var body: some Scene {
        WindowGroup {
            CustomView(model: model)
                .equatable()
        }
    }
}

struct CustomView: View, Equatable {
    let model: Model
    
    static func == (lhs: CustomView, rhs: CustomView) -> Bool {
        // << return yes on view properties which identifies that the
        // view is equal and should not be refreshed (ie. `body` is not rebuilt)
        true // <- true should disable the View update
    }
    
    var body: some View {
        ContentView(model: model)
    }
}

struct ContentView: View {
    let model: Model

    var body: some View {
        Text("Counter: \(model.counter)")
    }
}
SwiftUI 视图 相等

评论

0赞 lorem ipsum 9/13/2023
您应该更改 Observable 而不是 View,为什么要首先启动 Timer?
0赞 Reinhard Männer 9/13/2023
我的应用需要在运行时在运行模式和单步模式之间切换。因此,如果不重新构建,我无法更改 Observable。计时器只是用来显示视图是否已更新。
0赞 lorem ipsum 9/13/2023
计时器可以替代其他任何东西,任何使你的应用程序“运行”的东西都不应该运行,你当前的方法并没有真正停止“运行”,它只是隐藏了它。任何取代 SwiftUI 自动重绘的尝试现在或将来都会有问题,我们不知道这个系统。
0赞 Reinhard Männer 9/13/2023
对于这个误会,我深表歉意。我唯一想实现的是,在运行模式下,视图不会更新,因为这太慢了。我认为,视图的 Equatable 协议可以用于这种情况,正如提供的参考文献中所建议的那样。为什么它不起作用?
1赞 lorem ipsum 9/13/2023
这只是一个黑客,这里没有人能告诉你为什么它不起作用。苹果并没有告诉我们关于SwiftUI如何工作的所有信息,它隐藏在我们看不见的层后面。在 iOS 17 之前,它严重依赖 Equatable 和 Hashable,两者都不使用,它有自己的机制我们看不到,您可以尝试 s 并破解解决方案,但它可能会损坏并出现错误,我不久前在那篇帖子上发表了一篇关于“揭开 SwiftUI 神秘面纱”的评论,因为关于这个问题的 SO 有很多问题, 它在 iOS 16 中也无法可靠地工作。@ObservableObservableObject

答:

2赞 malsag 9/13/2023 #1

您在类中使用了 Apple 的新宏,该宏会在使用此类的任何位置自动处理视图更新。因此,该类中的每个变量都被视为 as 和 in 函数作为内部更新视图,而协议通常会防止外部触发不必要的视图更新(例如由父视图触发)。@Observable@PublishedmodelCustomView@ObservedObjectEquatable

在您的例子中,对类使用常规协议更合适,用 标记属性。然后,您可以在任何需要更新的视图中使用包装器。此外,您不需要实现协议。ObservableObject@Published@ObservedObjectEquatable

final class Model: ObservableObject {
    
    @Published var counter = 0
    var timer = Timer()

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.counter += 1
        }
    }
}

@main
struct ViewEquatableApp: App {
    
    let model = Model()
    
    var body: some Scene {
        WindowGroup {
            CustomView(model: model)
                
        }
    }
}

struct CustomView: View {
    let model: Model
    
    var body: some View {
        ContentView(model: model)
    }
}

struct ContentView: View {
    
    @ObservedObject var model: Model

    var body: some View {
        Text("Counter: \(model.counter)")
    }
}

以这种方式只更新,如果您不希望它更新,请将其删除ContentView@ObservedObject

评论

0赞 Reinhard Männer 9/13/2023
删除需要重新生成应用。但是,我的应用需要在运行时在运行模式和单步模式之间切换。很抱歉,我没有在我的问题中明确说明这一点。出于这个原因,我认为我仍然需要等同的观点。@ObservedObject
0赞 Reinhard Männer 9/15/2023 #2

我下一次尝试禁用 SwiftUI View 更新如下。
我正在学习 SwiftUI,我确信这不是实现它的最佳方式。因此,我们非常欢迎任何关于如何正确操作的建议!
正如 lorem ipsum 在他的上一条评论中指出的那样,问题中概述的方法显然适用于较旧的 SwiftUI 版本,但在使用宏时不再有效。
无论如何,这种方法太复杂了。有一个更简单的解决方案应该始终有效。
当观察到的属性发生更改时,通常会重新绘制 SwiftUI。这通常由 ViewModel 控制,而不是由 DataModel 本身控制。
因此,我更改了代码,以便创建一个传递给:
@ObservableAppViewModelContentView

@main
struct MyApp: App {
    
    let viewModel = ViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: viewModel)
        }
    }
}

ContentView访问传递的 ,并显示它:viewModelCounterViewModel

struct ContentView: View {
    let viewModel: ViewModel

    var body: some View {
        Text("Counter: \(viewModel.viewModelCounter)")
    }
}

ViewModel是。它有一个属性,此处初始化的数据模型,以及一个由 显示的属性。@ObservablemodelviewModelCounterContentView

Modal具有一个属性,用于确定是否应将模型中的数据更改转发回 。如果是这样,则更改将允许重新绘制其内容。 还有一个属性,该属性由计时器重复递增,以模拟不断变化的数据。 还引用了它,以便能够在需要时进行更改。modeViewModel.viewModelCounterViewModel.viewModelCounterContentViewModelmodelCounterModelViewModelviewModel.viewModelCounter

class Model {
    var mode = Mode.singleStep
    var viewModel: ViewModel?
    var modelCounter = 0
    var changeDataTimer = Timer()
    
    func changeDataRepeatedly() {
        changeDataTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [self] _ in
            self.modelCounter += 1
            if mode == .singleStep {
                viewModel?.viewModelCounter = modelCounter
            }
        }
    }
}

这个测试应用程序可以按要求工作,但是,如前所述,它肯定不是最好的解决方案。

评论

0赞 lorem ipsum 9/15/2023
看起来不错,我可能会抽象出更多的东西,比如订阅和取消订阅更改,但这与我在评论中提到的内容相同。