英文:
Why is one View updating and one not, as a variable changes?
问题
ContentView:
import SwiftUI
struct ContentView: View {
@State var timer: Timer?
@State var timeGone = 0
func startTimer(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in
timeGone += 1
})
}
var MenuBar: some View {
VStack{
Text(String(timeGone))
.font(.system(size: 12))
.foregroundColor(.white)
.padding(.vertical, 1)
.padding(.horizontal, 4)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.stroke(Color.white, lineWidth: 1.2)
).onAppear {
}
}
.padding(.vertical, 2)
.padding(.horizontal, 2)
.background(Color.clear)
.id("MenuBar")
}
var body: some View {
VStack {
VStack{
Text(String(timeGone))
.fontWeight(.semibold)
.font(.system(size: 90))
}
Spacer()
.frame(height: 30)
HStack{
Button("Start") {
startTimer()
}
.buttonStyle(StartButtonStyle())
}
}
.fixedSize()
.frame(width: 340.0, height: 200.0)
.padding(.all, 50.0)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AppDelegate:
import SwiftUI
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem!
private lazy var contentView: NSView? = {
let view = (statusItem.value(forKey: "window") as? NSWindow)?.contentView
return view
}()
func applicationDidFinishLaunching(_ notification: Notification) {
setupMenuBar()
}
func setupMenuBar(){
statusItem = NSStatusBar.system.statusItem(withLength: 45)
guard let contentView = self.contentView,
let menuButton = statusItem.button
else {return}
let hostingView = NSHostingView(rootView: ContentView().MenuBar)
hostingView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(hostingView)
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: contentView.topAnchor),
hostingView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
hostingView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
hostingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
如您所见,在ContentView文件中,我有一个Body视图,它是一个带有按钮的窗口,以及包含一个变量的文本标签,该变量在按下按钮后每秒递增一次。我还有一个MenuBar视图,它只是一个文本标签,其中包含与前一个相同的变量。此MenuBar视图将文本标签显示在MacOS的菜单栏上,而不是在窗口上。
我不明白的是为什么第二个文本标签在变量更改时更新,但第一个文本标签没有更新。
我没有特别尝试过什么,因为我甚至不确定如何解决这个问题。在我的头脑中,两个标签都应相应地更新。
提前感谢您的任何答案!
英文:
I have this short Code for my swiftUI project:
ContentView:
import SwiftUI
struct ContentView: View {
@State var timer: Timer?
@State var timeGone = 0
func startTimer(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in
timeGone += 1
})
}
var MenuBar: some View {
VStack{
Text(String(timeGone))
.font(.system(size: 12))
.foregroundColor(.white)
.padding(.vertical, 1)
.padding(.horizontal, 4)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.stroke(Color.white, lineWidth: 1.2)
).onAppear {
}
}
.padding(.vertical, 2)
.padding(.horizontal, 2)
.background(Color.clear)
.id("MenuBar")
}
var body: some View {
VStack {
VStack{
Text(String(timeGone))
.fontWeight(/*@START_MENU_TOKEN@*/.semibold/*@END_MENU_TOKEN@*/)
.font(.system(size: 90))
}
Spacer()
.frame(height: 30)
HStack{
Button("Start") {
startTimer()
}
.buttonStyle(StartButtonStyle())
}
}
.fixedSize()
.frame(width: 340.0, height: 200.0)
.padding(.all, 50.0)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AppDelegate:
import SwiftUI
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem!
private lazy var contentView: NSView? = {
let view = (statusItem.value(forKey: "window") as? NSWindow)?.contentView
return view
}()
func applicationDidFinishLaunching(_ notification: Notification) {
setupMenuBar()
}
func setupMenuBar(){
statusItem = NSStatusBar.system.statusItem(withLength: 45)
guard let contentView = self.contentView,
let menuButton = statusItem.button
else {return}
let hostingView = NSHostingView(rootView: ContentView().MenuBar)
hostingView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(hostingView)
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: contentView.topAnchor),
hostingView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
hostingView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
hostingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
As you can see in the ContentView file, I have the Body view, which is a window with a button on it and a text label containing a variable, which increments every second after the button is pressed. I also have the MenuBar view, which is just a text label with the same variable in it as the previous one. This MenuBar view displays the textLabel on the Menu Bar of MacOS rather than on a window.
What I can't understand is why the second text label updates as the variable changes but the first one doesn't.
I've not really tried much in particular as I'm not even sure how to approach this. It'd just make sense in my head that both labels update accordingly.
Thanks in advance for any Answers!
答案1
得分: 0
你有一个创建用于窗口的ContentView
实例。它包含了一个timer
和timerGone
变量。
当你使用以下方式创建菜单栏视图时:
let hostingView = NSHostingView(rootView: ContentView().MenuBar)
你实际上创建了一个全新的内容视图(通过ContentView()
),它具有完全独立的timer
和不同的timerGone
变量。这些变量与窗口中创建的变量没有关联(除了它们都是从相同的模板构建而来)。
换句话说,你有两个完全独立的ContentView
实例,它们唯一共享的是它们的类型。
要解决这个问题,你需要将模型(在这种情况下是timerGone
变量)提取到一个单独的对象中,这个对象由显示该值的两个视图共享。
以下是带有示例的Playground:
// 在这里插入代码
在这段代码中,“模型”——即不同视图之间共享的信息——存储在TimerModel
类的实例中。这个类是一个ObservableObject
,所以每当计数发生变化时,它可以通知监听者。
TickCountView
是一个可以显示TimerModel
中的tickCount
的视图。它从创建时传递给它的TimerModel
实例获取值。
TimerControlView
是一个引用TimerModel
实例并允许你控制它的视图。它使用TickCountView
,并将其指向自己的timerModel
,还添加了一个按钮,用于启动计时器。当计时器触发时,它会告诉模型对象更新计数。
DemonstrationView
创建了一个TimerModel
实例,该实例将由其多个子视图共享。这个实例是当前计数的“唯一真相之源”。DemonstrationView
创建了两个视图,它们都引用了这个唯一的真相之源。因此,当你按下按钮并控制视图开始对TimerModel
实例进行更改时,两个视图都会被通知到这个更改,并可以更新它们显示的内容。
对于SwiftUI来说,重要的是你有一个单一的位置来保存共享信息——唯一的真相之源,而任何想要了解该信息的视图都会引用到这个真相之源。SwiftUI提供了像Bindings
或ObservableObjects
(或者在最新的SDK中是@Observable
)这样的工具来帮助你建立这些引用。
英文:
You have an instance of your ContentView
that is created for the window. It has storage for a timer
and timerGone
variable.
When you create your menu bar view with:
let hostingView = NSHostingView(rootView: ContentView().MenuBar)
you are creating a completely new content view (through ContentView()
) that has its very own storage for a different timer
and a different timerGone
. Those variables have no relationship to the ones created for the window(apart from the fact that the were both built from the same template).
Or to put it another way, you have two complete ContentView
instances and their types are the only thing they share in common.
To fix the problem you would need to pull the model (in this case the timerGone
variable) into a separate object that is shared by the two views that display the value.
Here is a playground with an example:
import SwiftUI
import Cocoa
import Combine
import PlaygroundSupport
// Model class that holds the current tick count
class TimerModel: ObservableObject {
@Published var tickCount: Int = 0
init() {
}
}
struct TickCountView : View {
@ObservedObject var timerModel: TimerModel
var body: some View {
VStack{
Text(String(timerModel.tickCount))
.font(.system(size: 12))
.foregroundColor(.white)
.padding(.vertical, 1)
.padding(.horizontal, 4)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.stroke(Color.white, lineWidth: 1.2)
)
}
.padding(.vertical, 2)
.padding(.horizontal, 2)
.background(Color.clear)
}
}
struct TimerControlView : View {
@ObservedObject var timerModel: TimerModel
@State var timer = Timer.TimerPublisher(interval: 1.0, runLoop: RunLoop.main, mode: .common)
@State var cancelTimer: Cancellable?
var body: some View {
VStack {
VStack{
Text(String(describing: timerModel.tickCount))
.fontWeight(/*@START_MENU_TOKEN@*/.semibold/*@END_MENU_TOKEN@*/)
.font(.system(size: 90))
}
Spacer()
.frame(height: 30)
HStack{
Button("Start") {
cancelTimer = timer.connect()
}
}
}
.fixedSize()
.frame(width: 340.0, height: 200.0)
.padding(.all, 50.0)
.onReceive(timer) { _ in
timerModel.tickCount += 1
}
}
}
struct DemonstrationView: View {
@StateObject var timerModel = TimerModel()
var body: some View {
HStack {
TickCountView(timerModel: timerModel)
TimerControlView(timerModel: timerModel)
}
.frame(width: 640.0, height: 480.0)
}
}
PlaygroundSupport.PlaygroundPage.current.liveView = NSHostingController(rootView: DemonstrationView())
In the code the "model", the bit of information that is shared by different views, is stored in an instance of the TimerModel
class. This class is an ObservableObject
so whenever the tick changes, it can notify listeners.
TickCountView
is a view that can display the tickCount
in a TimerModel
. It gets its value from the TimerModel
instance you pass it when it's created.
TimerControlView
is a view that refers to a TimerModel
instance and allows you to control it. It makes use of TickCountView
and points it at its timerModel, and also adds a button that lets you start the timer. When the timer fires, it tells the model object to update the tick count.
The DemonstrationView
creates a single TimerModel instance that will be shared by its multiple subviews. This instance is the "single source of truth" for the current tick count. The DemonstrationView
creates two views that both refer back to this single source of truth. That way, when you push the button, and the control view starts making changes to the TimerModel
instance, both views are told about the change and can update what they display.
The important point, for SwiftUI, is that you have a single place where shared information is kept - the single source of truth, and any view that wants to know about that information references that truth. SwiftUI provides tools like Bindings
or ObservableObjects
(or @Observable
in the newest SDKs) to help you set up those references.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论