NSWindowController NSHostingView基于的内容视图:帧大小只有延迟更新

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

NSWindowController NSHostingView based contentView: Frame size only updated with delay

问题

我有一个简单的设置,代表了我在我的MacOS应用程序中所做的事情:

// AppDelegate.swift
import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        TestWindow()
    }

    func applicationWillTerminate(_ aNotification: Notification) {}

    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
        return true
    }
}
// BareView.swift
import SwiftUI

struct BareView: View {
    @ObservedObject var testState: TestState
    
    var body: some View {
        Color.white.frame(width: testState.width, height: testState.height)
    }
}
// TestWindow.swift
import Foundation
import AppKit
import SwiftUI

class TestState: ObservableObject {
    @Published var width: CGFloat = 100
    @Published var height: CGFloat = 100
}

class TestWindow: NSWindowController {
    let testState = TestState()

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    init() {
        super.init(window: NSWindow(
            contentRect: .zero,
            styleMask: [ .borderless ],
            backing: .buffered,
            defer: false))
        
        self.window!.contentView = NSHostingView(rootView: BareView(testState: testState))
        
        self.window!.setFrameOrigin(.init(x: NSScreen.main!.frame.width / 2, y: NSScreen.main!.frame.height / 2))
        self.window!.makeKeyAndOrderFront(nil)
        
        NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { event in
            self.testState.width += 10
            self.testState.height += 10
            self.window!.makeKeyAndOrderFront(nil)
            // behaviour only as expected if this line is added:
            // self.window!.setContentSize(.init(width: self.testState.width, height: self.testState.height))
            print("contentSize: \(self.testState.width)")
            print("viewFrame: \(self.window!.contentView!.frame)")
            return nil
        }
    }
}
The size of the SwiftUI View inside the NSHostingView is updated based on an EventHandler. **The SwiftUI View is actually resized and increases its size, but the contentViews frame size is not directly notified about this change**. This happens one iteration later (after the window has moved?).
So the console prints the following:

contentSize: 110.0
viewFrame: (0.0, 0.0, 100.0, 100.0)
contentSize: 120.0
viewFrame: (0.0, 0.0, 110.0, 110.0)


Once I manually update the contentViews size with setContentSize(_:) function the console output behaves as expected:

contentSize: 110.0
viewFrame: (0.0, 0.0, 110.0, 110.0)
contentSize: 120.0
viewFrame: (0.0, 0.0, 120.0, 120.0)


So it seems like Swift is recalculating the window only after the code has returned. Since the SwiftUI view has increased in size, but the Window does not know about this, the origin is not valid. This creates a bug in my app.
I can solve this problem by calling setContentSize(_:), but the information about its actual size is contained in a leave view all the way down the tree. I would need to use an ObservableObject to communicate the calculated size back to the NSWindowController.

Can someone explain to me if this is expected behaviour and if so, what happens here? There must be some kind of "window redraw cycle" that I don't know about.

<details>
<summary>英文:</summary>

I have a simple setup here representative of what I do in my MacOS app:

```Swift
// AppDelegate.swift
import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        TestWindow()
    }

    func applicationWillTerminate(_ aNotification: Notification) {}

    func applicationSupportsSecureRestorableState(_ app: NSApplication) -&gt; Bool {
        return true
    }
}
// BareView.swift
import SwiftUI

struct BareView: View {
    @ObservedObject var testState: TestState
    
    var body: some View {
        Color.white.frame(width: testState.width, height: testState.height)
    }
}
// TestWindow.swift
import Foundation
import AppKit
import SwiftUI

class TestState: ObservableObject {
    @Published var width: CGFloat = 100
    @Published var height: CGFloat = 100
}

class TestWindow: NSWindowController {
    let testState = TestState()

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }

    init() {
        super.init(window: NSWindow(
            contentRect: .zero,
            styleMask: [ .borderless ],
            backing: .buffered,
            defer: false))
        
        self.window!.contentView = NSHostingView(rootView: BareView(testState: testState))
        
        self.window!.setFrameOrigin(.init(x: NSScreen.main!.frame.width / 2, y: NSScreen.main!.frame.height / 2))
        self.window!.makeKeyAndOrderFront(nil)
        
        NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { event in
            self.testState.width += 10
            self.testState.height += 10
            self.window!.makeKeyAndOrderFront(nil)
            // behaviour only as expected if this line is added:
            // self.window!.setContentSize(.init(width: self.testState.width, height: self.testState.height))
            print(&quot;contentSize: \(self.testState.width)&quot;)
            print(&quot;viewFrame: \(self.window!.contentView!.frame)&quot;)
            return nil
        }
    }
}

The size of the SwiftUI View inside the NSHostingView is updated based on an EventHandler. The SwiftUI View is actually resized and increases its size, but the contentViews frame size is not directly notified about this change. This happens one iteration later (after the window has moved?).
So the console prints the following:

contentSize: 110.0
viewFrame: (0.0, 0.0, 100.0, 100.0)
contentSize: 120.0
viewFrame: (0.0, 0.0, 110.0, 110.0)

Once I manually update the contentViews size with setContentSize(_:) function the console output behaves as expected:

contentSize: 110.0
viewFrame: (0.0, 0.0, 110.0, 110.0)
contentSize: 120.0
viewFrame: (0.0, 0.0, 120.0, 120.0)

So it seems like Swift is recalculating the window only after the code has returned. Since the SwiftUI view has increased in size, but the Window does not know about this, the origin is not valid. This creates a bug in my app.
I can solve this problem by calling setContentSize(_:), but the information about its actual size is contained in a leave view all the way down the tree. I would need to use an ObservableObject to communicate the calculated size back to the NSWindowController.

Can someone explain to me if this is expected behaviour and if so, what happens here? There must be some kind of "window redraw cycle" that I don't know about.

答案1

得分: 2

如果您希望在窗口调整大小时立即捕获它,您可以尝试:

.onReceive(NotificationCenter.default.publisher(for: NSWindow.didResizeNotification)) { newValue in
    if let size = window?.frame.size {
        windowWidth = size.width
        windowHeight = size.height
    }
}
英文:

If you're looking to capture window resizing as it happens, you could try:

.onReceive(NotificationCenter.default.publisher(for: NSWindow.didResizeNotification)) { newValue in
    if let size = window?.frame.size {
        windowWidth = size.width
        windowHeight = size.height
    }
}

huangapple
  • 本文由 发表于 2023年2月16日 16:35:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75469627.html
匿名

发表评论

匿名网友

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

确定