UITextView in collectionview cell on mac spikes cpu - macos

I have a simple UIKit application that has a UITextView in a UICollectionViewCell. The app is designed for iOS/iPadOS and works just fine on those platforms. However, when run on Mac (Designed for iPad) as soon as I start scrolling the collectionview, the cpu usage spikes to ~85% and stays there indefinitely. The only way to lower the cpu is to click outside of the application window, but once it comes to the foreground again, the cpu usage jumps right back up. I've tried running on Mac in Catalyst mode too, but the same problem occurs with slightly less cpu usage (~45%).
Additionally the debugger constantly spits out [API] cannot add handler to 3 from 3 - dropping while scrolling.
Does anyone have an explanation or solutions for this?
I’m using Xcode Version 14.1 (14B47b) on macOS Ventura 13.0 (22A380).
class ViewController: UIViewController {
var dataSource: UICollectionViewDiffableDataSource<Section, String>! = nil
var collectionView: UICollectionView! = nil
var items = Array(0...100).map{"Item \($0)"}
enum Section: String {
case main
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "List"
configureCollectionView()
configureDataSource()
applyInitialSnapshot()
}
private func createLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])
return NSCollectionLayoutSection(group: group)
}
}
private func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<TestCell, String> { (cell, indexPath, item) in
cell.configure(title: item, row: indexPath.item)
}
dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: collectionView) {
(collectionView, indexPath, identifier) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
class TestCell: UICollectionViewCell {
private let annotationsTextView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
addViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(title: String, row: Int) {
annotationsTextView.attributedText = .init(string: "Row: \(row) Item: \(title)", attributes: [.font: UIFont.preferredFont(forTextStyle: .title1)])
}
private func addViews() {
annotationsTextView.isScrollEnabled = false
annotationsTextView.isEditable = false
annotationsTextView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(annotationsTextView)
NSLayoutConstraint.activate([
annotationsTextView.topAnchor.constraint(equalTo: contentView.topAnchor),
annotationsTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
annotationsTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
annotationsTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}

I am still on 13.0.1 and had the same problem (tons of [API] cannot add handler to 3 from 3 - dropping). I narrowed it down to a call to UITableView.scrollToNearestSelectedRow with animated = true.
Setting animated to false stops the logging. I guess I’ll have to check out 13.1

I'm experiencing this exact behavior in Ventura whenever there is a UITextView anywhere in the view hierarchy with either isSelectable or isEditable set to true, but only while the UITextView is NOT first responder. This occurs whether the UITextView is visible or hidden.
The excess CPU usage can be prevented by any of the following:
Remove the UITextView from the view hierarchy.
Set isSelectable AND isEditable to false.
Make the UITextView first responder.
I'm still investigating and will update here if I find more. We should all probably report the issue to Apple as I imagine they will need to fix this at the system level.
2022-12-01: Update
This issue appears to have been resolved as of macOS 13.1 beta 4 (22C5059b). Hallelujah!

Related

Disabling macOS focus ring in SwiftUI

Is it possible to disable the focus ring around a TextField in swiftUI for Mac?
I had that question as well, and after a couple hours of fiddling around, it seems like the answer is no. However, it is possible to wrap an NSTextField and get rid of the focus ring.
The following code has been tested in the latest release.
struct CustomTextField: NSViewRepresentable {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField(string: text)
textField.delegate = context.coordinator
textField.isBordered = false
textField.backgroundColor = nil
textField.focusRingType = .none
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
}
func makeCoordinator() -> Coordinator {
Coordinator { self.text = $0 }
}
final class Coordinator: NSObject, NSTextFieldDelegate {
var setter: (String) -> Void
init(_ setter: #escaping (String) -> Void) {
self.setter = setter
}
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
setter(textField.stringValue)
}
}
}
}
As stated in an answer by Asperi to a similar question here, it's not possible (yet) to turn off the focus ring for a specific field using SwiftUI; however, the following workaround will disable the focus ring for all NSTextField instances in the app:
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}
Not ideal, but it does provide one option that doesn't require stepping too far outside of SwiftUI.

