Custom NSView Drag and Drop images - image

I'm working on an OS X App and, I can't seem to get Drag and Drop to work. I've Googled a lot, but most posts about this subject are at least a few years old and none of them tells me the missing link I have in my thoughts.
Anyway, here is what I'm trying to do. I have an image somewhere on my desktop and I want the ability to drag and drop that into my Custom NSView. The custom view is a child object of a custom NSView named CircularImageView and is layer backed and only shows a circular shaped image on the screen.
Here's the code:
import Cocoa
import MCTools
#objc public protocol DragAndDropCircularImageViewDelegate {
func imageDumped(sender: AnyObject!)
}
#IBDesignable #objc public class DragAndDropCircularImageView: CircularImageView {
// This class provides the Drag And Drop Feature to the CircularImageView Class.
// MARK: New in this class
var highlight: Bool = false
public var delegate: DragAndDropCircularImageViewDelegate?
private func registerForDraggedImages() {
self.registerForDraggedTypes(NSImage.imageTypes())
}
// MARK: CircularImageView Stuff
public override var image: NSImage? {
didSet {
if let newImage = image {
delegate?.imageDumped(self)
}
}
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.registerForDraggedImages()
}
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.registerForDraggedImages()
}
public override func updateLayer() {
super.updateLayer()
if highlight == true {
}
}
// MARK: NS Dragging Destination Protocol
public override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation {
// When a drag enters our drop zone.
if NSImage.canInitWithPasteboard(sender.draggingPasteboard()) {
if ((sender.draggingSourceOperationMask().rawValue & NSDragOperation.Copy.rawValue) > 0) {
highlight = true
self.needsLayout = true
sender.enumerateDraggingItemsWithOptions(.Concurrent, forView: self, classes: [NSPasteboardItem.self], searchOptions: [NSPasteboardURLReadingContentsConformToTypesKey: self], usingBlock: { (draggingItem, idx, stop) -> Void in
return
})
}
return NSDragOperation.Copy
}
return NSDragOperation.None
}
public override func draggingExited(sender: NSDraggingInfo?) {
// When drag exits our drop zone remove highlight of the drop zone.
println("\(self)draggingExited")
highlight = false
self.needsLayout = true
}
public override func prepareForDragOperation(sender: NSDraggingInfo) -> Bool {
// Update view for hovering drop.
println("\(self)prepareForDragOperation")
highlight = false
self.needsLayout = true
// Can we accept the drop?
return NSImage.canInitWithPasteboard(sender.draggingPasteboard())
}
public override func performDragOperation(sender: NSDraggingInfo) -> Bool {
// Handle the drop data.
println("\(self)performDragOperation \(sender)")
if NSImage.canInitWithPasteboard(sender.draggingPasteboard()) {
self.image = NSImage(pasteboard: sender.draggingPasteboard())
}
return true
}
// MARK: Interface Builder Stuff
}
I have seen some posts that I should be using:
self.registerForDraggedTypes([NSFilenamesPboardType])
instead of:
self.registerForDraggedTypes(NSImage.imageTypes())
But this doesn't seem to work in my case, when I'm using NSFileNamesPboardType I get the following debug message even before any of the NSDraggingDestination protocol messages have been called:
2015-05-07 11:07:19.583 CircularImageViewTest[44809:14389647] -[CircularView.DragAndDropCircularImageView copyWithZone:]: unrecognized selector sent to instance 0x608000166d80
(lldb) p 0x608000166d80
(Int) $R0 = 106102873550208
I don't understand how this works. Somewhere the frameworks try to copyWithZone on an integer? Can anyone explain this to me?
Any help would be appreciated. Thanks in advance.

Ok, the code below works. It was all caused by sender.enumerateDraggingItemsWithOptions in draggingEntered. Something goes wrong in the Apple frameworks when it is called.
import Cocoa
import MCTools
#objc public protocol DragAndDropCircularImageViewDelegate {
func imageDumped(sender: AnyObject!)
}
#IBDesignable #objc public class DragAndDropCircularImageView: CircularImageView {
// This class provides the Drag And Drop Feature to the CircularImageView Class.
// MARK: New in this class
var highlight: Bool = false
public weak var delegate: DragAndDropCircularImageViewDelegate?
private func registerForDraggedImages() {
// self.registerForDraggedTypes(NSImage.imageTypes())
self.registerForDraggedTypes([NSFilenamesPboardType])
}
// MARK: CircularImageView Stuff
public override var image: NSImage? {
didSet {
if let newImage = image {
delegate?.imageDumped(self)
}
}
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.registerForDraggedImages()
}
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.registerForDraggedImages()
}
public override func updateLayer() {
super.updateLayer()
if highlight == true {
}
}
// MARK: NS Dragging Destination Protocol
public override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation {
// When a drag enters our drop zone.
if NSImage.canInitWithPasteboard(sender.draggingPasteboard()) {
if ((sender.draggingSourceOperationMask().rawValue & NSDragOperation.Copy.rawValue) > 0) {
highlight = true
self.needsLayout = true
}
return NSDragOperation.Copy
}
return NSDragOperation.None
}
public override func draggingExited(sender: NSDraggingInfo?) {
// When drag exits our drop zone remove highlight of the drop zone.
println("\(self)draggingExited")
highlight = false
self.needsLayout = true
}
public override func prepareForDragOperation(sender: NSDraggingInfo) -> Bool {
// Update view for hovering drop.
println("\(self)prepareForDragOperation")
highlight = false
self.needsLayout = true
// Can we accept the drop?
return NSImage.canInitWithPasteboard(sender.draggingPasteboard())
}
public override func performDragOperation(sender: NSDraggingInfo) -> Bool {
// Handle the drop data.
println("\(self)performDragOperation \(sender)")
if NSImage.canInitWithPasteboard(sender.draggingPasteboard()) {
self.image = NSImage(pasteboard: sender.draggingPasteboard())
self.delegate!.imageDumped(self)
}
return true
}
// MARK: Interface Builder Stuff
}

Related

Swift: is it correct to assign a custom class to all UINavigationControllers?

This is more or less my Main.storyboard situation:
where I have a root UITabBarController with 5 possibile choices. Then, I want that some UIViewControllers can rotate to landscape while I want also some other UIViewControllers to have only landscape mode. So I have written this file.swift:
class CustomNavigationController: UINavigationController, UINavigationControllerDelegate {
override func shouldAutorotate() -> Bool {
if (self.topViewController?.isKindOfClass(HomeViewController) != nil) {return false}
else if (self.topViewController?.isKindOfClass(ServicesViewController) != nil) {return false}
else if (self.topViewController?.isKindOfClass(PhotoGalleryViewController) != nil) {return false}
else if (self.topViewController?.isKindOfClass(SelectMapViewController) != nil) {return false}
else if (self.topViewController?.isKindOfClass(MapViewController) != nil) {return false}
else {return true}
}
}
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
override func shouldAutorotate() -> Bool {
return (self.selectedViewController as! UINavigationController).shouldAutorotate()
}
}
and I have assigned to all UINavigationControllers the same class
CustomNavigationController while I have assigned CustomTabBarController class to UITabBarController.
The result is that no view controller do not rotates. Is this because I have assigned the same class to them? Shall I create a custom navigation controller class for each UINavigationController I have?
UPDATE
A partial solution I found, even if it's a little intricate, is the following. I have modified the previous file like that:
class CustomNavigationController: UINavigationController, UINavigationControllerDelegate {
override func shouldAutorotate() -> Bool {
return (self.topViewController?.shouldAutorotate())!
}
}
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate {
override func shouldAutorotate() -> Bool {
return (self.selectedViewController as! UINavigationController).shouldAutorotate()
}
}
Then, in view controllers where rotation is allowed I simply have:
override func shouldAutorotate() -> Bool {
return true
}
while in view controllers where rotation is not allowed I have:
override func shouldAutorotate() -> Bool {
return false
}
override func viewWillAppear(animated: Bool) {
let value = UIInterfaceOrientation.Portrait.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
Anyway there's a little problem because the animation which sets mode to portrait is not correct meaning that the width of the screen is not adjusted. if I go from a landscape view controller to a portrait only view controller then the view controller frame is not correct. I get
instead of this:
Try this in AppDelegate
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if let rootController = self.window?.rootViewController as? UITabBarController {
if let navigationController = rootController.selectedViewController as? UINavigationController {
let controller = navigationController.topViewController!
if controller.isKindOfClass(HomeViewController.classForCoder()) {
return UIInterfaceOrientationMask.Portrait
}
if controller.isKindOfClass(ServicesViewController.classForCoder()) {
return UIInterfaceOrientationMask.Portrait
}
if controller.isKindOfClass(PhotoGalleryViewController.classForCoder()) {
return UIInterfaceOrientationMask.Portrait
}
if controller.isKindOfClass(SelectMapViewController.classForCoder()) {
return UIInterfaceOrientationMask.Portrait
}
if controller.isKindOfClass(MapViewController.classForCoder()) {
return UIInterfaceOrientationMask.Portrait
}
}
}
return UIInterfaceOrientationMask.All
}
Update:
This method forces application to change orientation in ViewController that should be only in Portrait (HomeViewController, ServicesViewController, PhotoGalleryViewController, SelectMapViewController, MapViewController):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let value = UIInterfaceOrientation.Portrait.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}

