NSPressGestureRecognizer called before minimumPressDuration - macos

I need my NSButton to respond to regular clicks, as well as long presses. I am adding NSPressGestureRecognizer like so:
override func viewDidLoad() {
super.viewDidLoad()
let gr = NSPressGestureRecognizer()
gr.minimumPressDuration = 1
gr.action = #selector(handleLongPress)
button.addGestureRecognizer(gr)
}
func handleLongPress(gr: NSPressGestureRecognizer) {
if gr.state == .Began {
Swift.print("long press")
}
}
Unfortunately, the handleLongPress randomly fires even at short single clicks, or double clicks. It happens even if i set the minimumPressDuration to higher values.
I have tried playing with the shouldBeRequiredToFailByGestureRecognizer but it is not solving the problem.
Is there something i am missing with my code?

I have solved this by handling multiple gesture recognizers:
the class must implement NSGestureRecognizerDelegate
var singleClick:NSClickGestureRecognizer?
var longClick:NSPressGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
longClick = NSPressGestureRecognizer()
longClick!.minimumPressDuration = 1
longClick!.action = #selector(handleGestureLong)
longClick!.delegate = self
singleClick = NSClickGestureRecognizer()
singleClick?.numberOfClicksRequired = 1
singleClick!.action = #selector(handleGestureSingle)
singleClick!.delegate = self
btn.addGestureRecognizer(longClick!)
btn.addGestureRecognizer(singleClick!)
}
func gestureRecognizer(gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOfGestureRecognizer otherGestureRecognizer: NSGestureRecognizer) -> Bool {
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick {
return false
}
return true
}
func handleGestureSingle(gr: NSClickGestureRecognizer) {
switch gr.state {
case .Ended:
print("single click")
break
default:
break
}
}
func handleGestureLong(gr: NSPressGestureRecognizer) {
switch gr.state {
case .Began:
print("long click")
break
default:
break
}
}

Related

Custom Delegate Filter VC Swift5

