在 SwiftUI 中使用参数初始化 @StateObject

Emi*_*pis 44 swift swiftui xcode12

我想知道目前是否有(在询问时,第一个 Xcode 12.0 Beta)使用@StateObject来自初始化程序的参数来初始化 a 的方法。

更具体地说,这段代码可以正常工作:

struct MyView: View {
  @StateObject var myObject = MyObject(id: 1)
}
Run Code Online (Sandbox Code Playgroud)

但这不会:

struct MyView: View {
  @StateObject var myObject: MyObject

  init(id: Int) {
    self.myObject = MyObject(id: id)
  }
}
Run Code Online (Sandbox Code Playgroud)

从我理解的作用@StateObject是使视图成为对象的所有者。我使用的当前解决方法是传递已经初始化的 MyObject 实例,如下所示:

struct MyView: View {
  @ObservedObject var myObject: MyObject

  init(myObject: MyObject) {
    self.myObject = myObject
  }
}
Run Code Online (Sandbox Code Playgroud)

但是现在,据我所知,创建对象的视图拥有它,而这个视图没有。

谢谢。

And*_*kyi 54

简答

StateObject下一个 init: init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)。这意味着它将StateObject在正确的时间创建对象的实例 - 在body第一次运行之前。但这并不意味着您必须在 View 中的一行中声明该实例,例如@StateObject var viewModel = ContentViewModel().

我找到的解决方案是传递一个闭包并允许StateObject在对象上创建实例。这个解决方案效果很好。有关更多详细信息,请阅读下面的长答案

class ContentViewModel: ObservableObject {}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: @autoclosure @escaping () -> ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
    }
}

struct RootView: View {
    var body: some View {
        ContentView(viewModel: ContentViewModel())
    }
}
Run Code Online (Sandbox Code Playgroud)

无论RootView创建多少次body, 的实例都ContentViewModel只有一个。

通过这种方式,您可以初始化@StateObject具有参数的视图模型。

长答案

@StateObject

在第一次运行之前创建一个 value 实例(SwiftUI@StateObject的 Data Essentials)。并且它在整个视图生命周期中保留该值的这一实例。您可以在 a 之外的某个位置创建视图的实例,您将看到of不会被调用。请参阅下面的示例:bodybodyinitContentViewModelonAppear

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()
}

struct RootView: View {
    var body: some View {
        VStack(spacing: 20) {
        //...
        }
        .onAppear {
            // Instances of ContentViewModel will not be initialized
            _ = ContentView()
            _ = ContentView()
            _ = ContentView()

            // The next line of code
            // will create an instance of ContentViewModel.
            // Buy don't call body on your own in projects :)
            _ = ContentView().view
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,将创建实例委托给StateObject.

为什么不应该将 StateObject(wrappedValue:) 与实例一起使用

让我们考虑一个通过传递实例来创建StateObjectwith实例的示例。当根视图触发 的额外调用时,将创建 的新实例。如果您的视图是整个屏幕视图,那可能会工作得很好。尽管如此,最好不要使用此解决方案。因为您永远无法确定父视图何时以及如何重绘其子视图。_viewModel = StateObject(wrappedValue: viewModel)viewModelbodyviewModel

final class ContentViewModel: ObservableObject {
    @Published var text = "Hello @StateObject"
    
    init() { print("ViewModel init") }
    deinit { print("ViewModel deinit") }
}

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel)
        print("ContentView init")
    }

    var body: some View { Text(viewModel.text) }
}

struct RootView: View {
    @State var isOn = false
    
    var body: some View {
        VStack(spacing: 20) {
            ContentView(viewModel: ContentViewModel())
            
            // This code is change the hierarchy of the root view.
            // Therefore all child views are created completely,
            // including 'ContentView'
            if isOn { Text("is on") }
            
            Button("Trigger") { isOn.toggle() }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我点击“触发”按钮 3 次,这是 Xcode 控制台中的输出:

视图模型初始化
内容视图初始化
视图模型初始化
内容视图初始化
视图模型初始化
内容视图初始化
ViewModel 解初始化
视图模型初始化
内容视图初始化
ViewModel 解初始化

正如您所看到的, 的实例ContentViewModel被创建了很多次。这是因为当根视图层次结构发生更改时,其中的所有内容body都会从头开始创建,包括ContentViewModel. 无论您@StateObject在子视图中将其设置为多少。您在根视图中调用的init次数与根视图更新body.

使用闭包

至于StateObjectinit 中的 use 闭包 -init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)我们也可以使用它并传递闭包。ContentViewModel代码与上一节(和)完全相同RootView,但唯一的区别是使用闭包作为 的 init 参数ContentView

ViewModel init
ContentView init
ViewModel init
ContentView init
ViewModel init
ContentView init
ViewModel deinit
ViewModel init
ContentView init
ViewModel deinit

点击“触发”按钮 3 次后 - 输出如下:

内容视图初始化
视图模型初始化
内容视图初始化
内容视图初始化
内容视图初始化

可以看到只ContentViewModel创建了一个实例。也是ContentViewModel在之后创建的ContentView

顺便说一句,最简单的方法是将属性设置为内部/公共并删除 init:

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel
    
    init(viewModel: @autoclosure @escaping () -> ContentViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
        print("ContentView init")
    }

    var body: some View { Text(viewModel.text) }
}
Run Code Online (Sandbox Code Playgroud)

结果是一样的。但viewModel在这种情况下不能是私有财产。


Mar*_*ark 33

@Asperi 给出的答案应该避免 Apple 在他们的 StateObject 文档中这么说

您不直接调用此初始化程序。相反,在视图、应用程序或场景中使用 @StateObject 属性声明一个属性,并提供初始值。

苹果试图在幕后进行很多优化,不要与系统作斗争。

只需为您首先要使用的参数创建ObservableObject一个Published值。然后使用.onAppear()来设置它的值,SwiftUI 将完成剩下的工作。

代码:

class SampleObject: ObservableObject {
    @Published var id: Int = 0
}

struct MainView: View {
    @StateObject private var sampleObject = SampleObject()
    
