从UIKit到AppKit
文章目录
【注意】最后更新于 March 3, 2017,文中内容可能已过时,请谨慎使用。
不同点
NSWindowController
在Mac上应用都支持多窗口(NSWindowController),AppKit 中都有 NSWindowController担当着类似在iOS中的view controller处理的任务。
window在iOS占据整个屏幕,几乎不怎么不用。
NSViewController
AppKit 中的 NSViewController默认不支持交互,缺少生命周期相关方法和UIKit中熟悉的特性。
但在OS X 10.10 Yosemite之后,NSViewController改进很多,默认支持交互中的响应链。
NSWindow 和 UIWindow
在UIKit中UIWindow是一个 view 的子类.
在AppKit中NSWindow用 contentView 属性持有一个指向其顶层 view 的引用。
响应者链(responder chain)
如果你在为 OS X 10.9 或者更低版本的系统开发,请注意在默认情况下view controller 并不是响应者链的一环。相反,事件会沿着视图树向上传递然后直接到达 window 和 window controller。在这种情况下,如果你想在 view controller 处理事件,你需要手动把它添加到响应者链中。
Target-Action消息传递方式
Target-Action 是回应 UI 事件时典型的消息传递方式。iOS 上的 UIControl 和 Mac 上的 NSControl/NSCell 都支持这个机制。
Target-Action 在消息的发送者和接收者之间建立了一个松散的关系。消息的接收者不知道发送者,甚至消息的发送者也不知道消息的接收者会是什么。如果 target 是 nil,action 会在响应链 (responder chain) 中被传递下去,直到找到一个响应它的对象。
传递机制的局限
基于 target-action 传递机制的一个局限是,发送的消息不能携带自定义的信息:
- 在
iOS中,可以选择性的把发送者和触发action的事件作为参数。 - 在
Mac平台上action方法的第一个参数永远是发送者,否则将不视为无效方法。 在AppKit唯一有效的action方法声明方式: {% codeblock lang:objc %}
- (void)performAction:(id)sender; {% endcodeblock %}
控件关联Action事件的区别
iOS 上的 UIControl 和 Mac 上的 NSControl/NSCell 都支持这个机制。
- 在
iOS中,每个控件可以通过addTarget:action:forControlEvents:和多个target-action关联。 - 在
AppKit中通常一个控件仅对应一个target-action对。
View视图系统
因为历史遗留问题,Mac 的视图系统和 iOS的视图系统有很大区别。
- 绘图处理器差异
iOS由Core Animation layer驱动,使用GPU处理。AppKit视图系统相关的任务主要靠CPU处理,因为在AppKit设计之初,GPU还没有诞生。 Mac开发参考资料: Introduction to View Programming Guide for Cocoa WWDC session:Layer-Backed Views: AppKit + Core Animation Optimizing Drawing and Scrolling
Layer-Backed View: iOS反哺AppKit层支持视图
iOS反哺AppKit默认情况下,AppKit的view不是由Core Animation layer驱动的;AppKit整合layer-backing是iOS反哺的结果。
layer backing启用/禁用:wantsLayer
AppKit 区分 layer-backed view 和 layer-hosting view,可以在每个视图树的根节点启用或者禁用 layer backing。
启用 layer backing
- 方法一:把窗口的
contentView的wantsLayer属性设置为YES - 方法二:在
Interface Builder的View Effects Inspector面板完成 这会导致window的视图树中所有的view都启用layer backing,这样就没必要反复设置每个view的wantsLayer属性了。
修改layer属性
在AppKit上开启layer backing之后,layer的拥有者是AppKit,这就意味着不能直接编辑 layer的属性。
在 iOS 上可以直接编辑:
{% codeblock lang:objc %}
self.layer.backgroundColor = [UIColor redColor].CGColor;
{% endcodeblock %}
在 AppKit编辑layer属性需要以下步骤:
- 重写
NSView的wantsUpdateLayer方法并返回YES,这能让你可以改变layer的属性。 这样以来,在view更新周期中,将不会再调用view的drawRect:方法。取而代之,调用updateLayer方法来更新Layer。 举个例子,用这方法去实现一个非常简单的有纯色背景的view(没错,NSView没有backgroundColor属性): 这个例子的前提是这个view的父 view已经为其视图树启用了layer backing。 另一种可行的实现则只需要重写drawRect:方法并在其中绘制背景颜色。 {% codeblock lang:objc %} @interface ColoredView: NSView @property (nonatomic) NSColor *backgroundColor; @end
@implementation ColoredView
(BOOL)wantsUpdateLayer { return YES; }
(void)updateLayer { self.layer.backgroundColor = self.backgroundColor.CGColor; }
(void)setBackgroundColor:(NSColor *)backgroundColor { _backgroundColor = backgroundColor; [self setNeedsDisplay:YES]; } @end {% endcodeblock %}
合并 Layer
当APP启动过多的layer-backed view 会带来巨大的内存消耗(每一个 layer 有其自己的 backing store,还有可能和其他 view 的 backing store 重叠)而且会带来潜在的合成这些 layer 的消耗。
canDrawSubviewsIntoLayer合并Layer
从 OS X 10.9 开始,如果不单独对一个 view 中的子 view 做动画,可以通过设置 canDrawSubviewsIntoLayer 属性来让 AppKit 合并一个视图树中所有 layer 的内容到一个共有的 layer。
隐式layer-backed合并Layer
所有隐式 layer-backed 的子 view(比如,没有显式地对这些子 view 设置 wantsLayer = YES)现在将会被绘制到同一个 layer 中。不过wantsLayer 设置为 YES 的子 view 仍然持有它们自己的 backing layer, 而且不管 wantsUpdateLayer 返回什么,它们的 drawRect: 方法仍然会被调用。
Layer 重绘策略
layer-backed view 默认的自动重绘策略
layer-backed view 会默认设置重绘策略为 NSViewLayerContentsRedrawDuringViewResize。在行为上,这个非 layer-backed view 是类似的,不过如果动画的每一帧都引入一个绘制步骤的话可能会对动画的性能造成不利影响。
设置layer-backed view手动重绘策略
- 设置手动重绘策略:把
layerContentsRedrawPolicy属性设置为NSViewLayerContentsRedrawOnSetNeedsDisplay - 实现手动重绘操作:调用
-setNeedsDisplay:方法来触发重绘操作 这样便由你来决定layer的内容何时需要重绘。帧的改变将不再自动触发重绘。
设置view的属性来重绘Layer
- view中
layerContentsPlacement属性:等价layer中的contentGravity属性。 这个属性允许你指定在调整大小的时候当前的layer内容该怎么映射到layer上。
Layer-Hosting View:使用 Core Animation layer
layer-hosting view 是视图树中的叶子节点,使用这种模式可以对layer 及其子 layer 做任何操作,代价是你再也不能给该 view 添加任何子 view。
创建
- 为
view的layer属性分配一个layer 对象, - 设置
wantsLayer为YES这些步骤的顺序是非常关键: {% codeblock lang:swift %}
- (instancetype)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layer = [[CALayer alloc] init];
self.wantsLayer = YES;
}
}
{% endcodeblock %}
在你设置了自定义的
layer之后,再设置wantsLayer是非常重要的。
其他与 View 相关的陷阱
坐标系统原点设置左下/左上角
通过重写isFlipped 并返回 YES 来恢复到你熟悉的左上角。
View背景颜色属性drawsBackground
由于 AppKit 中的 view 没有背景颜色属性可以让你直接设置为 [NSColor clearColor] 来让其变得透明,许多 NSView 的子类比如 NSTextView 和 NSScrollView 开放了一个 drawsBackground 属性,如果你想让这一类 view 透明,你必须设置该属性为 NO。
设置光标追踪区域
为了能接收光标进出一个 view 或者在 view 里面移动的事件,你需要创建一个追踪区域。你可以在 NSView 中指定的 updateTrackingAreas 方法中来做这件事情。一个通用的写法看起来是这样子的:
{% codeblock lang:swift %}
- (void)updateTrackingAreas { [self removeTrackingArea:self.trackingArea]; self.trackingArea = [[NSTrackingArea alloc] initWithRect:CGRectZero options:NSTrackingMouseEnteredAndExited|NSTrackingInVisibleRect|NSTrackingActiveInActiveApp owner:self userInfo:nil]; [self addTrackingArea:self.trackingArea]; } {% endcodeblock %}
NSCell困惑
AppKit 的控件之前是由 NSCell 的子类驱动的,可以被所有同类型的控件重用。
AppKit 最初区分 view 和 cell 是为了节省资源 - view 可以把所有的绘制工作代理给更轻量级的可以被所有同类型的 view 重用的 cell 对象。
不要混淆这些 cell 和 UIKit 里 table view 的 cell 及 collection view 的 cell。
自定义一个按钮控件
Apple 正在一步步地抛弃这样的实现方法了,但是你还是会时不时碰到这样的问题。 举个例子,如果你想创建一个自定义的按钮,
- 首先要继承
NSButton和NSButtonCell - 然后在这个
cell 子类里面进行你自定义的绘制, - 然后通过重写
+[NSControl cellClass]方法告诉自定义按钮使用你的cell 子类
获取 Core Graphics 上下文
最后,如果你想知道在你自己的 drawRect: 方法里怎么获取当前的 Core Graphics 上下文,答案是 NSGraphicsContext 的 graphicsPort 属性。详细内容请查看 Cocoa Drawing Guide。
动画
如果你的 view 不是由 layer 驱动的,那你的动画自然是完全由 CPU 处理,这意味着动画的每一步都必须相应地绘制到 window-backing store 上。
对 layer-backed view做动画
正如上面说的,在 AppKit 中,这些 layer 由 AppKit 管理,你不应该修改 layer-backed view 中的 layer。
几何属性
与iOS 相反,view 的几何属性并不仅仅是对应的 layer 的几何属性的映射,但 AppKit 却会把 view 内部的几何属性同步到 layer。
的animator proxy动画
{% codeblock lang:objc %}
view.animator.alphaValue = .5;
{% endcodeblock %}
在幕后,这句代码会启用 layer 的隐式动画,设置其透明度,然后再次禁用 layer 的隐式动画。
NSAnimationContext动画
结束回调 {% codeblock lang:objc %} [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
//持续时间 context.duration = 1;
//缓动类型 context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; view.animator.alphaValue = .5;
} completionHandler:^{ // … }]; {% endcodeblock %} 2. 无结束回调 {% codeblock lang:objc %} [NSAnimationContext currentContext].duration = 1; view.animator.alphaValue = .5; {% endcodeblock %}
启用隐式动画
{% codeblock lang:objc %} [NSAnimationContext currentContext].allowsImplicitAnimations = YES; view.alphaValue = .5; {% endcodeblock %}
CAAnimations控制动画
使用 CAAnimation 实例更全面地控制动画。和 iOS 相反,你不能直接把它们加到 layer 上(因为 layer 不应该由你来修改),不过你可以使用 NSAnimatablePropertyContainer 协议中定义的 API,NSView 和 NSWindow 已经实现了该协议。
{% codeblock lang:objc %}
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.values = @[@1, @.9, @.8, @.7, @.6];
view.animations = @{@“alphaValue”: animation};
view.animator.alphaValue = .5;
{% endcodeblock %}
帧动画
对于帧动画来说,把 view 的 layerContentsRedrawPolicy 设置为 NSViewLayerContentsRedrawOnSetNeedsDisplay 是非常重要的,不然的话 view 的内容在每一帧都会被重绘。
很遗憾,NSView 没有开放 Core Animation layer 所有可以进行动画的属性,transform 是其中最重要的例子。看看 Jonathan Willings 的这篇文章,它描述了你可以如何解决这些限制。不过注意,文章中的解决方案是不受官方支持的。
上面提到的所有东西都适用于 layer-backed view。对于 layer-hosting view 来说,你可以直接对 view 的 layer 或者子 layer 使用 CAAnimations,因为你拥有它们的控制权。
文字系统
有了 TextKit,iOS 7 终于有了和 Mac 上早就有了的 Cocoa Text System 等效的东西。但 Apple 并不仅仅是把文字系统从 Mac 上转移到 iOS;相反,Apple 对其做了些显著的改变。
举个例子,AppKit 开放 NSTypesetter 和 NSGlyphGenerator,你可以通过继承这两者来自定义它们的一些特性。iOS 并不开放这些类,但是你可以通过 NSLayoutManagerDelegate 协议达到定制的目的。
总体来说,两个平台的文字系统还是非常相似的,所有你在 iOS 上能做的在 Mac 上都可以做(甚至更多),但对于一些东西,你必须从不同的地方寻找合适的方法实现。
沙盒
符合沙盒机制的 Mac 应用才能通过 Mac App Store 销售。然而,我们已经习惯了沙盒机制还没出现之前的 Mac 开发环境,所以有时候会忽视一些你想要实现的功能会和沙盒的限制出现冲突。
管理Mac应用对沙盒支持:
独有特性
有很多事情你只能在 Mac 上做,这主要是因为它不同的交互模型和它更为宽松的安全策略。在本期话题中,我们有一些文章深入探讨了其中的一些内容:进程间通讯,使 Mac 应用脚本化,在沙盒中脚本化其他应用 , 为你的应用构建插件。 当然,这只是 Mac 独有特性中很小的一部分,但这给了你一个很好的视角看待 iOS 8 从头开始打造其可扩展性和 app 间通讯。最后,还有很多东西等待你去探索:Drag and Drop,Printing,Bindings,OpenCL 等等,这里仅仅是举几个例子。
文章作者 iTBoyer
上次更新 2017-03-03