Mac开发跬步积累(二):NSViewController 转场动画精耕细作

图片来自网络

iOS相比,在macOS中,控制器的转场情景相对要简洁一些,没有iOS中导航控制器的PushPop动画以及边缘返回手势, 保留下的Present方式,倒是提供了特有的切换方式, 可以供我们使用出许多效果.

关于NSViewController基础细节,有兴趣的同学可以参考我的Mac开发基础教程这个系列的教程,友情提示: 自学能力好的同学可以参考github中的课程代码.另外一门macOS 应用开发进阶课程,供有项目经验或对组件化感兴趣的同学参考.

0x00 : extension NSViewController

macOS 10.10之后,关于NSViewController,苹果公司专门在一个extension中提供了四个方法用来处理控制器之间的关系以及切换转场处理.

   1. 内嵌在同一个窗口中形式弹出新的ViewController
    open func presentViewControllerAsSheet(_ viewController: NSViewController)
    
   2. 新窗口的形式弹出新的ViewController
    open func presentViewControllerAsModalWindow(_ viewController: NSViewController)

   3. Popover的形式弹出新的ViewController
    open func presentViewController(_ viewController: NSViewController, asPopoverRelativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge, behavior: NSPopover.Behavior)

   4. 从fromViewController转换到toViewController
    open func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Swift.Void)? = nil)

0x01 : present 与 transition

在上面的系统提供的NSViewController四个方法中,可以分为presenttransition两种方式:

  • presentXXX: 所有的present方式都是通过调用 presentViewController(NSViewController, animator: Animator)这个方法来完成展示的,并提供一个遵守NSViewControllerPresentationAnimator协议的animator控制整个动画过程.
    <如果希望实现自定义的Present转场效果,可以通过自定义animator方式后面会讲到具体实现步骤>

  • transition: 使用一个容器视图Contain View, 通过addSubViewremoveSubView的方式实现两个控制器之间的动画切换展示,系统提供了下面8中过渡动画方式:

 @available(OSX 10.10, *)
    public struct TransitionOptions : OptionSet {

        public static var crossfade: NSViewController.TransitionOptions { get }
        
        public static var slideUp: NSViewController.TransitionOptions { get }

        public static var slideDown: NSViewController.TransitionOptions { get }

        public static var slideLeft: NSViewController.TransitionOptions { get }

        public static var slideRight: NSViewController.TransitionOptions { get }

        public static var slideForward: NSViewController.TransitionOptions { get }

        public static var slideBackward: NSViewController.TransitionOptions { get }

        public static var allowUserInteraction: NSViewController.TransitionOptions { get }
    }
crossfade 效果
crossfade效果
slideUp/slideDown 效果
slideUp/slideDown 效果
slideLeft/slideRight (slideForward/slideBackward ) 效果
slideLeft/slideRight (slideForward/slideBackward ) 效果
allowUserInteraction 效果
allowUserInteraction 效果

0x02 : transition 细节:

在进行transition时,所有需要切换的child ViewController必须是同一个 super ViewController,否则会抛出异常错误.

  1. transition方法仅支持有父子关系的控制器结构.
  2. transition由父控制器super ViewController进行调用.
  3. transition仅在子控制器child ViewController之间进行切换.
  4. transition方法中,fromViewcontroller 的视图必须有superView,否则抛出异常.

0x03: transition Demo

示例代码: TransAnimationController demo

  1. 搭建UI界面:
构建UI界面
  1. 代码部分:
class ViewController: NSViewController {
 
1. 从Storyboard中的CustomView 连线的控件属性,用来作为容器视图,显示每个ChildViewController的内容      
    @IBOutlet weak var containView: MYContainView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
2. 添加需要切换的子控制器: RedController 和BlueController 为自定义的两个控制器,仅显示不同的视图颜色.
        addChildViewController(RedController())
        addChildViewController(BlueController())
3. 需要将第一个ChildViewController的view添加到容器视图中;
        containView.addSubview(childViewControllers[0].view)
4. 设置容器视图的颜色
        containView.layer?.backgroundColor = NSColor.orange.cgColor
    
    }
5. 点击下一个按钮, 从RedController 切换到BlueController
    @IBAction func clickBtn(_ sender: Any) {
        transition(from: childViewControllers[0], to: childViewControllers[1], options: .slideLeft, completionHandler: nil)
    }
