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

时间:2020-06-29 10:14:19

标签: swift swiftui xcode12

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

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

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

但是据我所知,创建对象的视图拥有该对象,而该视图则没有。

谢谢。

5 个答案:

答案 0 :(得分:13)

应避免@Asperi给出的答案,苹果公司在其documentation for StateObject中这样说。

您不直接调用此初始化程序。而是在View,App或Scene中使用@StateObject属性声明一个属性,并提供初始值。

Apple尝试在后台进行很多优化,不要与系统抗争。

只需为要首先使用的参数创建一个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
            }
    }
}

答案 1 :(得分:7)

这里是解决方案的演示。经过Xcode 12b测试。

window

答案 2 :(得分:0)

我知道这里已经有一个可以接受的答案,但是我必须在这个问题上同意@malhal。我认为init将被多次调用,这是@StateObject意图的相反行为。

目前我对@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对象,而且我也不会更改整个App的体系结构,以使所有注入的对象都没有参数。

答案 3 :(得分:0)

我想我找到了一种解决方法,该方法可以控制用@StateObject包装的视图模型的实例化。如果您不将视图模型设为私有视图,则可以使用合成的逐成员init,在那里您可以毫无问题地控制其实例化。如果您需要一种公共的方式来实例化视图,则可以创建一个工厂方法来接收您的视图模型依赖性并使用内部综合的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))
}

答案 4 :(得分:0)

就像@Mark指出的那样,您不应在初始化期间在任何地方处理@StateObject。这是因为@StateObject在View.init()之后,刚好在正文被调用之前/之后被初始化。

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

版本

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

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

主视图

主视图具有@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可以是任何类型的对象(例如,配置对象a.s.o,您可以为其命名),在这个示例中,它只是子视图中也需要的字符串。

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

因此,通过点击按钮,我们的ViewModel将更新testID。我们还希望testID中的这个SubView,并且如果它发生变化,我们也希望SubView能够识别并处理这些变化。通过ViewModel @Published var testingID,我们可以发布对视图的更改。现在,让我们看一下 SubView SubViewModel

SubView

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

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。因此,现在我们在testingID中拥有相同的SubView。但是我们不想直接在视图中使用它,相反,我们需要将数据传递到SubViewModel中,请记住我们的SubViewModel是@StateObject来处理所有逻辑。而且,就像我一开始写的那样,我们无法在视图初始化期间将值传递到@StateObject中。另外,如果我们的{{1}中的数据(testingID: String)发生了变化,我们的MainViewModel应该能够识别并处理这些变化。

因此,我们使用两个SubViewModel

onChange

ViewModifiers

onChange 修饰符订阅我们.onChange(of: self.test) { (text) in self.viewModel.updateText(text: text) } 属性中的更改。因此,如果更改,这些更改将传递给我们的@Binding。请注意,您的媒体资源必须平等。如果您传递了一个更复杂的对象,例如SubViewModel,请确保在您的Struct实现该协议。

在出现

我们需要Struct处理“第一个初始数据”,因为onChange不会在您的视图第一次被初始化时触发。它仅用于更改

onAppear

好,这是 SubViewModel ,我想对此没有更多解释。

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

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

工作示例

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

附加说明

为什么我使用class SubviewModel: ObservableObject { @Published var subViewText: String? func updateText(text: String?) { self.subViewText = text } } onAppear而不是仅使用onChange:看来用onReceive替换这两个修饰符会导致连续数据流触发onReceive次。如果您需要流传输数据以进行演示,则可以,但是例如,如果您想处理网络呼叫,则可能会导致问题。这就是为什么我更喜欢“双重修正法”。

个人注意:请不要在相应视图范围之外修改stateObject。即使可能,但这也不意味着什么。