Implementing NSSegmentedControl in NSToolbar to control NSTabViewController

In my macOS application, I'm trying to replicate the Photos.app implementation of NSSegmentedControl in NSToolbar to control an NSTabViewController. For reference, here's what that looks like:
So, my approach was as follows:
Hide the default NSTabView header using the Interface Builder
Programmatically add an NSToolbar
Insert NSSegmentedControl as an NSToolbarItem.
Use a #selector to listen for changes to NSSegmentedControl.
Here's the current implementation:
class WindowController: NSWindowController, NSToolbarDelegate {
// MARK: - Identifiers
let mainToolbarIdentifier = NSToolbar.Identifier("MAIN_TOOLBAR")
let segmentedControlIdentifier = NSToolbarItem.Identifier("MAIN_TABBAR")
// MARK: - Properties
var tabBar: NSSegmentedControl? = NSSegmentedControl(labels: ["One", "Two"], trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(didSwitchTabs))
var toolbar: NSToolbar?
var tabBarController: NSTabViewController?
// MARK: - Life Cycle
override func windowDidLoad() {
super.windowDidLoad()
self.toolbar = NSToolbar(identifier: mainToolbarIdentifier)
self.toolbar?.allowsUserCustomization = false
self.toolbar?.delegate = self
self.tabBar?.setSelected(true, forSegment: 0)
self.tabBarController = self.window?.contentViewController as? NSTabViewController
self.tabBarController?.selectedTabViewItemIndex = 0
self.window?.toolbar = self.toolbar
}
// MARK: - NSToolbarDelegate
public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
var toolbarItem: NSToolbarItem
switch itemIdentifier {
case segmentedControlIdentifier:
toolbarItem = NSToolbarItem(itemIdentifier: segmentedControlIdentifier)
toolbarItem.view = self.tabBar
case NSToolbarItem.Identifier.flexibleSpace:
toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
default:
fatalError()
}
return toolbarItem
}
public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
// MARK: - Selectors
#objc func didSwitchTabs(sender: Any) {
let segmentedControl = sender as! NSSegmentedControl
if (segmentedControl.selectedSegment == 0) {
self.tabBarController?.selectedTabViewItemIndex = 0
} else if (segmentedControl.selectedSegment == 1) {
self.tabBarController?.selectedTabViewItemIndex = 1
}
}
}
And, here it is in action:
Now, I am new to macOS development and this feels like it's a very complicated and convoluted way of solving this problem. Is there an easier way I could achieve the same thing ? Perhaps somehow in Interface Builder ? What could be done to improve here ? What have I done wrong ?
Thanks for your time.
For anybody implementing NSSegmentedControl on the toolbar and it did not trigger IBAction, I got the same problem and pay my half-day to resolve this.
The problem is I connect my segmented with the NSWindowController class.
To fix this, create a subclass of NSWindow, set that class to base class of window on your storyboard, then create #IBOutlet #IBAction link to NSWindow. Remember, link it with NSWindowController will not work.

Blank/Black view when attempting to set delegate/data source