6. 点击上一个按钮, 从BlueController 切换到RedController
    @IBAction func clickUpButton(_ sender: Any) {
        transition(from: childViewControllers[1], to: childViewControllers[0], options: .slideRight, completionHandler: nil)
    }
}
6. 修改4,5 步骤中的option 参数,可以实现不同的transition 效果.

0x04 : Present 动画效果

  • presentViewControllerAsSheet
     @IBAction func presentTest(_ sender: Any) {
        1. 创建控制器
          let greenVC = GreenController()
       2. 以AsSheet方式弹出控制器
          presentViewControllerAsSheet(greenVC)
      }
    
AsSheet
  • presentViewControllerAsModalWindow
 @IBAction func presentTest(_ sender: Any) {
        let greenVC = GreenController()
       1. 以AsSheet方式弹出控制器
        presentViewControllerAsModalWindow(greenVC)
    }
AsModalWindow
  • presentViewControllerAsPopover
        let greenVC = GreenController()
       1. 以Popover方式弹出控制器
        presentViewController(greenVC, asPopoverRelativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX, behavior: NSPopover.Behavior.transient)
Jul-28-2018 20-56-14.gif

0x05 Present 自定义动画( 划重点)

  1. 自定义一个遵守NSViewControllerPresentationAnimator 协议的对象
  2. 实现NSViewControllerPresentationAnimator的两个方法

public protocol NSViewControllerPresentationAnimator : NSObjectProtocol {
1. present 动画时,执行这个方法,因此在这个方法中实现自定义的动画效果
    public func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController)

2. dismiss动画时,执行这个方法 ,在这个方法中可以实在自定义的动画效果
    public func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController)
}
  1. 在需要执行Present的地方调用presentViewController(ViewController, animator: )
class PresentAnimator: NSObject {
  
}
// MARK:  NSViewControllerPresentationAnimator
extension PresentAnimator: NSViewControllerPresentationAnimator{
    func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) {
        // 这里实现present的动画效果
        /**viewController: 将要被present出来的视图控制器, fromViewcontroller --> presented动作 ---> viewController */
        1. 获取容器view
        let containerView = fromViewController.view
        
        2. 计算最终显示的frame
        let finalFrame = NSInsetRect(containerView.bounds, 50, 50)
        3. 需要显示的view
        let modalView = viewController.view
        
        4. 设置将要显示视图的初始frame
        modalView.frame = finalFrame
        modalView.setFrameOrigin(NSMakePoint(finalFrame.origin.x, finalFrame.origin.y - 200))

        5 .添加视图到容器视图中
        containerView.addSubview(modalView)

        6. 执行动画效果
        NSAnimationContext.runAnimationGroup({ (animationContext) in
            animationContext.duration = 0.5
            modalView.animator().frame = finalFrame
            
        }, completionHandler: nil)    
    }
    
    func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) {
        // 这里实现dismiss时的动画效果
        1. 获取开始动画的frame
        let startFrame = viewController.view.frame
        2. 执行动画
        NSAnimationContext.runAnimationGroup({ (animationContext) in
            animationContext.duration = 0.5
            viewController.view.animator().setFrameOrigin(NSMakePoint(startFrame.origin.x, startFrame.origin.y - fromViewController.view.bounds.size.height - 100))
        }) {
       3. 动画完成后,移除子视图
            viewController.view.removeFromSuperview()
        }
    }
}
  • 示例效果:
自定义present 动画效果

Summary(总结)

  1. macOS中,控制器的转场切换无论是presentViewController方式或者transition方式,本质上都是将要显示的控制器视图View,通过addSubView方法添加到容器视图中展示.

  2. 通常开发中如果没有特殊需求,transition的系统样式基本都可以满足使用.

  3. 自定义present 动画时,需要注意事件穿透问题:

    • 由于显示出来的控制器视图(Controller View)是通过addSubView方式添加到容器视图中,因此在控制器视图(Controller View)上进行点击操作,可能会触发容器视图中控件(比如按钮)的方法

    • 解决办法: 给容器视图添加一层背景视图(自定义的NSView, 重写mouseDown方法即可),通过背景视图屏蔽鼠标操作,防止事件穿透到容器视图中

腾讯云+社区

为了方便更多同学可以了解到macOS开发相关的内容,我准备授权同步到腾讯云+社区上,希望大家多多支持...
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=30okgs5f2aucw

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容