Skip to content

Instantly share code, notes, and snippets.

@florianpircher
Last active August 30, 2022 18:59
Show Gist options
  • Save florianpircher/be8507c20a888614296ba8123335fc4b to your computer and use it in GitHub Desktop.
Save florianpircher/be8507c20a888614296ba8123335fc4b to your computer and use it in GitHub Desktop.
A drop-in replacement for a plain NSTabViewController that animates size changes of the preferences window when selecting a different tab.

In a XIB files, the tab view controller should be configured to a Style of Toolbar and the tab view should be configured to a Style of Tabless.

The toolbar item labels are derived from the tab view item labels. The window title matches the currently selected tab view item but a custom title can be provided by setting a title on the view controller of the tab view item. The image of the tab vie witem is used for the toolbar item image.

//
// PreferencesTabViewController.swift
// Light Table
//
// Copyright 2022 Florian Pircher
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Cocoa
final class PreferencesTabViewController: NSTabViewController {
private struct ToolbarItemSpecs {
let identifier: NSToolbarItem.Identifier
let index: Int
let label: String
let title: String
let image: NSImage?
}
private var toolbarItemsSpecs: [ToolbarItemSpecs] = []
private var bottomConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
guard let window = tabView.window else {
return
}
window.toolbarStyle = .preference
toolbarItemsSpecs = tabView.tabViewItems.enumerated().map { index, tabItem in
ToolbarItemSpecs(
identifier: NSToolbarItem.Identifier("preference:\(index)"),
index: index,
label: tabItem.label,
title: tabItem.viewController?.title ?? tabItem.label,
image: tabItem.image)
}
let toolbar = NSToolbar()
toolbar.delegate = self
window.toolbar = toolbar
if let firstSpec = toolbarItemsSpecs.first {
tabView.selectTabViewItem(at: firstSpec.index)
window.title = firstSpec.title
toolbar.selectedItemIdentifier = firstSpec.identifier
}
if let superview = tabView.superview {
tabView.removeFromSuperview()
superview.addSubview(tabView)
NSLayoutConstraint.activate([
superview.topAnchor.constraint(equalTo: tabView.topAnchor),
superview.leadingAnchor.constraint(equalTo: tabView.leadingAnchor),
superview.trailingAnchor.constraint(equalTo: tabView.trailingAnchor),
])
bottomConstraint = superview.bottomAnchor.constraint(equalTo: tabView.bottomAnchor)
bottomConstraint.isActive = true
}
}
override func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarItemsSpecs.map { $0.identifier }
}
override func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarAllowedItemIdentifiers(toolbar)
}
override func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarAllowedItemIdentifiers(toolbar)
}
override func toolbarImmovableItemIdentifiers(_ toolbar: NSToolbar) -> Set<NSToolbarItem.Identifier> {
Set(toolbarAllowedItemIdentifiers(toolbar))
}
override func toolbar(_ toolbar: NSToolbar, itemIdentifier: NSToolbarItem.Identifier, canBeInsertedAt index: Int) -> Bool {
true
}
override func toolbarWillAddItem(_ notification: Notification) {}
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
guard let spec = toolbarItemsSpecs.first(where: { $0.identifier == itemIdentifier }) else {
return nil
}
let item = NSToolbarItem(itemIdentifier: spec.identifier)
item.label = spec.label
item.image = spec.image
item.target = self
item.action = #selector(Self.selectToolbarItem(_:))
return item
}
@objc func selectToolbarItem(_ sender: NSToolbarItem) {
guard let spec = toolbarItemsSpecs.first(where: { $0.identifier == sender.itemIdentifier }),
let window = tabView.window,
tabView.tabViewItems.indices.contains(spec.index)
else {
return
}
let newTabViewItem = tabView.tabViewItems[spec.index]
guard let newContentView = newTabViewItem.view else {
return
}
let contentFrame = tabView.frame
let newContentSize = newContentView.fittingSize
let windowFrame = window.frame
let windowHeightOffset = windowFrame.height - contentFrame.height
let newWindowSize = CGSize(
width: max(windowFrame.width, newContentSize.width),
height: newContentSize.height + windowHeightOffset)
let windowOrigin = windowFrame.origin
let newWindowOrigin = CGPoint(
x: windowOrigin.x + ((newWindowSize.width - windowFrame.width) / -2),
y: windowOrigin.y + (windowFrame.height - newWindowSize.height))
let newWindowFrame = CGRect(origin: newWindowOrigin, size: newWindowSize)
tabView.isHidden = true
bottomConstraint.isActive = false
window.setFrame(newWindowFrame, display: true, animate: true)
tabView.selectTabViewItem(at: spec.index)
bottomConstraint.isActive = true
tabView.isHidden = false
window.title = spec.title
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment