Search code examples
swiftuiappkitnswindow

Have a fullSizeContentView NSWindow taking the size of a SwiftUI view


I want an NSWindow with fullSizeContentView to take the exact size of a SwiftUI view that has an intrinsic content size. I saw similar posts like this one but they were different in that it was fine to provide a fixed frame at a top level. I don’t want to do that, I want the window size to be exactly the size of the view. How can I do that?

This is a Playground snippet that runs in Xcode 14.1.

import AppKit
import SwiftUI

class MyWindow: NSWindow {
    override func setFrame(_ frameRect: NSRect, display flag: Bool) {
        print("\(Date().timeIntervalSince1970) setFrame called \(frameRect)")
        super.setFrame(frameRect, display: flag)
    }
}

let window = MyWindow()

window.styleMask = [
    .titled,
    .closable,
    .resizable,
    .fullSizeContentView
]

window.toolbar = nil

window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.isMovable = true
window.isMovableByWindowBackground = true
window.standardWindowButton(.closeButton)?.isHidden = false
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true

print("\(Date().timeIntervalSince1970) Before content \(window.frame)")
window.contentView = NSHostingView(rootView: ContentView())
print("\(Date().timeIntervalSince1970) After setting content \(window.frame)")

window.makeKeyAndOrderFront(nil)

print("\(Date().timeIntervalSince1970) After makeKeyAndOrderFront \(window.frame)")

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    print("\(Date().timeIntervalSince1970) After 1 second \(window.frame)")
}

struct ContentView: View {
    var body: some View {
        Text("Hello")
            .font(.system(size: 200))
            .background(.blue)
            .fixedSize()
            .ignoresSafeArea()
    }
}

The problem is that it leaves some space at the end. Why is this code behaving like that? Window capture

It prints this:

1674086812.362426 setFrame called (100.0, 100.0, 100.0, 100.0)
1674086812.363435 Before content (100.0, 100.0, 100.0, 100.0)
1674086812.373186 setFrame called (100.0, -63.0, 431.0, 263.0)
1674086812.3741732 After setting content (100.0, -63.0, 431.0, 263.0)
1674086812.374618 setFrame called (100.0, 85.0, 431.0, 263.0)
1674086812.375651 After makeKeyAndOrderFront (100.0, 85.0, 431.0, 263.0)
1674086812.4359 setFrame called (100.0, 57.0, 431.0, 291.0)
1674086813.41998 After 1 second (198.0, 99.0, 431.0, 291.0)

Why is SwiftUI setting the frame with a different size after showing it?


Solution

  • Ok, after spending a lot of time struggling with this, I think I found a workaround. The idea is to avoid completely ignoreSafeArea which seems buggy. In order to do that, I suppressed safe area behavior on the AppKit side by extending NSHostingView and overriding safe area related behavior. This is the code.

    import AppKit
    import SwiftUI
    
    class MyWindow: NSWindow {
        override func setFrame(_ frameRect: NSRect, display flag: Bool) {
            print("\(Date().timeIntervalSince1970) setFrame called \(frameRect)")
            super.setFrame(frameRect, display: flag)
        }
    }
    
    let window = MyWindow()
    
    window.styleMask = [
        .titled,
        .closable,
        .resizable,
        .fullSizeContentView
    ]
    
    window.titlebarAppearsTransparent = true
    window.titleVisibility = .hidden
    window.isMovable = true
    window.isMovableByWindowBackground = true
    window.standardWindowButton(.closeButton)?.isHidden = false
    window.standardWindowButton(.miniaturizeButton)?.isHidden = true
    window.standardWindowButton(.zoomButton)?.isHidden = true
    
    class NSHostingViewSuppressingSafeArea<T : View>: NSHostingView<T> {
        required init(rootView: T) {
            super.init(rootView: rootView)
    
            addLayoutGuide(layoutGuide)
            NSLayoutConstraint.activate([
                leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
                topAnchor.constraint(equalTo: layoutGuide.topAnchor),
                trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
                bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor)
            ])
        }
    
        private lazy var layoutGuide = NSLayoutGuide()
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override var safeAreaRect: NSRect {
            print ("super.safeAreaRect \(super.safeAreaRect)")
            return frame
        }
    
        override var safeAreaInsets: NSEdgeInsets {
            print ("super.safeAreaInsets \(super.safeAreaInsets)")
            return NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        }
    
        override var safeAreaLayoutGuide: NSLayoutGuide {
            print ("super.safeAreaLayoutGuide \(super.safeAreaLayoutGuide)")
            return layoutGuide
        }
    
        override var additionalSafeAreaInsets: NSEdgeInsets {
            get {
                print ("super.additionalSafeAreaInsets \(super.additionalSafeAreaInsets)")
                return NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
            }
    
            set {
                print("additionalSafeAreaInsets.set \(newValue)")
            }
        }
    }
    
    print("\(Date().timeIntervalSince1970) Before content \(window.frame)")
    window.contentView = NSHostingViewSuppressingSafeArea(rootView: ContentView())
    print("\(Date().timeIntervalSince1970) After setting content \(window.frame)")
    
    window.makeKeyAndOrderFront(nil)
    
    print("\(Date().timeIntervalSince1970) After makeKeyAndOrderFront \(window.frame)")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        print("\(Date().timeIntervalSince1970) After 1 second \(window.frame)")
    }
    
    struct ContentView: View {
        var body: some View {
            Text("Hello")
                .font(.system(size: 200))
                .fixedSize()
                .background(.blue)
        }
    }
    
    

    The result is: enter image description here

    And it prints this:

    1675110465.322774 setFrame called (100.0, 100.0, 100.0, 100.0)
    1675110465.332483 Before content (100.0, 100.0, 100.0, 100.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    1675110465.3494139 setFrame called (100.0, -35.0, 431.0, 235.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.safeAreaInsets NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    1675110465.3513222 After setting content (100.0, -35.0, 431.0, 235.0)
    1675110465.352477 setFrame called (100.0, 85.0, 431.0, 235.0)
    1675110465.3534908 After makeKeyAndOrderFront (100.0, 85.0, 431.0, 235.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    super.additionalSafeAreaInsets NSEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    additionalSafeAreaInsets.set NSEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0)
    1675110466.401649 After 1 second (636.0, 490.0, 431.0, 235.0)