Xcode Swift: How to call UIPickerView from UIButton?

I need to open UIPickerView once a user touch a UIButton and to return the text value choosen on UIPickerview to UIButton label.
I'm not able to change the UIButton'n inputview like for UITextField, so making the property writable seems to be the right way. Unfortunatelly nothing happens when the button is touched.
import UIKit
class ABButton: UIButton {
var modInputView: UIView!
override var inputView: UIView { get {
if modInputView != nil {
return modInputView
}
else {
return super.inputView!
}
}}
override func canBecomeFirstResponder() -> Bool {
return true
}
}
class LiczydloViewController: UIViewController {
#IBOutlet weak var buttonTempo10: ABButton!
override func viewDidLoad() {
super.viewDidLoad()
var tempoPicker = UIDatePicker()
buttonTempo10.modInputView = tempoPicker
}
Add an action for touchUpInside for the button, and call button.becomeFirstResponder()
try to set the frame of UIDatePicker. I tried it with luck.
class ZYInputButton: UIButton {
var zyInputView: UIView?
var zyInputAccessoryView: UIView?
override var inputView: UIView? {
get {
return self.zyInputView
}
set {
self.zyInputView = newValue
}
}
override var inputAccessoryView: UIView? {
get {
return self.zyInputAccessoryView
}
set {
self.zyInputAccessoryView = newValue
}
}
override func canBecomeFirstResponder() -> Bool {
return true
}
}

UINavigationController-Alike for Desktop-Cocoa?

I'm currently developing an application with an user interface much like Twitter for Mac (Pushing in/out of views like on iOS).
Has anyone implemented a UIViewController for desktop Cocoa? This would save me many hours of work.
There isn't one in standard AppKit at this time. You'll have to write your own.
This may help if you decide to go down that path: http://parsekit.com/umekit/
UMEKit is a little framework for Cocoa that implements some equivalents to UIKit classes and UI components.
I spent ages looking for this and started to write my own, then found https://github.com/bfolder/BFNavigationController
For some reason Google doesn't know about it.
Here how we did NavigationController, without navigation bar. Implementing navigation bar with transitions between navigation items similar to how transition between views made in example below.
import AppKit
public class NavigationController: NSViewController {
public private (set) var viewControllers: [NSViewController] = []
open override func loadView() {
view = NSView()
view.wantsLayer = true
}
public init(rootViewController: NSViewController) {
super.init(nibName: nil, bundle: nil)
pushViewController(rootViewController, animated: false)
}
public required init?(coder: NSCoder) {
fatalError()
}
}
extension NavigationController {
public var topViewController: NSViewController? {
return viewControllers.last
}
public func pushViewControllerAnimated(_ viewController: NSViewController) {
pushViewController(viewController, animated: true)
}
public func pushViewController(_ viewController: NSViewController, animated: Bool) {
viewController.navigationController = self
viewController.view.wantsLayer = true
if animated, let oldVC = topViewController {
embedChildViewController(viewController)
let endFrame = oldVC.view.frame
let startFrame = endFrame.offsetBy(dx: endFrame.width, dy: 0)
viewController.view.frame = startFrame
viewController.view.alphaValue = 0.85
viewControllers.append(viewController)
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.2
context.allowsImplicitAnimation = true
context.timingFunction = .easeOut
viewController.view.animator().frame = endFrame
viewController.view.animator().alphaValue = 1
oldVC.view.animator().alphaValue = 0.25
}) {
oldVC.view.alphaValue = 1
oldVC.view.removeFromSuperview()
}
} else {
embedChildViewController(viewController)
viewControllers.append(viewController)
}
}
#discardableResult
public func popViewControllerAnimated() -> NSViewController? {
return popViewController(animated: true)
}
#discardableResult
public func popViewController(animated: Bool) -> NSViewController? {
guard let oldVC = viewControllers.popLast() else {
return nil
}
if animated, let newVC = topViewController {
let endFrame = oldVC.view.frame.offsetBy(dx: oldVC.view.frame.width, dy: 0)
view.addSubview(newVC.view, positioned: .below, relativeTo: oldVC.view)
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.23
context.allowsImplicitAnimation = true
context.timingFunction = .easeIn
oldVC.view.animator().frame = endFrame
oldVC.view.animator().alphaValue = 0.85
}) {
self.unembedChildViewController(oldVC)
}
} else {
unembedChildViewController(oldVC)
}
return oldVC
}
}
Reusable extensions:
extension NSViewController {
private struct OBJCAssociationKey {
static var navigationController = "com.mc.navigationController"
}
public var navigationController: NavigationController? {
get {
return ObjCAssociation.value(from: self, forKey: &OBJCAssociationKey.navigationController)
} set {
ObjCAssociation.setAssign(value: newValue, to: self, forKey: &OBJCAssociationKey.navigationController)
}
}
}
extension NSViewController {
public func embedChildViewController(_ vc: NSViewController, container: NSView? = nil) {
addChildViewController(vc)
vc.view.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
vc.view.autoresizingMask = [.height, .width]
(container ?? view).addSubview(vc.view)
}
public func unembedChildViewController(_ vc: NSViewController) {
vc.view.removeFromSuperview()
vc.removeFromParentViewController()
}
}
struct ObjCAssociation {
static func value<T>(from object: AnyObject, forKey key: UnsafeRawPointer) -> T? {
return objc_getAssociatedObject(object, key) as? T
}
static func setAssign<T>(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_ASSIGN)
}
static func setRetainNonAtomic<T>(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
static func setCopyNonAtomic<T>(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
static func setRetain<T>(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN)
}
static func setCopy<T>(value: T?, to object: Any, forKey key: UnsafeRawPointer) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_COPY)
}
}

