4

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?

narduk
  • 964
  • 1
  • 9
  • 19

2 Answers2

2

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)

narduk
  • 964
  • 1
  • 9
  • 19
1

The problem is your ordering of modifiers. You've put .background before .ignoresSafeArea(), so the background takes the safe area into account, If you re-order as follows, it works as required:

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

enter image description here


Second attempt

Adding some borders to the views after modifiers, it seems that the text is overlapping the title bar, and is inset that amount from the bottom.

struct ContentView: View {
    
    var body: some View {
        Text("Hello")
            .font(.system(size: 200))
            .fixedSize()
            .border(.pink, width: 2)
            .ignoresSafeArea()
            .border(.yellow, width: 1)
            .background(.blue.opacity(0.5))
    }
}

enter image description here

Applying a small offset to the View after .ignoresSafeArea()

.ignoresSafeArea()
.offset(y: 0.1)
.background(.blue)     

gives:

enter image description here

If you do the same after the .background modifier, the title bar is shown:

.ignoresSafeArea()
.offset(y: 0.1)
.background(.blue) 

enter image description here

I don't know why an offset seems to fix the problem.

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • Thanks for the suggestion! The problem I see with this solution is that it is actually taking more space than the actual content. In this example, the content wants to have a height of 235. To measure that, I put the content in a very big frame and measured the background. But here, the background ends up being larger, specifically, 291. As I stated in my question, I want the window to take the size of the content, not more, not less. – narduk Jan 19 '23 at 13:33
  • I'v done some more research into the problem, with a potential workaround. – Ashley Mills Jan 19 '23 at 17:45
  • Thanks for doing the research! Unfortunately, the workaround doesn't have the right height yet. It moved the text down a bit but the size of the window is still larger than the ideal size. – narduk Jan 19 '23 at 21:28