Cannot modify NSTabViewItem - swift

I may be getting lost in a glass of water as I am not an experienced developer but I cannot seem to be able to implement a simple override to modify the size of an NSTabView item.
I have a Tab View Controller (Style = toolbar)
I have a Tabless Tab View
I have 3 Tab Items. For testing I have only subclassed one of them to the subclass below
I have created a new subclass of NSTabViewItem: MyTabViewItem and subclassed one of the 3 tab Items. The code is:
import Cocoa
class MyTabViewItem: NSTabViewItem {
override func drawLabel(_ shouldTruncateLabel: Bool, in labelRect: NSRect) {
var size = self.sizeOfLabel(false)
size.width = 180
print("Draw!!")
}
override func sizeOfLabel(_ computeMin: Bool) -> NSSize {
var size = super.sizeOfLabel(false)
size.width = 180
print("Draw!!")
return size
}
}
Everything works, except the subclassing. The Tabs appear, they do operate by switching the views and the program runs as it should. Except that it does not resize the Tab Item. The code in the subclass MyTabViewItem is never reached (it never prints Draw!! as it should.
I cannot understand what I am missing here. I have not read of any IB connection to make (and I cannot seem to be able to connect the Tab Items anyways). Please apologise if it isa trivial question but I have searched everywhere and not found anything to help me.
Thank you

You said:
I have a Tabless Tab View
This is your problem. An NSTabView only asks an NSTabViewItem to drawLabel if the NSTabView itself is responsible for drawing the tab bar, but you have a “Tabless” tab view. (“Tabless” is the default style when you drag an NSTabViewController into a storyboard.)
You also said:
I have a Tab View Controller (Style = toolbar)
So you don't even want the tab view to draw a tab bar; you want items in the window toolbar to select tabs (like in Xcode's preference window).
Your ability to customize the toolbar items created for your tabs is limited. You can subclass NSTabViewController and override toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:, like this:
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
if
let toolbarItem = toolbarItem,
let tabViewItem = tabViewItems.first(where: { ($0.identifier as? String) == itemIdentifier.rawValue })
{
toolbarItem.label = "\(tabViewItem.label) 😀"
}
return toolbarItem
}
But I found that making other changes didn't work well:
Setting toolbarItem.image didn't work well for me.
Setting toolbarItem.view made the item stop receiving clicks.
Note that the minSize and maxSize properties are only used if toolbarItem.view is set.
Your best bet is probably to manage the toolbar yourself, without trying to use NSTabViewController's support.

I have also subclassed the NSTabViewController as follows:
import Cocoa
class MyTabViewController: NSTabViewController {
#IBOutlet weak var TradingTabItem: MyTabViewItem!
override func viewDidLoad() {
super.viewDidLoad()
print("Loaded Tab View")
TradingTabItem.label = "New"
// Do view setup here.
}
}
What happens now is that the tab item in my subclass (the only one of the 3 I subclassed) does change its label string to New. However, even if I have added the item as an IBOutlet here, it still does not change seize (and the overridden sizeOfLabel function is not reached).

Related

How to disable lazy loading in NSTabViewController?