Cocoa Button that will light up with mouse over

Is there a flag that can be set that will cause a Cocoa button to be highlighted when it is moused over. I need to this programatically with objective C on OSX.
Setup a tracking area for the view with addTrackingArea (provided you are using Leopard or newer OS X). You'll get events on mouse enter and mouse exit.
something below maybe the answer.
class HoverButton: NSButton{
var backgroundColor: NSColor?
var hoveredBackgroundColor: NSColor?
var pressedBackgroundColor: NSColor?
private var hovered: Bool = false
override var wantsUpdateLayer:Bool{
return true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInit()
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.commonInit()
}
func commonInit(){
self.wantsLayer = true
self.createTrackingArea()
self.hovered = false
self.hoveredBackgroundColor = NSColor.selectedTextBackgroundColor()
self.pressedBackgroundColor = NSColor.selectedTextBackgroundColor()
self.backgroundColor = NSColor.clearColor()
}
private var trackingArea: NSTrackingArea!
func createTrackingArea(){
if(self.trackingArea != nil){
self.removeTrackingArea(self.trackingArea!)
}
let circleRect = self.bounds
let flag = NSTrackingAreaOptions.MouseEnteredAndExited.rawValue + NSTrackingAreaOptions.ActiveInActiveApp.rawValue
self.trackingArea = NSTrackingArea(rect: circleRect, options: NSTrackingAreaOptions(rawValue: flag), owner: self, userInfo: nil)
self.addTrackingArea(self.trackingArea)
}
override func mouseEntered(theEvent: NSEvent) {
self.hovered = true
NSCursor.pointingHandCursor().set()
self.needsDisplay = true
}
override func mouseExited(theEvent: NSEvent) {
self.hovered = false
NSCursor.arrowCursor().set()
self.needsDisplay = true
}
override func updateLayer() {
if(hovered){
if (self.cell!.highlighted){
self.layer?.backgroundColor = pressedBackgroundColor?.CGColor
}
else{
self.layer?.backgroundColor = hoveredBackgroundColor?.CGColor
}
}
else{
self.layer?.backgroundColor = backgroundColor?.CGColor
}
}
}
link: https://github.com/fancymax/HoverButton
It is also possible to override the button cell to propagate the mouse enter/exit event to the view. I did not test all buttons styles, I use Recessed style.
The drawBezel function is called on mouseEnter if showsBorderOnlyWhileMouseInside is true.
That's why I simply override it, and manage my custom display behaviour in the button.
class myButtonCell: NSButtonCell {
override func mouseEntered(with event: NSEvent) {
controlView?.mouseEntered(with: event)
// Comment this to remove title highlight (for button style)
super.mouseEntered(with: event)
}
override func mouseExited(with event: NSEvent) {
controlView?.mouseExited(with: event)
// Comment this to remove title un-highlight (for recessed button style)
// Here, we un-hilight the title only if the button is not selected
if state != .on { super.mouseExited(with: event) }
}
// removes the default highlight behavior
override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {}
}
// Set the cell class to MyButtonCell in interface builder
class MyButton: NSButton {
var mouseIn: Bool = false { didSet {
// Up to you here - setNeedsDisplay() or layer update
}}
override func mouseEntered(with event: NSEvent) { mouseIn = true }
override func mouseExited(with event: NSEvent) { mouseIn = false }
}

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