代码之家  ›  专栏  ›  技术社区  ›  Maxime Dupré

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

  •  0
  • Maxime Dupré  · 技术社区  · 3 年前

    我有 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 在我的内容视图中(通过手动键入文本字段)。

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

    1 回复  |  直到 3 年前
        1
  •  2
  •   Cristik    3 年前

    @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相比,您根本不关心视图实例。

    现在,如果您想使用的值 seconds 从…起 AppDelegate ,或任何其他地方,则必须循环使用数据存储而不是视图。

    对于初学者,请更改视图以接收 @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更多的是关于数据,而不是视图本身,所以如果你想在多个地方访问相同的数据,请确保你循环使用存储,而不是数据所附的视图。还要确保数据的真实性来源是一个类,因为对象引用保证指向相同的内存位置(当然假设引用不会更改)。

        2
  •  1
  •   Asperi    3 年前

    你的 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”

    你打电话 launchTimer 在里面 init ,但是在那个地方 State 尚未构造,因此绑定到它是无效的。如上文所述 状态在正文中有效 ,所以你也必须在中设置计时器 什么时候可以装订,比如在 .onAppear ,如下所示

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

    准备&使用Xcode 13/iOS 15进行测试

    demo

        3
  •  0
  •   keyvan yaghoubian    3 年前

    向appDelegate添加变量

    var seconds : Int? 
    

    然后在您的内容中执行此操作查看:

        .onChange(of: seconds) { seconds in
            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
            appDelegate.seconds = seconds
        }
    
        4
  •  0
  •   Maxime Dupré    3 年前

    最终实例化 ContentView AppDelegate 作为参数。我添加了一个 seconds 的属性 AppDelegate 由修改 内容视图 .

    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之旅才刚刚结束,所以现在就到此为止,哈哈。如果你有更好的解决方案,请发布。