I'm adding a custom delegate to my app and, for some reason, it is not working.
My app has a map where I show several markers of different company types. There is also a button that, once pressed, takes me to another viewController where the user can input some filters. The user then presses "Apply" which would pass the filtering data to the map viewController.
The issue here is that no data is being passed.
As reference I followed the guideline https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef which works perfectly fine.
Here is the full project code https://github.com/afernandes0001/Custom-Delegate
I use Firebase but code below just shows pieces related to the delegate.
mapViewController - you will notice that I added a print to the prepareForSegue. When first loading the app and clicking "Search" button it shows nav1 as nil (which is expected) but, if I click Search and Apply (in filterVC), that print is never done.
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, FilterVCDelegate {
#IBOutlet weak var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
map.register(MyAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "clinicDetailsSegue" {
let clinicsDetailsViewController = segue.destination as! ClinicsDetailsViewController
clinicsDetailsViewController.id = self.note.mapId
} else if segue.identifier == "searchSegue" {
print("segue call")
let nav1 = segue.destination as? UINavigationController
print("nav1 \(nav1)")
if let nav = segue.destination as? UINavigationController, let filterVC = nav.topViewController as? FilterViewController {
filterVC.delegate = self
}
}
}
func chosenData(clinicNameFilter: String, stateFilter: String, cityFilter: String, esp1Filter: String, esp2Filter: String) {
print("Received data \(clinicNameFilter), \(stateFilter), \(cityFilter), \(esp1Filter), \(esp2Filter)")
}
}
FilterViewController
import UIKit
protocol FilterVCDelegate: class {
func chosenData(clinicNameFilter: String, stateFilter: String, cityFilter: String, esp1Filter: String, esp2Filter: String)
}
class FilterViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
weak var delegate: FilterVCDelegate?
var selectedName = ""
var statesJSON = [Estado]()
var cities = [Cidade]()
var state : Estate? // Selected State identifier
var city : City? // Selected City identifier
var selectedState = "" // Used to retrieve info from Firebase
var selectedCity = "" // Used to retrieve info from Firebase
var specialtiesJSON = [Specialty]()
var specialties2 = [Specialty2]()
var specialty1 : Specialty? // Selected Specialty1 identifier
var specialty2 : Specialty2? // Selected Specialty2 identifier
var selectedSpecialty1 = ""
var selectedSpecialty2 = ""
#IBOutlet weak var clinicName: UITextField!
#IBOutlet weak var statePicker: UIPickerView!
#IBOutlet weak var esp1Picker: UIPickerView!
#IBOutlet weak var esp2Picker: UIPickerView!
override func viewDidLoad() {
readJsonStates()
readJsonSpecialties()
super.viewDidLoad()
clinicName.text = ""
}
#IBAction func applyFilter(_ sender: Any) {
if clinicName.text == nil {
clinicName.text = ""
}
if selectedState != "" {
if selectedCity != "" {
if selectedSpecialty1 != ""{
if selectedSpecialty2 != "" {
delegate?.chosenData(clinicNameFilter: clinicName.text!, stateFilter: selectedState, cityFilter: selectedCity, esp1Filter: selectedSpecialty1, esp2Filter: selectedSpecialty2)
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController]
self.navigationController?.popToViewController(viewControllers[viewControllers.count - 2], animated: true)
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
statePicker.reloadAllComponents()
if pickerView == statePicker {
if component == 0 {
self.state = self.statesJSON[row]
self.coties = self.statesJSON[row].cities
statePicker.reloadComponent(1)
statePicker.selectRow(0, inComponent: 1, animated: true)
} else {
self.city = self.cities[row]
statePicker.reloadAllComponents()
}
} else if pickerView == esp1Picker {
self.specialty1 = self.specialtiesJSON[row]
self.specialties2 = self.specialtiesJSON[row].specialty2
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
esp2Picker.selectRow(0, inComponent: 0, animated: true)
} else if pickerView == esp2Picker {
self.specialty2 = self.specialties2[row]
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
}
let indexSelectedState = statePicker.selectedRow(inComponent: 0)
let indexSelectedCity = statePicker.selectedRow(inComponent: 1)
let indexSelectedEsp1 = esp1Picker.selectedRow(inComponent: 0)
let indexSelectedEsp2 = esp2Picker.selectedRow(inComponent: 0)
if indexSelectedState >= 0 {
if indexSelectedCity >= 0 {
selectedState = estadosJSON[indexSelectedState].name
selectedCity = cidades[indexSelectedCity].name
}
}
if indexSelectedEsp1 >= 0 {
if indexSelectedEsp2 >= 0 {
selectedSpecialty1 = specialtiesJSON[indexSelectedEsp1].name
selectedSpecialty2 = specialtiesJSON[indexSelectedEsp1].specialty2[indexSelectedEsp2].name
}
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
if pickerView == statePicker {
return 2
} else if pickerView == esp1Picker {
return 1
} else if pickerView == esp2Picker {
return 1
}
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView == statePicker {
if component == 0 {
return statesJSON.count
} else {
return cities.count
}
} else if pickerView == esp1Picker {
return self.specialtiesJSON.count
} else if pickerView == esp2Picker {
return specialties2.count
}
return 1
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
var rowTitle = ""
let pickerLabel = UILabel()
pickerLabel.textColor = UIColor.black
if pickerView == statePicker {
if component == 0 {
rowTitle = statesJSON[row].name
} else {
rowTitle = cities[row].name
}
} else if pickerView == esp1Picker {
rowTitle = specialtiesJSON[row].name
} else if pickerView == esp2Picker {
rowTitle = specialties2[row].name
}
pickerLabel.text = rowTitle
pickerLabel.font = UIFont(name: fontName, size: 16.0)
pickerLabel.textAlignment = .center
return pickerLabel
}
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
if pickerView == statePicker {
if component == 0 {
return 50
} else {
return 300
}
}
return 300
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
func readJsonStates() {
let url = Bundle.main.url(forResource: "StatesAndCities", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootState.self, from: data)
//handles the array of countries on your json file.
self.statesJSON = jsonResult.state
self.cities = self.statesJSON.first!.cities
} catch {
}
}
func readJsonSpecialties() {
let url = Bundle.main.url(forResource: "Specialties", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootEsp.self, from: data)
//handles the array of specialties on your json file.
self.specialtiesJSON = jsonResult.specialty
self.specialties2 = self.specialtiesJSON.first!.specialty2
} catch {
}
}
}
Any idea why, when I click ApplyFilter, delegate is not updated in the MapViewController?
Thanks
I found the error in my project.
The issue was with my Navigation Controller.
When I posted the error above, my Storyboard looked like the below
To make it work, I added the Navigation Controller to the Filter View Controller as below
That did the work and protocol is working as expected.