I am attempting to load a view from a .xib file as per this question:
Assign xib to the UIView in Swift
My setup is as follows:
StripView.xib — the .xib file from which I am loading the view, contains a single UIView
StripView.swift — the 'owner' of the UIView in StripView.xib, contains references/IBOutlets for each component in StripView.xib
StripViewController.swift — The view controller, which loads an instance of StripView and handles most of the logic/data source/delegate stuff.
This all worked fine in Xcode 6 Beta 4, however, upgrading to Xcode 6 Beta 5 (the latest) breaks. On app launch, I simply get a blank, black screen. All other parts of the app seem to work fine. This bug only occurs when I attempt to set the delegate or data source of a UIPickerView (owned by StripView) to the StripViewController.
Code snippet:
class StripViewController: SWRevealViewController, RSColorPickerViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate, BLEDelegate, UIActionSheetDelegate, UITextFieldDelegate {
var stripView: StripView!
var colorPicker: RSColorPickerView!
var connectBtn:UIButton!
var connectionSpinner:UIActivityIndicatorView!
var tapRecognizer:UITapGestureRecognizer!
init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// use UIView extension as per https://stackoverflow.com/questions/24370061/assign-xib-to-the-uiview-in-swift
stripView = UIView.loadFromNibNamed("StripView") as StripView
self.view.addSubview(stripView)
// nav bar setup //
self.title = "Strip \(self.tabBarController.selectedIndex+1)"
let leftNav:UIView = UIView(frame: CGRectMake(0,0,100,40))
self.connectBtn = UIButton(frame: CGRectMake(0,0,95,40))
connectBtn.frame = CGRectMake(0,0,connectBtn.bounds.size.width,connectBtn.bounds.size.height)
connectBtn.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Left
connectBtn.setTitle("Connect", forState: UIControlState.Normal)
connectBtn.setTitleColor(UIColor(red: 0.0, green: 122.0/255.0, blue: 1.0, alpha: 1.0), forState: UIControlState.Normal)
connectBtn.addTarget(self, action: "connectButtonPressed:", forControlEvents: UIControlEvents.TouchDown)
leftNav.addSubview(connectBtn)
self.connectionSpinner = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.Gray)
connectionSpinner.frame = CGRectMake(connectBtn.frame.minX + connectBtn.bounds.size.width, connectBtn.frame.minY + 10, connectionSpinner.bounds.size.width, connectionSpinner.bounds.size.height)
connectionSpinner.hidesWhenStopped = true
leftNav.addSubview(connectionSpinner)
let leftNavItem:UIBarButtonItem = UIBarButtonItem(customView: leftNav)
self.navigationItem.leftBarButtonItem = leftNavItem
// set up color picker
colorPicker = RSColorPickerView(frame: CGRectMake(20, 78, 160, 160))
self.view.addSubview(colorPicker)
colorPicker.delegate = self
// set up mode picker
// THESE APPEAR TO BE THE OFFENDING LINES —
// COMMENTING THEM OUT DISPLAYS THE VIEW AS EXPECTED
self.stripView.modePicker.dataSource = self;
self.stripView.modePicker.delegate = self;
// set up notifications
NSNotificationCenter.defaultCenter().addObserver(self, selector: "colorFieldDidChange:", name: "ColorFieldDidChangeNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
// set up UI and populate fields
self.stripView.setAnimSpeed()
// set up BLE
BLEManager.instance.ble.delegate = self
}
[...]
Once more, the offending lines appear to be:
self.stripView.modePicker.dataSource = self;
self.stripView.modePicker.delegate = self;
If I comment them out, all else displays as normal. I even attempted moving the delegate/data source into StripView.swift (not StripViewController), but I see the same result.
Here's the part of StripViewController that implements UIPickerViewDataSource and UIPickerViewDelegate methods:
func numberOfComponentsInPickerView(pickerView: UIPickerView!) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView!, numberOfRowsInComponent component: Int) -> Int {
return DataModel.instance.ModeNames.count
}
func pickerView(pickerView: UIPickerView!, titleForRow row: Int, forComponent component: Int) -> String! {
return DataModel.instance.ModeNames[row]
}
func pickerView(pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int) {
// send mode to arduino
println("--- sending mode select command ---")
var buf:[UInt8] = [0x00, 0x00, 0x00, 0x00]
buf[0] = Modes.instance.SET_MODE
buf[1] = UInt8(DataModel.instance.ModeOpcodes[DataModel.instance.ModeNames[row]]!)
let data:NSData = NSData(bytes: buf, length: 4)
BLEManager.instance.ble.write(data)
}
Did something change in the way the Swift language handles delegates/data sources, or view loading from a XIB?
EDIT:
I've narrowed down the problem to the DataModel singleton I'm using as a datasource. That code (DataModel.swift) looks like so, implementing a basic singleton pattern as per https://github.com/hpique/SwiftSingleton :
class DataModel {
// setup singleton
class var instance: DataModel {
struct Static {
static let instance : DataModel = DataModel()
}
return Static.instance
}
// modes
// eventually may replace this with a more flexible data structure
// for now, we just need names
let ModeNames: Array<String> = ["Light All", "Rainbow"]
let ModeOpcodes: Dictionary<String, UInt8> = [DataModel.instance.ModeNames[0]: Modes.instance.LIGHT_ALL, DataModel.instance.ModeNames[1]: Modes.instance.RAINBOW];
}
Replacing instances of this with a variable defined in StripViewController results in things working fine. Something about it being a singleton seems to be throwing things off. Thoughts? Perhaps the singleton is not being instantiated correctly?
One thing easily overlooked is connecting the data source and delegate to the view controller itself. In addition to ctrl dragging an outlet, you need to ctrl drag the data source and delegate from the connections inspector to the small view controller icon on top of the view in the storyboard.