    var body: some View {
        Text("Identifier: \(sampleObject.id)")
            .onAppear() {
                sampleObject.id = 9000
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这个问题是在 WWDC21 休息室中提出的,答案似乎与文档的警告相矛盾:“[传递给 StateObject.init(wrappedValue:) 的对象]将仅在视图生命周期开始时创建并保持活动状态。StateObject的包装值是一个自动闭包,仅在视图生命周期开始时调用一次。这也意味着 SwiftUI 将在首次创建时捕获计划的值;[...]如果您视图身份没有更改但你传递一个不同的[对象] SwiftUI 不会注意到这一点。” (11认同)
  • 如果 SampleObject 在初始化时需要参数怎么办?另外 .onAppear 在 SwiftUI 2.0 中有点混乱,它可能会被多次调用。 (8认同)
  • @布雷特谢谢。我已经尝试过了,也确实看到了正确的行为。视图被重新创建,但 StateObject 未被覆盖。我只是希望这在 SwiftUI 的未来版本中不会改变。 (3认同)
  • 如果您使用上述方法,那么如何将核心数据 viewContext 注入视图模型中。 (3认同)
  • 我需要找到我在哪里读到这篇文章,但我认为@Asperi的答案是正确的,因为视图 ​​init 可能会被多次调用,但 StateObject 不会被覆盖,它只在第一次被设置。 (2认同)
  • `onAppear` 解决方案适用于简单的用例,但如果 `SampleObject` 更复杂并且需要在其 .init 中添加参数(例如注入的服务),则它就不起作用了。@Brett 如果使用自定义 init 是正确的方法,请分享 Apple 文档。这将解决很多解决方法 (2认同)

Asp*_*eri 24

这是解决方案的演示。使用 Xcode 12b 测试。

class MyObject: ObservableObject {
    @Published var id: Int
    init(id: Int) {
        self.id = id
    }
}

struct MyView: View {
    @StateObject private var object: MyObject
    init(id: Int = 1) {
        _object = StateObject(wrappedValue: MyObject(id: id))
    }

    var body: some View {
        Text("Test: \(object.id)")
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 不要错过这里的巨大警告:wrappedValue 是一个闭包!如果您执行诸如在一行上创建视图模型,然后将其传递到另一行上的wrappedValue 之类的操作,则您将默默地破坏StateObject 并在每次更新时获取视图模型的新实例。这绝对让我措手不及 (26认同)
  • 在 @StateObject 的文档中,它说“你不能直接调用这个初始化程序”。 (14认同)
  • 这个答案有错误的做法请勿使用!请参阅马克的回答以了解正确的方法,甚至还可以查看苹果公司关于这种错误方法的警告。 (8认同)
  • 每次重建视图时都会初始化该对象,这会在任何状态更改时发生,这肯定违背了 StateObject 的目的吗? (5认同)
  • 每次重建视图时都会调用 init() ,是的。然而 State 和 StateObject 只会在第一次初始化它们的对象。 (4认同)
  • 这是在 WWDC21 的 SwiftUI 数字休息室期间提出的,并在这种情况下得到了 SwiftUI 团队的认可。SwiftUI-Lab.com 对数字休息室的惊人总结记录在这里:https://www.swiftui-lab.com/random-lessons#data-10 当然,这种软背书是否使您和您的项目你很容易遵循这种方法。 (3认同)

cic*_*rgo 6

我想我找到了一种解决方法,能够控制用 @StateObject 包装的视图模型的实例化。如果您没有在视图上将视图模型设为私有,则可以使用合成的成员初始化,这样您就可以毫无问题地控制它的实例化。如果您需要一种公共方式来实例化视图,您可以创建一个工厂方法来接收视图模型依赖项并使用内部合成的 init。

import SwiftUI

class MyViewModel: ObservableObject {
    @Published var message: String

    init(message: String) {
        self.message = message
    }
}

struct MyView: View {
    @StateObject var viewModel: MyViewModel

    var body: some View {
        Text(viewModel.message)
    }
}

public func myViewFactory(message: String) -> some View {
    MyView(viewModel: .init(message: message))
}
Run Code Online (Sandbox Code Playgroud)

  • 无论您在何处调用“MyView(params)”,您都将调用“myViewFactory(params)” (2认同)