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

Swift UI - How to access a @State variable externally?

我有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() 总是 returns 60,即使我在内容视图中修改了 TexField 的值(通过在文本字段中手动输入)。

如何从应用程序委托中获取状态变量的包装 value/real 值?

向 appDelegate 添加一个变量

var seconds : Int? 

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

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

最终以 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 旅程只到此为止,所以现在就可以了,哈哈。如果您有更好的解决方案,请post

您的 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.

内部有效

如果你需要在不同地方 share/access 的东西然后使用 class 确认 ObservableObject 作为视图模型类型,并且作为这样的实例 class 是一个参考输入然后传递它,你可以 use/access 来自不同地方的相同对象。

更新:

why the value stays to "60" even inside the ContentView

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

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

准备并测试 Xcode 13 / iOS 15

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

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

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

正如其他人所说,ContentView 是一个结构,它是一个值类型,这意味着它的内容在所有使用站点中被复制,因此没有像 class 这样的唯一实例有,你最终有多个实例。

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

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

这是使用 Swift 的好处之一UI,与 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 更多的是关于数据而不是视图本身,所以如果你想在多个地方访问相同的数据,确保你循环存储而不是数据附加到的视图。还要确保该数据的真实来源是 class,因为对象引用保证指向相同的内存位置(当然假设引用没有改变)。