Computed property causes application crash

I'm porting the Apple class ImageAndTextCell to swift and use it inside a NSTableView, it was a trivial task but when I click on table row the application crashes.
I suppose the crash is due to a deallocation problem but I don't understand how to fix it.
The app crashes with the error: Thread 1: EXC_BAD_ADDRESS (code=EXC_i386_GPFLT), no other message is present so debugging the error is hard
I've isolated the code to easily reproduce the crash.
My ImageAndTextCell is shown below, it declares an icon property and access to it using the computed property image
import Foundation
import Cocoa
class ImageAndTextCell : NSTextFieldCell {
private var icon : NSImage?
override var image : NSImage! {
get {
return icon
}
set {
if newValue !== icon {
icon = newValue
if let im = newValue {
im.size = NSMakeSize(CGFloat(16.0), CGFloat(16.0))
}
}
}
}
deinit { println(" is being deinitialized") }
override func copyWithZone(zone: NSZone) -> AnyObject! {
var cell = super.copyWithZone(zone) as ImageAndTextCell
cell.icon = icon
return cell
}
}
The Application delegate is
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var window: NSWindow!
var items = [String]()
override func awakeFromNib() {
items.append("/Users/dave/trash/test.txt")
}
func numberOfRowsInTableView(tableView: NSTableView!) -> Int {
return self.items.count
}
func tableView(tableView: NSTableView!, objectValueForTableColumn tableColumn: NSTableColumn!, row: Int) -> AnyObject! {
return self.items[row]
}
func tableView(tableView: NSTableView, willDisplayCell cell:AnyObject!, forTableColumn tableColumn:NSTableColumn!, row:NSInteger) {
let item = self.items[row]
var fieldCell = cell as NSTextFieldCell
fieldCell.drawsBackground = false
// commenting the line below the app works fine but obviously the cell doesn't show the image
fieldCell.image = NSWorkspace.sharedWorkspace().iconForFile(item)
}
}
copyWithZone and deinit are called every time I click on the row so I suppose some pointer is released before the time
The table cell 'custom class' is set inside Interface Builder to ImageAndTextCell
Any idea?

How to draw your own NSTabView tabs?

