Mac开发跬步积累(三):被忽略的 NSTabViewController

图片来自网络

从名字上看,NSTabViewController 很容易让熟悉iOS开发的人联想到UITableviewController,但是它在行为上更像是iOS中另外一个常用的控制器UITabBarController

0x00: NSTabViewController 简介

NSTabViewControllermacOS 10.10 之后推出的一个UI层级的控制器,可以通过使用多个Tab标签来管理多个子业务控制器,实现业务分离.

Apple 官方描述

NSTabViewController作为一个容器业务控制器,可以管理多个页面,并且一次仅显示一个页面

  1. 我们先看一个简单的示例效果:


    NSTabViewController的 四种 Style
  • NSTabViewController 有四种显示样式,可以通过tabStyle属性进行设置,它是一个枚举类型,具体效果如上图;
extension NSTabViewController {
    @available(OSX 10.10, *)
    public enum TabStyle : Int {
        /// Uses an NSSegmentedControl to show the UI for the tabs. The control is on the top of the view.
        case segmentedControlOnTop
        /// Uses an NSSegmentedControl to show the UI for the tabs. The control is on the bottom of the view.
        case segmentedControlOnBottom
        /// Automatically pushes the tabs into the window's toolbar as toolbar items, if non-nil. This style will cause the TabViewController to set its containing window's toolbar to its own and become that toolbar's delegate. The toolbar items can be customized or supplemented by overriding the relevant NSToolbarDelegate methods.
        case toolbar
        /// NSTabViewController will not provide any of its own tab control UI. Separate UI, such as a NSSegmentedControl or NSPopupButton, can be easily bound to the TabViewController. Or \c tabView.tabViewType can be changed for the TabView itself to draw the UI.
        case unspecified
    }
}
  1. NSTabViewController提供了默认的切换子控制器的转场效果:Crossfade
    子控制器的转场切换效果 Crossfade

    NSTabViewController提供了一个枚举属性transitionOptions可以设置切换转场效果
 open var transitionOptions: NSViewController.TransitionOptions

关于NSViewController.TransitionOptions详细效果可以参看Mac开发跬步积累(二):NSViewController 转场动画精耕细作

0x01: NSTabViewController设置更多Style

NSTabViewControllertabStyle属性仅提供了4种样式,但实际开发中可能会需要下图中的两种情况(居左/居右)

切换栏居左/居右

我们使用tabViewtabViewType代替NSTabViewController的样式设置,即可实现更多的样式设置效果.

  1. 使用Storyboard设置:


    storyboard 设置 tabView 的 type
  2. 使用代码设置:

import Cocoa
class TabViewController: NSTabViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        1. 先设置NSTableView的样式为unspecified 
        tabStyle = .unspecified  
        2. 设置tabView的type样式 居左
        tabView.tabViewType = .leftTabsBezelBorder
    }
}

从代码设置中可以看出一个事实: NSTabViewController的最终样式是由NSTabViewController的tabStyle属性与tabViewtabViewType属性值共同作用的效果;
我们可以使用下面这段代码来验证这个事实:

import Cocoa
class TabViewController: NSTabViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        1. 设置显示在顶部
        tabStyle = .segmentedControlOnTop      
        2. 设置显示在左边
        tabView.tabViewType = .leftTabsBezelBorder  
    }
}

实现效果如图(同时显示顶部和左边):


同时显示顶部和左边

0x03: NSTabViewController的应用场景

无论在macOS系统中或者在其他应用中,NSTabViewController都有广泛的使用场景

NSTabViewController的应用场景

每个macOS App 几乎都有一个功能: 偏好设置,如果偏好设置中的选项比较少,一个页面就足够展示,这种情况使用一个NSViewController就可以实现效果了,但通常来讲,我们希望自己的App能提供给用户更多的选项设置,以便于用户可以更多的进行个性化选择功能,这时候就属于NSTabViewController用武之地

0x04: 用NSTabViewController实现偏好设置功能(敲黑板~划重点)

我们先看一下系统Finder的偏好设置,然后我们通过NSTabViewController来模仿类似的效果来强化对NSTabViewController的学习.

  1. 系统Finder 偏好设置的切换效果:
系统Finder 偏好设置
  • 需求点: 在NSTabViewController切换业务控制器时,需要动态的调整所在window尺寸
  1. 效果实现:
    要在NSTabViewController切换选项时,动态的计算窗口size,并根据实际size设置window的尺寸,我们需要通过创建一个继承NSTabViewController的子类重写tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?)即可.
import Cocoa

class TabViewController: NSTabViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
        super.tabView(tabView, didSelect: tabViewItem)
        
        guard let itemView = tabViewItem?.view,
            let window = view.window
            else {return}
        let oldFrame = window.frame
        
        let newViewSize = itemView.fittingSize
        
        var newFrame = window.frameRect(forContentRect: NSMakeRect(oldFrame.origin.x, oldFrame.origin.y, newViewSize.width, newViewSize.height))
        
        let newY = oldFrame.origin.y - ( newFrame.size.height - oldFrame.size.height)
        newFrame.origin = NSMakePoint(oldFrame.origin.x, newY)
        
        NSAnimationContext.runAnimationGroup({ (context) in
            window.animator().setFrame(newFrame, display: window.isVisible)
        }, completionHandler: nil)
    }
}

划重点
如果你实现的效果与预期的不同,那么一定是你在子业务控制器中少写了下面这行代码

   self.preferredContentSize = view.frame.size
  1. 最终实现效果:


    实现效果
  2. Demo Github 地址: Day31- NSTabViewController

0x05: NSTabViewController 小结

  • NSTabViewController 支持的样式有4种;
  • 实现更多的样式,需要使用tabViewtabViewType枚举;
  • NSTabViewControllerviewNSView,它里面包含一个NSTabViewNSSegmentedControl(样式为segmentedTop/segmentedBottom时)
  • NSTabViewController的样式结果由NSTabViewControllertabStyle属性与tabViewtabViewType属性值共同作用的
  • 切换子业务控制器时,会触发方法tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?)

0x06: One more thing .....

NSTabViewController的非ToolBar样式时如果需要实现特殊的选项卡效果,需要自定义NSSegmentedControl.
关于NSViewNSViewController的相关基础,有兴趣的同学可以参考macOS 开发基础视频教程中的项目代码(地址在文章中有链接)

推荐阅读更多精彩内容