简而言之--使用ViewModel("MyView"/"MyViewModel")构建自定义SwiftUI视图时,如下所示:

struct ContentView: View {

    var body: some View {
        MyView(viewModel: MyViewModel())
    }
}

为什么会这样:

struct MyView: View {

    @StateObject var viewModel: MyViewModel

    var body: some View {
        Text("Hello world!")
    }
}

与此不同:

struct MyView: View {

    @StateObject var viewModel: MyViewModel

    init(viewModel: MyViewModel) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }

    var body: some View {
        Text("Hello world!")
    }
}

在调试时,第二个选项似乎会在SwiftUI每次重新构建MyView时创建MyViewModel的新实例.但在第一个选项中,StateObject包装器似乎完成了它的工作,并确保只为MyView的所有重建创建一个MyViewModel的实例.

是否有一些额外的SwiftUI魔力应用于使用视图的默认成员式初始化器与自定义初始化器?也许是通过ViewBuilder?

下面是一个简单的示例应用程序MyApp.swft,它可以查看这种行为的实际情况.

//
//  MyApp.swift
//

import SwiftUI

@main
struct MyApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {

    @AppStorage("redBackground") private var redBackground: Bool = false

    var body: some View {
        ZStack {
            // Flipping "redBackground" will cause a reconstruction of the view hierarchy
            if redBackground {
                Color.red
            } else {
                Color.green
            }
            MyView(viewModel: MyViewModel())
        }
    }
}

final class MyViewModel: ObservableObject {

    init() {
        print("MyViewModel.init")
    }
}

struct MyView: View {

    @StateObject var viewModel: MyViewModel

    @AppStorage("redBackground") private var redBackground: Bool = false

    // WARNING: Uncommenting this causes the view model to be recreated every reconstruction of the view!
    //    init(viewModel: MyViewModel) {
    //        self._viewModel = StateObject(wrappedValue: viewModel)
    //    }

    var body: some View {
        VStack {
            Button("Toggle background") {
                redBackground = !redBackground
            }
        }
    }
}

推荐答案

请注意,StateObject.init(wrappedValue:)初始化式的取值为autoclosure.

init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)

@autoclosure被传播到自动生成的成员式初始值设定项MyView,使得传递给视图的表达式MyViewModel()的计算变得迟缓.这就是导致SwiftUI仅为视图的所有重绘创建一个视图模型的原因.

我找不到任何文档来记录这种@autoclosure的传播,但我可以确认它发生在下面的代码中:

@propertyWrapper
struct MyStateObject {
    var wrappedValue: String
    
    init(wrappedValue: @autoclosure () -> String) {
        self.wrappedValue = wrappedValue()
    }
}

struct MyView {
    @MyStateObject var foo: String
}

编译它时,二进制文件中有一个名为MyView.init(foo: @autoclosure () -> Swift.String) -> MyView的符号.见godbolt.org

另一方面,您的手写初始值设定项不需要@autoclosure,因此MyViewModel()会被迫切地计算出来.现在传递给StateObject.init(wrappedValue:)的延迟表达式只是参数名viewModel,计算起来并不复杂:)

因此,要使用您自己的手写初始化器重新创建相同的行为,您也应该添加@autoclosure:

init(viewModel: @autoclosure @escaping () -> MyViewModel) {
    self._viewModel = StateObject(wrappedValue: viewModel())
}

Swift相关问答推荐

为什么不能使用actor作为AsyncSequence中的状态机类型?

传递给SWIFT ResultBuilder的闭包中结果类型的对象

如何在AudioKit中实现自定义音效 node ?

SWIFT计划计时器方法在时间间隔后未被调用

使用 @resultBuilder 的通用 buildList 函数

表视图插座单元测试失败

在SwiftUI中使用ForEach循环来显示字典项的键和值在Form视图中

Swift UI中视图上的值未更新

将 ArgumentParser 与 Swift 并发一起使用

数组中某些元素的总和

从 actor 的 init 方法调用方法

有没有更快的方法来循环浏览 macOS 上已安装的应用程序?

这是 Int64 Swift Decoding 多余的吗?

ZStack 中的 ProgressView 与 View 的行为不同

如何在 SwiftUI 中通过按钮点击一次为一个更改设置动画?

如何在 GCD 中停止 DispatchWorkItem?

Swift 中的可选数组与空数组

'类 'className' 的 NSManagedObject 必须具有有效的 NSEntityDescription.错误

无法将 NSAttributedString.DocumentAttributeKey 类型的值转换为 .DocumentReadingOptionKey

ld:入口点(_main)未定义.对于架构 x86_64:Xcode 9