64

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

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

struct MyView: View {
  @StateObject var myObject = MyObject(id: 1)
}

但这不会:

struct MyView: View {
  @StateObject var myObject: MyObject

  init(id: Int) {
    self.myObject = MyObject(id: id)
  }
}

据我了解的作用@StateObject是使视图成为对象的所有者。我使用的当前解决方法是像这样传递已经初始化的 MyObject 实例:

struct MyView: View {
  @ObservedObject var myObject: MyObject

  init(myObject: MyObject) {
    self.myObject = myObject
  }
}

但是现在,据我了解,创建对象的视图拥有它,而这个视图不拥有。

谢谢。

4

9 回答 9

39

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

您不直接调用此初始化程序。相反,在 View、App 或 Scene 中声明具有 @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
            }
    }
}
于 2020-08-29T09:30:58.630 回答
38

这是解决方案的演示。使用 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)")
    }
}
于 2020-06-29T10:22:33.550 回答
3

就像@Mark 指出的那样,您不应该@StateObject在初始化期间处理任何地方。这是因为在@StateObjectView.init() 之后以及在调用主体之前/之后稍微初始化了。

关于如何将数据从一个视图传递到另一个视图,我尝试了很多不同的方法,并提出了一个适合简单和复杂视图/视图模型的解决方案。

版本

Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)

此解决方案适用于 iOS 14.0 以上版本,因为您需要.onChange()视图修饰符。该示例是在 Swift Playgrounds 中编写的。如果您需要onChange较低版本的 like 修饰符,您应该编写自己的修饰符。

主视图

主视图@StateObject viewModel处理所有视图逻辑,例如按钮点击和“数据” (testingID: String)-> 检查 ViewModel

struct TestMainView: View {
    
    @StateObject var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            Button(action: { self.viewModel.didTapButton() }) {
                Text("TAP")
            }
            Spacer()
            SubView(text: $viewModel.testingID)
        }.frame(width: 300, height: 400)
    }
    
}

主视图模型 (ViewModel)

viewModel 发布一个testID: String?. 此 testID 可以是任何类型的对象(例如,配置对象 aso,您可以命名它),对于本示例,它只是子视图中也需要的字符串。

final class ViewModel: ObservableObject {
    
    @Published var testingID: String?
    
    func didTapButton() {
        self.testingID = UUID().uuidString
    }
    
}

因此,通过点击按钮,我们ViewModel将更新testID. 我们也希望这一点testIDSubView如果它发生变化,我们也希望我们SubView能够识别和处理这些变化。通过ViewModel @Published var testingID我们能够发布对我们视图的更改。现在让我们看看我们的SubViewSubViewModel

子视图

所以SubView有自己@StateObject的处理自己的逻辑。它与其他视图和 ViewModel 完全分离。在此示例中,SubView仅显示其 中的 testID MainView。但请记住,它可以是任何类型的对象,例如数据库请求的预设和配置。

struct SubView: View {
    
    @StateObject var viewModel: SubviewModel = .init()
    
    @Binding var test: String?
    init(text: Binding<String?>) {
        self._test = text
    }
    
    var body: some View {
        Text(self.viewModel.subViewText ?? "no text")
            .onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }
            .onAppear(perform: { self.viewModel.updateText(text: test) })
    }
}

为了“连接”我们testingID 发布的我们MainViewModel,我们SubView@Binding. 所以现在我们testingIDSubView. 但是我们不想直接在视图中使用它,而是需要将数据传递给我们的SubViewModel,记住我们的 SubViewModel 是一个@StateObject处理所有逻辑的。而且我们不能@StateObject在视图初始化期间将值传递给我们,就像我在开始时写的那样。此外,如果我们的数据(testingID: String)发生变化MainViewModel,我们SubViewModel应该识别并处理这些变化。

因此我们使用两个ViewModifiers.

改变

.onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }

onChange修饰符订阅我们@Binding属性的更改。因此,如果它发生变化,这些变化就会传递给我们的SubViewModel. 请注意,您的属性必须是Equatable。如果你传递一个更复杂的对象,比如 a ,Struct确保在你的.Struct

出现

我们需要onAppear处理“第一个初始数据”,因为 onChange 在您的视图第一次初始化时不会触发。它只是为了改变

.onAppear(perform: { self.viewModel.updateText(text: test) })

好的,这里是SubViewModel,我猜没有什么可以解释的了。

class SubviewModel: ObservableObject {
    
    @Published var subViewText: String?
    
    func updateText(text: String?) {
        self.subViewText = text
    }
}

现在您的数据在MainViewModelSubViewModel之间是同步的,并且这种方法适用于具有许多子视图和这些子视图的子视图的大型视图等。它还使您的视图和相应的视图模型具有高度的可重用性。

工作示例

GitHub 上的游乐场: https ://github.com/luca251117/PassingDataBetweenViewModels

补充说明

为什么我使用onAppearandonChange而不是 only onReceive:似乎将这两个修饰符替换为onReceive会导致连续数据流SubViewModel updateText多次触发。如果您需要流式传输数据以进行演示,这可能很好,但如果您想处理网络呼叫,例如,这可能会导致问题。这就是为什么我更喜欢“两个修饰符方法”。