I want to draw my own tabs for NSTabViewItems. My Tabs should look different and start in the top left corner and not centered.
How can I do this?
it is possible to set the NSTabView's style to Tabless and then control it with a NSSegmentedControl that subclasses NSSegmentedCell to override style and behavior. For an idea how to do this, check out this project that emulates Xcode 4 style tabs: https://github.com/aaroncrespo/WILLTabView/.
One of possible ways to draw tabs - is to use NSCollectionView. Here is Swift 4 example:
Class TabViewStackController contains TabViewController preconfigured with style .unspecified and custom TabBarView.
class TabViewStackController: ViewController {
private lazy var tabBarView = TabBarView().autolayoutView()
private lazy var containerView = View().autolayoutView()
private lazy var tabViewController = TabViewController()
private let tabs: [String] = (0 ..< 14).map { "TabItem # \($0)" }
override func setupUI() {
view.addSubviews(tabBarView, containerView)
embedChildViewController(tabViewController, container: containerView)
}
override func setupLayout() {
LayoutConstraint.withFormat("|-[*]-|", forEveryViewIn: containerView, tabBarView).activate()
LayoutConstraint.withFormat("V:|-[*]-[*]-|", tabBarView, containerView).activate()
}
override func setupHandlers() {
tabBarView.eventHandler = { [weak self] in
switch $0 {
case .select(let item):
self?.tabViewController.process(item: item)
}
}
}
override func setupDefaults() {
tabBarView.tabs = tabs
if let item = tabs.first {
tabBarView.select(item: item)
tabViewController.process(item: item)
}
}
}
Class TabBarView contains CollectionView which represents tabs.
class TabBarView: View {
public enum Event {
case select(String)
}
public var eventHandler: ((Event) -> Void)?
private let cellID = NSUserInterfaceItemIdentifier(rawValue: "cid.tabView")
public var tabs: [String] = [] {
didSet {
collectionView.reloadData()
}
}
private lazy var collectionView = TabBarCollectionView()
private let tabBarHeight: CGFloat = 28
private (set) lazy var scrollView = TabBarScrollView(collectionView: collectionView).autolayoutView()
override var intrinsicContentSize: NSSize {
let size = CGSize(width: NSView.noIntrinsicMetric, height: tabBarHeight)
return size
}
override func setupHandlers() {
collectionView.delegate = self
}
override func setupDataSource() {
collectionView.dataSource = self
collectionView.register(TabBarTabViewItem.self, forItemWithIdentifier: cellID)
}
override func setupUI() {
addSubviews(scrollView)
wantsLayer = true
let gridLayout = NSCollectionViewGridLayout()
gridLayout.maximumNumberOfRows = 1
gridLayout.minimumItemSize = CGSize(width: 115, height: tabBarHeight)
gridLayout.maximumItemSize = gridLayout.minimumItemSize
collectionView.collectionViewLayout = gridLayout
}
override func setupLayout() {
LayoutConstraint.withFormat("|[*]|", scrollView).activate()
LayoutConstraint.withFormat("V:|[*]|", scrollView).activate()
}
}
extension TabBarView {
func select(item: String) {
if let index = tabs.index(of: item) {
let ip = IndexPath(item: index, section: 0)
if collectionView.item(at: ip) != nil {
collectionView.selectItems(at: [ip], scrollPosition: [])
}
}
}
}
extension TabBarView: NSCollectionViewDataSource {
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return tabs.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let tabItem = tabs[indexPath.item]
let cell = collectionView.makeItem(withIdentifier: cellID, for: indexPath)
if let cell = cell as? TabBarTabViewItem {
cell.configure(title: tabItem)
}
return cell
}
}
extension TabBarView: NSCollectionViewDelegate {
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
if let first = indexPaths.first {
let item = tabs[first.item]
eventHandler?(.select(item))
}
}
}
Class TabViewController preconfigured with style .unspecified
class TabViewController: GenericTabViewController<String> {
override func viewDidLoad() {
super.viewDidLoad()
transitionOptions = []
tabStyle = .unspecified
}
func process(item: String) {
if index(of: item) != nil {
select(itemIdentifier: item)
} else {
let vc = TabContentController(content: item)
let tabItem = GenericTabViewItem(identifier: item, viewController: vc)
addTabViewItem(tabItem)
select(itemIdentifier: item)
}
}
}
Rest of the classes.
class TabBarCollectionView: CollectionView {
override func setupUI() {
isSelectable = true
allowsMultipleSelection = false
allowsEmptySelection = false
backgroundView = View(backgroundColor: .magenta)
backgroundColors = [.clear]
}
}
class TabBarScrollView: ScrollView {
override func setupUI() {
borderType = .noBorder
backgroundColor = .clear
drawsBackground = false
horizontalScrollElasticity = .none
verticalScrollElasticity = .none
automaticallyAdjustsContentInsets = false
horizontalScroller = InvisibleScroller()
}
}
// Disabling scroll view indicators.
// See: https://stackoverflow.com/questions/9364953/hide-scrollers-while-leaving-scrolling-itself-enabled-in-nsscrollview
private class InvisibleScroller: Scroller {
override class var isCompatibleWithOverlayScrollers: Bool {
return true
}
override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat {
return CGFloat.leastNormalMagnitude // Dimension of scroller is equal to `FLT_MIN`
}
override func setupUI() {
// Below assignments not really needed, but why not.
scrollerStyle = .overlay
alphaValue = 0
}
}
class TabBarTabViewItem: CollectionViewItem {
private lazy var titleLabel = Label().autolayoutView()
override var isSelected: Bool {
didSet {
if isSelected {
titleLabel.font = Font.semibold(size: 10)
contentView.backgroundColor = .red
} else {
titleLabel.font = Font.regular(size: 10.2)
contentView.backgroundColor = .blue
}
}
}
override func setupUI() {
view.addSubviews(titleLabel)
view.wantsLayer = true
titleLabel.maximumNumberOfLines = 1
}
override func setupDefaults() {
isSelected = false
}
func configure(title: String) {
titleLabel.text = title
titleLabel.textColor = .white
titleLabel.alignment = .center
}
override func setupLayout() {
LayoutConstraint.withFormat("|-[*]-|", titleLabel).activate()
LayoutConstraint.withFormat("V:|-(>=4)-[*]", titleLabel).activate()
LayoutConstraint.centerY(titleLabel).activate()
}
}
class TabContentController: ViewController {
let content: String
private lazy var titleLabel = Label().autolayoutView()
init(content: String) {
self.content = content
super.init()
}
required init?(coder: NSCoder) {
fatalError()
}
override func setupUI() {
contentView.addSubview(titleLabel)
titleLabel.text = content
contentView.backgroundColor = .green
}
override func setupLayout() {
LayoutConstraint.centerXY(titleLabel).activate()
}
}
Here is how it looks like:
NSTabView isn't the most customizable class in Cocoa, but it is possible to subclass it and do your own drawing. You won't use much functionality from the superclass besides maintaining a collection of tab view items, and you'll end up implementing a number of NSView and NSResponder methods to get the drawing and event handling working correctly.
It might be best to look at one of the free or open source tab bar controls first, I've used PSMTabBarControl in the past, and it was much easier than implementing my own tab view subclass (which is what it was replacing).
I've recently done this for something I was working on.
I ended using a tabless tab view and then drawing the tabs myself in another view. I wanted my tabs to be part of a status bar at the bottom of the window.
You obviously need to support mouse clicks which is fairly easy, but you should make sure your keyboard support works too, and that's a little more tricky: you'll need to run timers to switch the tab after no keyboard access after half a second (have a look at the way OS X does it). Accessibility is another thing you should think about but you might find it just works—I haven't checked it in my code yet.
I very much got stuck on this - and posted NSTabView with background color - as the PSMTabBarControl is now out of date also posted https://github.com/dirkx/CustomizableTabView/blob/master/CustomizableTabView/CustomizableTabView.m
It's very easy to use a separate NSSegmentedCell to control tab selection in an NSTabView. All you need is an instance variable that they can both bind to, either in the File's Owner, or any other controller class that appears in your nib file. Just put something like this in the class Interface declaraton:
#property NSInteger selectedTabIndex;
Then, in the IB Bindings Inspector, bind the Selected Index of both the NSTabView and the NSSegmentedCell to the same selectedTabIndex property.
That's all you need to do! You don't need to initialize the property unless you want the default selected tab index to be something other than zero. You can either keep the tabs, or make the NSTabView tabless, it will work either way. The controls will stay in sync regardless of which control changes the selection.

Resources