XCode Page View Controller: make dots transparent

i have added a page view controller and a 3 pages.
I set Transition Style to Scroll and implemented the presentationCount and the presentationIndex method. now i get a black bar with gray/white dots at the bottom of my view. However, but i want the view to goright to the bottom and the make the dots appear over it(without the black background.
How do i do that?
here is my code:
import UIKit
class FilterViewController: UIPageViewController, UIPageViewControllerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
private(set) lazy var orderedViewControllers: [UIViewController] = {
return [self.newViewController(category: "first"),
self.newViewController(category: "second"),
self.newViewController(category: "third")]
}()
private func newViewController(category: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewController(withIdentifier: "\(category)ViewController")
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return orderedViewControllers.last
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = orderedViewControllers.count
guard orderedViewControllersCount != nextIndex else {
return orderedViewControllers.first
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return orderedViewControllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first,
let firstViewControllerIndex = orderedViewControllers.index(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
ok, i got it myself. If anyone has the same problem, just override the viewDidLayoutSubviews() of the Page View Controller:
override func viewDidLayoutSubviews() {
let v = self.view
let subviews = v?.subviews
if subviews?.count == 2 {
var sv:UIScrollView?
var pc:UIPageControl?
for t in subviews! {
if t is UIScrollView {
sv = t as! UIScrollView
} else {
pc = t as! UIPageControl
}
}
if(sv != nil && pc != nil) {
sv?.frame = (v?.bounds)!
v?.bringSubview(toFront: pc!)
}
}
super.viewDidLayoutSubviews()
}

Unrecognized selector sent to instance when using UIPanGesture

Hi I am trying to add UIPanGestureRecognizer to UIImageView (in my case, it's an emoji). All other UIGestureRecognizers such as long press, rotation, and pinch work well. However, it gives me an error: unrecognized selector sent to instance when I add UIPanGestureRecognizer. I've spent a day trying to figure out the reason but failed to fix it. Please help! Thanks in advance.
This is a function where I added UIGestureRecognizer to sticker
func emojiInsert(imageName: String) {
deleteButtonHides()
let stickerView: UIImageView = UIImageView(frame: CGRectMake(backgroundImage.frame.width/2 - 50, backgroundImage.frame.height/2 - 50, stickerSize, stickerSize))
stickerView.image = UIImage(named: imageName)
stickerView.userInteractionEnabled = true
stickerView.accessibilityIdentifier = "sticker"
let deleteStickerButton: UIImageView = UIImageView(frame: CGRectMake(stickerView.frame.width - 5 - stickerView.frame.width/3, 5, stickerView.frame.width/3, stickerView.frame.height/3))
deleteStickerButton.image = UIImage(named: "button_back")
deleteStickerButton.accessibilityIdentifier = "delete"
deleteStickerButton.userInteractionEnabled = true
deleteStickerButton.alpha = 0
deleteStickerButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "deleteButtonTouches:"))
stickerView.addSubview(deleteStickerButton)
stickerView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: "handlePinch:"))
stickerView.addGestureRecognizer(UIRotationGestureRecognizer(target: self, action: "handleRotate:"))
stickerView.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: "handleLongPress:"))
stickerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePan"))
print("emojiInsert : \(imageName)")
backgroundImage.addSubview(stickerView)
}
Below are call back functions I added in the end of the view.swift. I used touchesbegan and touchesMoved to drag an emoji but emoji moved in weird way after rotation. So now I am trying to use UIPanGesture to drag an emoji.
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
if(deleteMode) {
return
}
print("handlePinch \(recognizer.scale)")
if let view = recognizer.view {
view.transform = CGAffineTransformScale(view.transform,
recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
}
#IBAction func handleRotate(recognizer : UIRotationGestureRecognizer) {
if(deleteMode) {
return
}
if let view = recognizer.view {
view.transform = CGAffineTransformRotate(view.transform, recognizer.rotation)
recognizer.rotation = 0
}
}
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
if(deleteMode) {
return
}
let translation = recognizer.translationInView(self.view)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: self.view)
}
#IBAction func handleLongPress(recognizer: UILongPressGestureRecognizer) {
if(recognizer.state == UIGestureRecognizerState.Began) {
if(!deleteMode) {
print("LongPress - Delete Shows")
for (_, stickers) in self.backgroundImage.subviews.enumerate() {
for (_, deleteButtons) in stickers.subviews.enumerate() {
if let delete:UIImageView = deleteButtons as? UIImageView{
if(delete.accessibilityIdentifier == "delete") {
delete.alpha = 1
}
}
}
}
deleteMode = true
} else {
deleteButtonHides()
}
}
}
Again, please help! Thanks in advance.
The problem is that you're missing a colon. In the following line:
stickerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePan"))
The handlePan should be handlePan:. That's because the Objective-C signature for your method is:
- (void)handlePan:(UIPanGestureRecognizer *)recognizer
The colon is part of the method name.

