Swift UI - 如何从外部访问 @State 变量?

Posted

技术标签:

【中文标题】Swift UI - 如何从外部访问 @State 变量?【英文标题】:Swift UI - How to access a @State variable externally? 【发布时间】:2021-09-21 16:16:00 【问题描述】:

我有ContentView.swift

struct ContentView: View 
    @State var seconds: String = "60"

    init(_ appDelegate: AppDelegate) 
        launchTimer()
    
    
    func launchTimer() 
        let timer = DispatchTimeInterval.seconds(5)
        let currentDispatchWorkItem = DispatchWorkItem 
            print("in timer", "seconds", seconds) // Always `"60"`
            print("in timer", "$seconds.wrappedValue", $seconds.wrappedValue) // Always `"60"`

            launchTimer()
        
        
        DispatchQueue.main.asyncAfter(deadline: .now() + timer, execute: currentDispatchWorkItem)
    

    var body: some View 
        TextField("60", text: $seconds)
    
    
    func getSeconds() -> String 
        return $seconds.wrappedValue;
    

还有AppDelegate.swift

class AppDelegate: NSObject, NSApplicationDelegate 
    var contentView = ContentView()

    func applicationDidFinishLaunching(_ aNotification: Notification) 
        launchTimer()
    

    
    func launchTimer() 
        print(contentView.getSeconds()) // Always `"60"`

        let timer = DispatchTimeInterval.seconds(Int(contentView.getSeconds()) ?? 0)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + timer) 
            self.launchTimer()
        
    
    

    @objc func showPreferenceWindow(_ sender: Any?) 
        if(window != nil) 
            window.close()
        
        
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.isReleasedWhenClosed = false
        window.contentView = NSHostingView(rootView: contentView)
        
        window.center()
        window.makeKeyAndOrderFront(nil)
        NSApplication.shared.activate(ignoringOtherApps: true)
    


contentView.getSeconds() 始终返回 60,即使我在内容视图中修改了 TexField 的值(通过在文本字段中手动输入)。

如何从应用委托中获取状态变量的包装值/实际值?

【问题讨论】:

制作一个 ViewModel 并以某种方式共享它。 SwiftUI 视图不应该以这种方式访问​​。 这是一种方法***.com/a/69055669/12299030 @MaximeDupré SwiftUI 中的视图是可传递的——你不能持有对它们的引用并期望它们持续存在。您没有在代码中显示如何在 App Delegate 中获得 contentView,但它肯定不会是在屏幕上呈现的同一个实例。一般来说,父视图/对象不应该尝试引用子视图拥有的东西——数据应该始终从父视图传递给子视图。 编辑您的问题,向我们展示在屏幕上显示ContentView 的代码。 啊,现在你真的在问一个意见类型的问题。你问我(我认为)一个完整的初学者,想要学习编写 Mac 应用程序(而不是 ios 应用程序),是否应该使用 Swift AppKit 或 SwiftUI。我不知道。 :) 但是 SwiftUI 肯定是一个有趣的选择;但是,我真的认为您应该先阅读 Apple 的交互式教程。 developer.apple.com/tutorials/swiftui 你也应该观看一些 WWDC SwiftUI 视频。 developer.apple.com/videos/wwdc2021?q=swiftui 【参考方案1】:

@State/@StateObject 是一件棘手的事情,发生的事情是 SwiftUI 将状态值连接到 UI 层次结构中的某个视图实例。

在您的情况下,@State var seconds: String = "60" 连接到下面的视图(简化方案):

NSWindow
  -> NSHostingView
    -> ContentView <----- this is where the @State usage is valid

正如其他人所说,ContentView 是一个结构,它是一种值类型,这意味着它的内容会在所有使用站点中复制,因此您最终会得到多个实例,而不是像类那样拥有唯一的实例.

只有其中一个实例,即 SwiftUI 在将视图添加到 UI 树时创建的副本,是连接到文本字段并得到更新的实例。

为了让事情更有趣,State property wrapper 也是一个结构,这意味着它“遭受”与视图相同的症状。

这是使用 SwiftUI 的好处之一,与 UIKit 相比,您根本不需要关心视图实例。

现在,如果您想使用来自AppDelegate 或任何其他地方的seconds 的值,您将不得不循环数据存储而不是视图。

对于初学者,更改视图以接收@Binding instead

struct ContentView: View 
    @Binding var seconds: String = "60"

    // the rest of the view code remains the same

然后创建一个存储数据的ObservableObject,并在创建视图时注入其$seconds绑定:

class AppData: ObservableObject 
    @Published var seconds: String = "60"


