为什么一个视图在变量更改时更新,而另一个不更新?

huangapple go评论62阅读模式
英文:

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实例。它包含了一个timertimerGone变量。

当你使用以下方式创建菜单栏视图时:

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提供了像BindingsObservableObjects(或者在最新的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.

huangapple
  • 本文由 发表于 2023年7月17日 20:30:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76704477.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定