Cocoa: react to keyDown in QLPreviewPanel

I implemented quick look in my project in the following way in Swift 2 (I'm including this here for reference and because it might help someone else set it up).
My NSViewController contains a NSTableView subclass where I implemented keyDown to listen to the spacebar key being pressed (maybe not the best way but it works):
override func keyDown(theEvent: NSEvent) {
let s = theEvent.charactersIgnoringModifiers!
let s1 = s.unicodeScalars
let s2 = s1[s1.startIndex].value
let s3 = Int(s2)
if s3 == Int(" ".utf16.first!) {
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name: "MyTableViewSpacebar", object: nil))
return
}
super.keyDown(theEvent)
}
In my view controller, I have an observer for this notification and the functions required by the QLPreviewPanel:
//...
class ViewController: NSViewController {
#IBOutlet weak var myTableView: MyTableView!
var files = [FilesListData]() //array of custom class
//...
override func viewDidLoad() {
//...
NSNotificationCenter.defaultCenter().addObserver(self, selector: "spaceBarKeyDown:", name: "MyTableViewSpacebar", object: nil)
}
func spaceBarKeyDown(notification: NSNotification) {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
panel.makeKeyAndOrderFront(self)
}
}
override func acceptsPreviewPanelControl(panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(panel: QLPreviewPanel!) {
panel.delegate = self
panel.dataSource = self
}
override func endPreviewPanelControl(panel: QLPreviewPanel!) {
}
}
extension ViewController: QLPreviewPanelDataSource {
func numberOfPreviewItemsInPreviewPanel(panel: QLPreviewPanel!) -> Int {
return self.myTableView.selectedRowIndexes.count
}
func previewPanel(panel: QLPreviewPanel!, previewItemAtIndex index: Int) -> QLPreviewItem! {
if self.myTableView.selectedRow != -1 {
var items = [QLPreviewItem]()
let manager = NSFileManager.defaultManager()
for i in self.myTableView.selectedRowIndexes {
let path = self.files[i].path //path to a MP3 file
if manager.fileExistsAtPath(path) {
items.append(NSURL(fileURLWithPath: path))
} else {
items.append(qm_url) //image of a question mark used as placeholder
}
}
return items[index]
} else {
return qm_url //image of a question mark used as placeholder
}
}
}
What I would like to do now is listen to the keys "up arrow" and "down arrow" being pressed while the quick look panel is open, in order to change the selected row in the NSTableView, much like Finder behaves when you preview files with quick look. I have no clue as to how I could implement this. Any ideas?
Thanks.
Finally found what I was looking for and it's actually pretty simple.
Since my main view controller is also my delegate for the QLPreviewPanel, I added this:
extension ViewController: QLPreviewPanelDelegate {
func previewPanel(panel: QLPreviewPanel!, handleEvent event: NSEvent!) -> Bool {
let kc = event.keyCode
if (kc == 126 || kc == 125) { //up and down arrows
if event.type == NSEventType.KeyDown {
self.myTableView.keyDown(event) //send the event to the table
} else if event.type == NSEventType.KeyUp {
self.myTableView.keyUp(event)
}
return true
}
return false
}
}
Then in my table view delegate:
func tableViewSelectionDidChange(notification: NSNotification) {
guard myTableView.numberOfSelectedRows > 0 else {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.close()
}
}
return
}
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.reloadData()
}
}
}
That's it! The QLPreviewPanelDataSource handles the rest.

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