I am designing a SwiftUI wrapper for NSTabViewController with the toolbar style. I want it to be a drop-in replacement for TabView. TabView uses a modifier tabItem(_:) to specify the tab name and icon. So I designed a similar modifier for my own ToolbarTabView:
extension View {
func toolbarTabItem(_ label: LocalizedStringKey, nsImage: NSImage? = nil, tooltip: LocalizedStringKey? = nil) -> some View {
self.preference(key: ToolbarTabItemPreferenceKey.self, value: ToolbarTabItemPreference(label: label, nsImage: nsImage, tooltip: tooltip))
}
}
I wrap each View in a NSHostingController and create a NSTabViewItem. Then I use onPreferenceChange to set the NSTabViewItem's label and image property. Finally, I have a NSViewControllerRepresentable to pass my array of NSTabViewItem to a NSTabViewController. This all works well except for the following issue.
By design NSTabViewController will only load its first tab. This loads the first NSHostingController which lays out the first View. That calls onPreferenceChange and sets the label for the first tab. However, the remaining tabs are not loaded and therefore the label remains unset.
I know that I can re-design my APIs to pass in the labels and images explicitly and that works, but then how does Apple implement their TabView? They must have the same issue with the views being lazy loaded because the macOS implementation of TabView looks like NSTabViewController.
I think a workaround would be to force all the tabs to load, which is the title of this question, but I am open to other ideas as well.
Reference:
https://github.com/utmapp/UTM/blob/dev/Platform/macOS/ToolbarTabView.swift
https://github.com/utmapp/UTM/blob/dev/Platform/macOS/ToolbarTabViewController.swift
Here is the dumb workaround I came up with
public class UTMTabViewController: NSTabViewController {
public override func viewDidAppear() {
super.viewDidAppear()
for i in self.tabViewItems.indices {
self.selectedTabViewItemIndex = i
}
self.selectedTabViewItemIndex = 0
}
}
Basically I force load every tab once the view appears. I really hope there's a better answer than this but I'll leave it here just in case.

Menu bar for NSDocument doesn't appear

I transplanted a storyboard to another project that uses xibs (yes, the deployment target for the app is 10.9). This storyboard is connected to a NSDocument subclass (available only on 10.10+) which seems to work very good as expected... but the only problem is the main menu that only appear when the window's document goes behind other windows (such Finder ones) and then I put it back in front.
My question is: how can I ensure the main menu get connected to my document?
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
Swift.print("validateMenuItem")
return super.validateMenuItem(menuItem)
}
override func makeWindowControllers() {
let wc = DocumentWC.loadFromNib()
self.addWindowController(wc!)
}
Not sure what causing the problem (the project it's huge), and probably you can call this a patch instead of a fix:
override func viewDidAppear() {
super.viewDidAppear()
if !self.menufixed {
self.menufixed = true // just to call it once
let win = self.view.window
win?.resignMain()
win?.becomeMain()
win?.orderFrontRegardless()
win?.resignKey()
win?.becomeKey()
win?.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: true)
}
}
P.S. added to the view controller when the view and the window appear.

NSTabViewController add NSToolbarItems

I'd like to use the NSTabViewController for switching through 6 different Tabs with the toolbar style.
All tabs have in common that they show different aspects of a Customer entity.
Now I want to add aditional NSToolbarItems to the toolbar of the NSTabViewController? But I haven't found a way to access the toolbar.
I also would like to add Space between the ToolbarItems.
Is there a way to do so?
Or how can I add my ViewController from the Storyboard to a NSTabView without using NSTabViewController?
Regards
Oliver
In the meantime I've tried another approach that I thought was more promising but lead to another strange behaviour:
I've created a new NSViewController and put a NSTabView inside. In order to load my already existing ViewControllers I used this
override func viewDidLoad() {
super.viewDidLoad()
let customerController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("CustomerVCID")) as! CustomerViewController
let servicesController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("ServicesVCID")) as! ServicesController
customerController.customer = self.customer
servicesController.customer = self.customer
self.tabView.tabViewItems[0].view = customerController.view
self.tabView.tabViewItems[1].view = servicesController.view
}
That indeed worked, but now all my NSButtons that have actions will cause my application to crash.
There is only one toolbar per window. So your NSTabViewController shares it.
Select toolbar mode of NSTabViewController
Override NSWindowController and add your items
Example:
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.insertItem(withItemIdentifier: .print, at: 0)
}
You can always access your toolbar via following path view->window->toolbar
Your only issue is that there is one delegate per NSToolbar. Which means you have to create your custom NSToolbarItem inside NSTabViewController delegate.
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == .export {
return ExportToolbarItem.new()
} else {
return super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
}
}
Remember your are required to call super. This is because underlying method wants to create bindings to view controller.
In case you need actionable buttons in toolbar just add them without calling super.