class AppDelegate 
    let appData = AppData()

    // ... other code left out for clarity

    @objc func showPreferenceWindow(_ sender: Any?) 
        // ... 
        let contentView = ContentView(seconds: appData.$seconds
        window.contentView = NSHostingView(rootView: contentView)
        // ... 
    

SwiftUI 更多的是关于数据而不是视图本身,所以如果你想在多个地方访问相同的数据,请确保循环存储而不是数据附加到的视图。并且还要确保该数据的真实来源是一个类,因为对象引用保证指向相同的内存位置(当然假设引用没有改变)。

【讨论】:

(1/2) 感谢您的回答。 “为了让事情更有趣,State 属性包装器也是一个结构,这意味着它“遭受”与视图相同的症状。” ->?。这就是为什么它总是在我的ContentView 的计时器中打印"60" 的原因吗?另外,为什么传递给TextField 的状态变量seconds 的“副本”是“好的”副本:TextField("60", text: $seconds)?传递给TextField的副本和连接到struct实例的副本不是两个不同的副本吗? (2/2) 我怎么可以将两个不同的副本传递给两个不同的TextFields,但这些文本字段将是同步的,并且看似具有相同的绑定(一个不同的例如,比计时器中的那个)? 请注意,您将 $seconds(注意美元符号)传递给文本字段,这将创建一个绑定,该绑定在与 seconds 属性相同的存储上运行(seconds 实际上是由@State 属性包装器包装的属性) 是的,但我也在DispatchQueue/timer 中使用$seconds(我的ContentView inside),但值始终是@987654347 @,即使在文本字段被修改后。此外,$seconds 不只是状态变量 projectedValue -> seconds.projectedValue 的简写,seconds (State) 和 seconds.projectedValue (Binding) 结构,这意味着它们是不同的副本每次我在我的代码中引用它们? @MaximeDupré 都是关于值类型与引用类型的,尝试将ContentView 设为类而不是结构,看看情况是否开始改变。【参考方案2】:

您的 ContentView(以及任何其他 SwiftUI 视图)是 struct,即。值类型,因此在您使用它的每个地方 - 您都使用初始创建值的新副本,即:

class AppDelegate: NSObject, NSApplicationDelegate 
    var contentView = ContentView()       // << 1st copy

    ...
    
    func launchTimer() 
        print(contentView.getSeconds())   // << 2nd copy
        let timer = DispatchTimeInterval.seconds(Int(contentView.getSeconds()) ?? 0)  // << 3d copy
        
    ...

    
    
    ...

    @objc func showPreferenceWindow(_ sender: Any?) 

    ...
        
        window.contentView = NSHostingView(rootView: contentView)   // << 4th copy
        
    

您不应在外部使用任何@State,因为它仅在视图的body 内部有效。

如果您需要在不同的地方共享/访问某些内容,则使用确认 ObservableObject 的类作为视图模型类型,并且由于此类的实例是引用类型,然后在这里和那里传递它,您可以使用/访问相同的对象来自不同的地方。

更新:

为什么即使在 ContentView 中,该值也保持为“60”

您在init 中调用launchTimer,但在那个地方State 尚未构造,因此绑定到它是无效的。正如上面已经写的状态在正文中有效,所以当绑定准备好时,你还必须在body 中设置你的计时器,比如在.onAppear 中,如下所示

var body: some View 
    TextField("60", text: $seconds)
         .onAppear 
              launchTimer()
         

使用 Xcode 13 / iOS 15 准备和测试

【讨论】:

我明白了。那个 JS 背景把我弄乱了:D。我理解你的回答,但它似乎没有回答为什么即使在ContentView内部(见编辑) 的值也保持在“60” @MaximeDupré,查看更新部分 那么国家是什么时候建立的?您知道一些指向初始化顺序的文档吗?如果 State 尚未构建,如何获得 projectedValue(也就是使用变量上的 $ 符号)?我认为您的回答是正确的,但我仍然想了解这些问题..【参考方案3】:

将变量添加到 appDelegate

var seconds : Int? 

然后在您的 contentView 中执行此操作:

    .onChange(of: seconds)  seconds in
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else return
        appDelegate.seconds = seconds
    

【讨论】:

感谢您的回答。你知道为什么我的例子不起作用/你能指出一些可以解释原因的文档吗?您的解决方案应该有效 - 但是,onChange 似乎仅在 macOS 11 中可用,而我在 10.15 上。你知道在 10.15 上可以使用的另一种方法吗? 您正在 AppDelegate 中创建 ContetView 的实例,这与您的 @main: App 创建的实例不同。基本上你正在使用 ContetView struct 的 2 个不同实例,这就是它保持打印 60 的原因。 @MaximeDupré 你使用的是 swiftUI 生命周期还是 uiKit ? 哦...我认为是 uiKit,因为 swiftUI 生命周期仅在 macOS 11 中可用 您确定我正在使用其他实例吗?我似乎使用了正确的(见编辑)【参考方案4】:

最终以AppDelegate 作为参数来实例化ContentView。我在AppDelegate 中添加了一个seconds 属性,以便由ContentView 修改。

AppDelegate.swift

import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate 
    var contentView: ContentView?
    var seconds = 60
    
    func applicationDidFinishLaunching(_ aNotification: Notification) 
        self.contentView = ContentView(self)        

        launchTimer()
    
    
    func launchTimer() 
        let timer = DispatchTimeInterval.seconds(seconds)
        
        print(timer)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + timer) 
            self.launchTimer()
        
    


ContentView.swift

import SwiftUI

struct ContentView: View 
    @State var seconds: String = "60"
    var appDelegate: AppDelegate
    
    init(_ appDelegate: AppDelegate) 
        self.appDelegate = appDelegate
    

    var body: some View 
        TextField("60", text: $seconds, onCommit: 
            self.appDelegate.seconds = Int($seconds.wrappedValue) ?? 0
         )
    


这段代码伤害了我的大脑,当然有更好的方法来做到这一点?。但我的 Swift 之旅只到此为止,所以现在就可以了,哈哈。如果您有更好的解决方案,请发布。

【讨论】:

以上是关于Swift UI - 如何从外部访问 @State 变量?的主要内容,如果未能解决你的问题,请参考以下文章

如何从线程内访问外部属性

如何从 UI 测试用例类访问 SwiftUI 视图的属性?

如何在单独的函数中访问@State 变量

swift延迟后如何更新UI

如何从 ui-router statechange 返回 $state.current.name

使用 Swift 反应原生自定义 UI 组件:如何在事件中访问 reactTag