7

This is how SwiftUI can be inserted into NSToolbar using an accessory view controller:

import SwiftUI
import PlaygroundSupport

var hostingView = NSHostingView(rootView:
  ZStack {
    Color.clear
    HStack {
      Text("Hey")
      Text("SwiftUI")
    }
  }
  .padding()
  .edgesIgnoringSafeArea(.all)
)
hostingView.frame.size = hostingView.fittingSize

let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = hostingView
titlebarAccessory.layoutAttribute = .trailing

let mask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
let window = NSWindow(
  contentRect: .init(x: 0, y: 0, width: 480, height: 300),
  styleMask: mask, backing: .buffered, defer: false)
window.center()
window.contentView = NSHostingView(rootView: Color(.windowBackgroundColor))
window.toolbar = .init()
window.titleVisibility = .hidden
window.addTitlebarAccessoryViewController(titlebarAccessory)

PlaygroundPage.current.liveView = window.contentView?.superview

The code above does work:

If we insert a button however:

HStack {
  Text("Hey")
  Button(action: {}) {
    Text("SwiftUI")
  }
}

It would not work as expected:

Any suggestions?

P. S. This is a working solution:

HStack {
  Text("Hey")
    .offset(x: 0, y: -1)
  Button(action: {}) {
    Text("SwiftUI")
      .offset(x: 0, y: -7)
  }
}
.font(.caption)

Vadim
  • 9,383
  • 7
  • 36
  • 58

4 Answers4

2

The way I eventually handled it (making it work also when the user makes the window full screen AND regardless of the height* of the content) is below. Note that I'm using Toolbar view in an NSHostingView within NSTitlebarAccessoryViewController of a macOS app built with most recent Xcode version (obviously, on macOS Catalina).

Important things: *you still need to setup a fixed height for the frame (but at least you won't rely to the "variable" -7) and offset to top by half of the actual height ONLY when there is safe area top inset (in full screen mode, apparently, it isn't) and therefore GeometryReader is a must:

struct Toolbar : View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text("1st row")
                Text("2nd row")
            }
            .offset(y: geometry.safeAreaInsets.top > 0 ? -geometry.size.height / 2 : 0)
        }
        .frame(height: 42)
    }
}

and where window is created:

let toolbar = NSHostingView(rootView: Toolbar())
toolbar.frame.size = toolbar.fittingSize
let toolbarController = NSTitlebarAccessoryViewController()
toolbarController.view = toolbar
window.addTitlebarAccessoryViewController(toolbarController)
Sorin Dolha
  • 159
  • 1
  • 8
1

Posting my comment as an answer per request. Not necessarily a reliable solution but as a workaround you can use

Text("SwiftUI")
  .padding(EdgeInsets(top: -7, leading: 0, bottom: 0, trailing: 0))

or .offset on the Text in the button. No guarantees on how long that will last as a solution.

Lucas Derraugh
  • 6,929
  • 3
  • 27
  • 43
  • This may still not work when/if the user makes the window full screen, unfortunately. Also, the -7 value is "variable" and depends on the height of the actual view. – Sorin Dolha Mar 22 '20 at 19:42
  • Like I said, more of a workaround rather than a solution. I was requested to post my comment as an answer. – Lucas Derraugh Apr 02 '20 at 22:17
0

Toolbar item has more vertical space. To fix that use Spacer view it will push the button to the top:

VStack {
  Button(action: {}) {
    Text("Text")
  }.frame(height: 22)

  Spacer()
}
velocityzen
  • 86
  • 10
  • 1
    This works, except if there is an actual toolbar button below the accessory view, it cannot be accessed by the accessibility APIs (as you can see by running the Accessibility Inspector on it), which prevents assistive devices from being able to access those buttons. – marcprux Jun 15 '20 at 14:27
0

It seems that applying PlainButtonStyle to the button helps. Default style is doing some weird magic that is breaking the layout in NSToolbar.

msmialko
  • 1,439
  • 2
  • 20
  • 35