How to draw your own NSTabView tabs?

前端 未结 6 1783
迷失自我
迷失自我 2021-02-09 17:05

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?

相关标签:
6条回答
  • 2021-02-09 17:17

    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/.

    0 讨论(0)
  • 2021-02-09 17:21

    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.

    0 讨论(0)
  • 2021-02-09 17:23

    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:

    0 讨论(0)
  • 2021-02-09 17:32

    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.

    0 讨论(0)
  • 2021-02-09 17:32

    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).

    0 讨论(0)
  • 2021-02-09 17:32

    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

    0 讨论(0)
提交回复
热议问题