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) 我怎么可以将两个不同的副本传递给两个不同的TextField
s,但这些文本字段将是同步的,并且看似具有相同的绑定(一个不同的例如,比计时器中的那个)?
请注意,您将 $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 变量?的主要内容,如果未能解决你的问题,请参考以下文章