Menubar with Storyboard - validateMenuItem not get called

I'm trying to setup a menubar Application using storyboard but my validateMenuItem method not get called.
I will try to explain what i did.
First i dragged a Menu Item in my Application Scene. Then one Object for my MenuController. Created a MenuController (MenuController.swift) and filled it with code. Back in my storyboard I set my Menu delegate to MenuController and MenuController Outlet to Menu. (I'm not totally sure whether i have set the delegates correctly.)
When i start the app, the menu icon appears and the first item title is set to test. But when i'm clicking the icon the validateMenuItem method not get called.
MenuController.swift
import Cocoa
class MenuController: NSObject {
var statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1)
#IBOutlet weak var statusMenu: NSMenu!
#IBOutlet weak var item1: NSMenuItem!
override func awakeFromNib() {
print("awakeFromNib")
self.item1.title = "Test"
let icon = NSImage(named: "menubarIcon")
statusItem.image = icon
statusItem.menu = statusMenu
}
override func validateMenuItem(menuItem: NSMenuItem) -> Bool {
print("validateMenuItem")
return true
}
}
Storyboard Menu Delegates
(source: picr.de)
Storyboard MenuController Delegates
(source: picr.de)
Has anybody an idea?
Greets from Austria!
The menu/UI validation mechanism does not query the menu's delegate but uses the item's target to determine the enabled state instead.
If the target is not explicitly set, it walks the responder chain.
To get basic validation, you have to make sure that the following things are setup:
"Auto Enables Items" is checked in Interface Builder (on by default)
You control-dragged the menu's action to the first responder
The menu's action is implemented in a class that is part of the responder chain (e.g. a view controller that manages a set of actions for your UI)
A basic implementation of validateUserInterfaceItem: could look like the following for an item with an action selector called test:
func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool
{
if anItem.action() == Selector("test:") {
print("validating item \(anItem)")
return true
}
return true
}

Change views inside NSSplitViewController

I'm trying to migrate my Objective-C and now improving Swift knowledge to an application for Mac OS X. Steep learning curve!
I'm trying to load a NSSplitViewController with different views in the "detail view" depending on buttons pressed on the "master view" if you will. Following tutorials and searching for hours has led me to nothing.
I currently have:
import Cocoa
class MainSplitView: NSSplitViewController, BlissWindowDelegate {
var masterViewController: vcMainMenu {
let masterItem = splitViewItems[0] as! NSSplitViewItem
return masterItem.viewController as! vcMainMenu
}
override func viewDidLoad() {
super.viewDidLoad()
masterViewController.delegate = self
}
func userDidSelectFunction(function: String) {
switch function {
case "app":
println("You have selected to load the appointment screen")
case "cust":
println("You have selected to load the customer screen")
case "login":
println("I think I am here and you've clicked login?")
let detailItem = splitViewItems[1] as! NSSplitViewItem
// Trying to load the views here ... but no idea how to
case "admin":
println("You've clicked admin")
default:
println("Nothing here ...")
}
}
}
I'm using BlissWindowDelegate to tell me which button was pressed. I am then trying to load into the splitViewItem[1] various views from a Storyboard. But having no luck. Can anyone point me in the right direction please? Even for a decent reference? Nothing on Google is seeming to help.
Thanks.
Since it sounds like you have a specific set of detail panes that can be shown, using an NSTabViewController is probably best way to accomplish this.
Basically, your NSSplitViewController has two children: the master view controller, and a NSTabViewController. And the tab view controller has its own children for each of the detail panes. Since tab view controller shouldn't present its own tab selection UI (the master pane is doing that), you would set the tabStyle to be .Unspecified. The storyboard would look something like this:
Your MainSplitViewController would also have a reference to the tab view controller, detailController. Then on userDidSelectFunction(), you would set the detailController's selectedTabViewItemIndex to be that of the corresponding detail pane. NSTabViewController will take care of the view transition, including animating between the panes if setup to do so.