个人注意:请不要在相应视图范围之外修改 stateObject。即使以某种方式可能,这也不是它的意思。

于 2020-11-23T07:53:33.193 回答
3

我想我找到了一种解决方法,可以控制用@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))
}
于 2020-10-20T13:23:31.803 回答
3

简答

具有StateObject下一个初始化:init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType). 这意味着StateObject将在正确的时间创建对象的一个​​实例——在body第一次运行之前。但这并不意味着您必须在 View like 中的一行中声明该实例@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())
    }
}

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

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

长答案

@StateObject

在第一次@StateObject运行之前创建一个 value 实例( SwiftUI 中的 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
        }
    }
}

因此,将创建实例委托给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() }
        }
    }
}

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

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

如您所见, 的实例ContentViewModel被创建了很多次。这是因为当根视图层次结构发生变化时,它的所有内容都是body从头开始创建的,包括ContentViewModel. 无论您@StateObject在子视图中将其设置为什么。您在根视图中调用的问题init与根视图如何更新body.

使用闭包

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

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) }
}

在“触发”按钮被点击 3 次后 - 输出是下一个:

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

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

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

struct ContentView: View {
    @StateObject var viewModel: ContentViewModel
}

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

于 2022-02-06T18:13:15.100 回答
1

目前我对@StateObjects 并没有很好的解决方案,但我试图在@main App 中使用它们作为@EnvironmentObjects 的初始化点。我的解决方案是不使用它们。我把这个答案放在这里给那些试图和我做同样事情的人。

在提出以下建议之前,我为此苦苦挣扎了一段时间:

这两个 let 声明在文件级别

private let keychainManager = KeychainManager(service: "com.serious.Auth0Playground")
private let authenticatedUser = AuthenticatedUser(keychainManager: keychainManager)

@main
struct Auth0PlaygroundApp: App {

    var body: some Scene {
    
        WindowGroup {
            ContentView()
                .environmentObject(authenticatedUser)
        }
    }
}

这是我发现使用参数初始化 environmentObject 的唯一方法。如果没有 keychainManager,我将无法创建 authenticatedUser 对象,并且我不打算更改整个应用程序的体系结构以使所有注入的对象都不带参数。

于 2020-10-09T07:55:27.883 回答
1

@cicerocamargo 的回答是一个很好的建议。我在我的应用程序中遇到了同样的困难,试图弄清楚如何在我的@StateObject 视图模型中注入依赖项,并在经过多次测试后得出了相同的答案。这样,视图模型在所有情况下都只会被实例化一次。

class MyViewModel: ObservableObject {
   @Published var dependency: Any

   init(injectedDependency: Any) {
       self.dependency = injectedDependency
   }
}

struct  MyView: View {
    @StateObject var viewModel: MyViewModel
    
    var body: some View {
       // ...
    } 
}

struct MyCallingView: View {
    var body: some View {
        NavigationLink("Go to MyView",
            destination: MyView(viewModel: MyViewModel(injectedDependency: dependencyValue)))
    }
}

唯一要记住的是,视图模型的实例化应该与视图的实例化内联。如果我们将调用视图代码更改为:

struct MyCallingView: View {
    var body: some View {
        let viewModel = MyViewModel(injectedDependency: dependencyValue)
        NavigationLink("Go to MyView",
            destination: MyView(viewModel: viewModel))
    }
}

那么编译器将无法优化此代码,并且每次MyCallingView失效并需要重绘时都会实例化视图模型。从好的方面来说,即使每次都实例化它,也只会使用原始实例。

于 2021-10-28T09:14:48.580 回答
0

Asperi 的答案很棒,但由于文件所说的内容,它似乎不是很完美。我发现了以下方法,但我不知道它是否有效。

class Object: ObservableObject {
    let id: String
    init(id: String) {
        self.id = id
    }
}

struct ParentView: View {
    @State var obj: Object?
    var body: some View {
        if let obj = obj {
            ChildView().environmentObject(obj)
        } else {
            Button("Tap") {
                self.obj = Object(id: "id")
            }
        }
    }
}

struct ChildView: View {
    @EnvironmentObject var customObject: Object
    var body: some View {
        Text(customObject.id)
    }
}
于 2021-12-18T07:23:11.353 回答
0

每当我的视图表现不佳时,我经常访问此页面。我意识到我需要从 UIKit 调整我的想法,我会自由地使用 ViewModels 来封装视图状态。我对这些对象的初始化和拆卸以及我的观点更有信心。将 StateObject 用于具有注入状态的 ViewModel 有点像黑匣子并且令人困惑。我认为这篇文章的答案证明了这一点。

我现在正在努力的是这里提出的模型https://nalexn.github.io/clean-architecture-swiftui/

我仍然会使用 StateObject 来严格查看属性,但每当我发现自己想要将状态注入对象时,我都会认为它可能是代码异味。例如具有 UserViewModel(id: 1) 的 UserView。我已经使用 _state = ... 方法在视图中尝试了注入的 viewModel 和 init,虽然它们可能一开始可以工作,但我遇到了一些错误。

上面链接的干净架构促进了一个单独的 AppState,它将通过绑定将其数据传递到视图。似乎有点 Manager/Singleton ish,但至少我的状态管理更加明显。

于 2021-11-26T10:36:59.107 回答