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,因为对象引用保证指向相同的内存位置(当然假设引用没有改变)。
我有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,因为对象引用保证指向相同的内存位置(当然假设引用没有改变)。