鍍金池/ 教程/ iOS/ 從 UIKit 到 AppKit
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫(xiě)腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開(kāi)發(fā)
Collection View 動(dòng)畫(huà)
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫(huà)
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫(huà)解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫(huà)
常見(jiàn)的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫(huà)
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺(jué)
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開(kāi)發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

從 UIKit 到 AppKit

Mac 不僅是一個(gè)強(qiáng)大的生產(chǎn)平臺(tái),也十分值得你為其開(kāi)發(fā)一些東西。去年我們開(kāi)始構(gòu)建我們的第一款 Mac 應(yīng)用,成功為我們?nèi)粘9ぷ魉诘钠脚_(tái)開(kāi)發(fā)點(diǎn)東西是一次十分美好的體驗(yàn)。但是,和為 iOS 系統(tǒng)開(kāi)發(fā)應(yīng)用相比,在我們了解 Mac 特性的過(guò)程中也遇到了一些困難。這篇文章總結(jié)了我們從這一過(guò)渡中得到的經(jīng)驗(yàn),希望能啟發(fā)你們?nèi)ラ_(kāi)發(fā)自己的第一個(gè) Mac 應(yīng)用。

在這篇文章中,我們假定 OS X Yosemite 為我們默認(rèn)使用的系統(tǒng)。今年,為了融合 iOS 和 OS X,蘋(píng)果站在開(kāi)發(fā)者的角度對(duì) OS X 做出了巨大的改進(jìn)。不過(guò),我們會(huì)指出哪些特性僅適用于 Yosemite,而哪些特性也適用于之前的系統(tǒng)版本。

相似點(diǎn)

盡管 iOS 和 OS X 是兩個(gè)獨(dú)立的系統(tǒng),它們卻有很多共性。先就開(kāi)發(fā)環(huán)境而言,它們使用同樣的開(kāi)發(fā)語(yǔ)言,同樣的IDE。所以你會(huì)對(duì)這一切都感到非常熟悉。

更重要的是,OS X 和你已經(jīng)熟悉的 iOS 共用許多框架,像 Foundation,Core Data 和 Core Animation。今年,Apple 進(jìn)一步整合兩個(gè)平臺(tái),并給 Mac 帶來(lái)了一些之前僅能在 iOS 上面使用的框架,其中一個(gè)例子就是 Multipeer Connectivity。在更底層的地方,你立刻可以看到你熟悉的 API:Core Graphics,Core Text,libdispatch 等等。

真正開(kāi)始有區(qū)別的是 UI 框架 — AppKit 早在 NeXT 時(shí)代就已面世并不斷進(jìn)化,而 UIKit 就像是簡(jiǎn)約版及現(xiàn)代版的 AppKit。出現(xiàn)這種情況的原因,是當(dāng) Apple 推出 iPhone 時(shí)可以從頭開(kāi)始,并吸取 AppKit 的經(jīng)驗(yàn):把已證實(shí)過(guò)可行的概念和部件拿過(guò)來(lái)用,并改進(jìn)不夠精良的設(shè)計(jì)。

如果你對(duì)這個(gè)轉(zhuǎn)換是怎么發(fā)生的感興趣,請(qǐng)觀看前 Apple iOS 應(yīng)用總監(jiān) Nitin Ganatra 播客上的精彩劇集:System 7 to Carbon,OS X to iOS,以及 iPhone to iPad

考慮到這一點(diǎn),也就不奇怪為什么 UIKit 和 AppKit 仍舊共享許多概念了。UI 是基于 window 和 view 構(gòu)建起來(lái)的,消息像 iOS 一樣通過(guò)響應(yīng)者鏈傳遞。此外,UIViewNSView,UIControlNSControl,UIImageNSImage,UIViewControllerNSViewController,UITextViewNSTextView...這樣的例子不勝枚舉。

看起來(lái)就像你僅需把 UI 前綴替換為 NS 前綴,你就可以用同樣的方法使用這些類。但事實(shí)是在很多情況下這并不奏效。它們?cè)趯?shí)現(xiàn)上并沒(méi)有在概念上那么相似。你在 iOS 上的經(jīng)驗(yàn)至多能幫你大致了解構(gòu)建用戶界面的基礎(chǔ),以及使用很多設(shè)計(jì)模式,比如代理,都是類似的。但是細(xì)節(jié)是魔鬼 — 你真的應(yīng)該通過(guò)閱讀文檔來(lái)學(xué)習(xí)如何使用這些類。

下一節(jié),我們來(lái)看看那些常見(jiàn)的陷阱。

不同點(diǎn)

Window 和 Window Controller

雖然在 iOS 上你幾乎從來(lái)不用與 window 交互(因?yàn)樗鼈冋紦?jù)了整個(gè)屏幕),window 在 Mac 上卻是一個(gè)關(guān)鍵組件。從歷史上看, Mac 應(yīng)用包含多個(gè) window,每個(gè) window 有其自己的角色,非常類似于 iOS 上面的 view controller。因此, AppKit 有 NSWindowController,它接管很多在 iOS 上你會(huì)在 view controller 里面處理的任務(wù)。view controller 被添加到 AppKit 的時(shí)間并不長(zhǎng),而且直到現(xiàn)在,它們默認(rèn)不接受 action,并且缺失很多生命周期的方法、view controller 容器,以及很多你在 UIKit 中熟悉的特性。

但 AppKit 框架已經(jīng)改變,因?yàn)?Mac 應(yīng)用越來(lái)越依賴于一個(gè)單一的 window。就 OS X 10.10 Yosemite 而言,NSViewController 在許多方面與 UIViewController 類似。它也默認(rèn)是響應(yīng)者鏈中的一環(huán)。但要記住,如果你的 Mac 應(yīng)用需要兼容 OS X 10.9 或更早版本的系統(tǒng),Mac 上的 window controller 更類似于 iOS 上你熟悉的 view controller。正如 Mike Ash 所言,在 Mac 上實(shí)例化窗口的一個(gè)好的模式是:每個(gè)窗口類型對(duì)應(yīng)一個(gè) nib 文件和一個(gè) window controller。

此外,NSWindow 并不像 UIWindow 一樣是一個(gè) view 的子類。相反,每個(gè) window 用 contentView 屬性持有一個(gè)指向其頂層 view 的引用。

響應(yīng)者鏈

如果你在為 OS X 10.9 或者更低版本的系統(tǒng)開(kāi)發(fā),請(qǐng)注意在默認(rèn)情況下 view controller 并不是響應(yīng)者鏈的一環(huán)。相反,事件會(huì)沿著視圖樹(shù)向上傳遞然后直接到達(dá) window 和 window controller。在這種情況下,如果你想在 view controller 處理事件,你需要手動(dòng)把它添加到響應(yīng)者鏈中。

除了在響應(yīng)者鏈方面的不同,AppKit 在 action 的命名方法上還有一個(gè)嚴(yán)格的慣例,一個(gè) action 方法看起來(lái)總是類似這樣子的:

- (void)performAction:(id)sender;

以上方法在 iOS 上面所允許的沒(méi)有參數(shù),或者有一個(gè) sender 和一個(gè) event 參數(shù),而這些變體在 OS X 上面是無(wú)法使用的。此外,控件(譯者注:指 NSControl 及其子類)在 AppKit 中通常對(duì)應(yīng)一個(gè) target 和一個(gè) action,而不像在 iOS 上可以通過(guò) addTarget:action:forControlEvents: 方法為一個(gè)控件關(guān)聯(lián)多個(gè) target-action 對(duì)。

View

因?yàn)闅v史遺留問(wèn)題,Mac 的視圖系統(tǒng)和 iOS 的視圖系統(tǒng)有很大區(qū)別。iOS 上的 view 一開(kāi)始就由 Core Animation layer 驅(qū)動(dòng)。但是 AppKit 比 Core Animation 早出來(lái)了很久,當(dāng) Apple 設(shè)計(jì) AppKit 時(shí),我們現(xiàn)在熟知的 GPU 還沒(méi)有出現(xiàn)。因此,那時(shí)視圖系統(tǒng)相關(guān)的任務(wù)主要靠 CPU 處理。

當(dāng)你要開(kāi)始進(jìn)行 Mac 相關(guān)的開(kāi)發(fā)時(shí),我們強(qiáng)烈推薦你查看 Apple 的 Introduction to View Programming Guide for Cocoa。此外,你還應(yīng)該看一下這兩個(gè)精彩的 WWDC session:Layer-Backed Views: AppKit + Core AnimationOptimizing Drawing and Scrolling。

Layer-Backed View

默認(rèn)情況下,AppKit 的 view 不是由 Core Animation layer 驅(qū)動(dòng)的;AppKit 整合 layer-backing 是 iOS 反哺的結(jié)果。一些在 AppKit 需要做的決定你在 UIKit 從來(lái)不需要關(guān)心。AppKit 區(qū)分 layer-backed view 和 layer-hosting view,可以在每個(gè)視圖樹(shù)的根節(jié)點(diǎn)啟用或者禁用 layer backing。

把窗口的 contentView 的 wantsLayer 屬性設(shè)置為 YES 是啟用 layer backing 最簡(jiǎn)單的方法。這會(huì)導(dǎo)致 window 的視圖樹(shù)中所有的 view 都啟用 layer backing,這樣就沒(méi)必要反復(fù)設(shè)置每個(gè) view 的 wantsLayer 屬性了。這個(gè)操作可以用代碼或者在 Interface Builder 的 View Effects Inspector 面板完成。

和 iOS 相比而言,在 Mac 上你應(yīng)該把 backing layer 看做是一個(gè)實(shí)現(xiàn)細(xì)節(jié)。這意味著你不應(yīng)該和這些 layer 直接交互,因?yàn)?AppKit 才是這些 layer 的擁有者。舉個(gè)例子,在 iOS 上你可以隨意編寫(xiě)這樣的代碼:

self.layer.backgroundColor = [UIColor redColor].CGColor;

但是在 AppKit,你不應(yīng)該直接修改這些 layer。如果想用這種方式和 layer 交互,你還有一步工作要做。重寫(xiě) NSViewwantsUpdateLayer 方法并返回 YES,這能讓你可以改變 layer 的屬性。如果你這樣做,AppKit 將不會(huì)再調(diào)用 view 的 drawRect: 方法。取而代之,你應(yīng)該在 updateLayer 里修改 Layer,這個(gè)方法會(huì)在 view 的更新周期中被調(diào)用。

舉個(gè)例子,你可以用這方法去實(shí)現(xiàn)一個(gè)非常簡(jiǎn)單的有純色背景的 view(沒(méi)錯(cuò),NSView 沒(méi)有 backgroundColor 屬性):

@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

這個(gè)例子的前提是這個(gè) view 的父 view 已經(jīng)為其視圖樹(shù)啟用了 layer backing。另一種可行的實(shí)現(xiàn)則只需要重寫(xiě) drawRect: 方法并在其中繪制背景顏色。

合并 Layer

選擇使用眾多 layer-backed view 會(huì)帶來(lái)巨大的內(nèi)存消耗(每一個(gè) layer 有其自己的 backing store,還有可能和其他 view 的 backing store 重疊)而且會(huì)帶來(lái)潛在的合成這些 layer 的消耗。從 OS X 10.9 開(kāi)始,你可以通過(guò)設(shè)置 canDrawSubviewsIntoLayer 屬性來(lái)讓 AppKit 合并一個(gè)視圖樹(shù)中所有 layer 的內(nèi)容到一個(gè)共有的 layer。如果你不需要單獨(dú)對(duì)一個(gè) view 中的子 view 做動(dòng)畫(huà),這將是一個(gè)很好的選擇。

所有隱式 layer-backed 的子 view(比如,你沒(méi)有顯式地對(duì)這些子 view 設(shè)置 wantsLayer = YES)現(xiàn)在將會(huì)被繪制到同一個(gè) layer 中。不過(guò),wantsLayer 設(shè)置為 YES 的子 view 仍然持有它們自己的 backing layer, 而且不管 wantsUpdateLayer 返回什么,它們的 drawRect: 方法仍然會(huì)被調(diào)用。

Layer 重繪策略

另外一個(gè)需要注意的地方:layer-backed view 會(huì)默認(rèn)設(shè)置重繪策略為 NSViewLayerContentsRedrawDuringViewResize。在行為上,這個(gè)非 layer-backed view 是類似的,不過(guò)如果動(dòng)畫(huà)的每一幀都引入一個(gè)繪制步驟的話可能會(huì)對(duì)動(dòng)畫(huà)的性能造成不利影響。

為了避免這個(gè)問(wèn)題,你可以把 layerContentsRedrawPolicy 屬性設(shè)置為 NSViewLayerContentsRedrawOnSetNeedsDisplay 。這樣子的話,便由你來(lái)決定 layer 的內(nèi)容何時(shí)需要重繪。幀的改變將不再自動(dòng)觸發(fā)重繪;現(xiàn)在你要負(fù)責(zé)調(diào)用 -setNeedsDisplay: 來(lái)觸發(fā)重繪操作。

一旦你這樣更改了重繪策略,你也許會(huì)想了解下 view 中和 layer 的 contentGravity 屬性等價(jià)的 layerContentsPlacement 屬性。這個(gè)屬性允許你指定在調(diào)整大小的時(shí)候當(dāng)前的 layer 內(nèi)容該怎么映射到 layer 上。

Layer-Hosting View

NSView 的 layer 故事并沒(méi)有完結(jié)。你可以用另一種完全不一樣的方式來(lái)使用 Core Animation layer — 稱為 layer-hosting view。簡(jiǎn)單來(lái)說(shuō),你可以對(duì)一個(gè) layer-hosting view 的 layer 及其子 layer 做任何操作,代價(jià)是你再也不能給該 view 添加任何子 view。layer-hosting view 是視圖樹(shù)中的葉子節(jié)點(diǎn)。

要?jiǎng)?chuàng)建一個(gè) layer-hosting view,你首先要為 view 的 layer 屬性分配一個(gè) layer 對(duì)象,然后把 wantsLayer 設(shè)置為 YES。注意,這些步驟的順序是非常關(guān)鍵的:

- (instancetype)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.layer = [[CALayer alloc] init];
        self.wantsLayer = YES;
    }
}

在你設(shè)置了你自定義的 layer 之后才設(shè)置 wantsLayer 是非常重要的。

其他與 View 相關(guān)的陷阱

默認(rèn)情況下,Mac 上視圖的坐標(biāo)系統(tǒng)原點(diǎn)位于左下角,而不是像 iOS 的左上角。剛開(kāi)始這可能會(huì)讓人混亂,不過(guò)你可以通過(guò)重寫(xiě) isFlipped 并返回 YES 來(lái)恢復(fù)到你熟悉的左上角。

由于 AppKit 中的 view 沒(méi)有背景顏色屬性可以讓你直接設(shè)置為 [NSColor clearColor] 來(lái)讓其變得透明,許多 NSView 的子類比如 NSTextViewNSScrollView 開(kāi)放了一個(gè) drawsBackground 屬性,如果你想讓這一類 view 透明,你必須設(shè)置該屬性為 NO。

為了能接收光標(biāo)進(jìn)出一個(gè) view 或者在 view 里面移動(dòng)的事件,你需要?jiǎng)?chuàng)建一個(gè)追蹤區(qū)域。你可以在 NSView 中指定的 updateTrackingAreas 方法中來(lái)做這件事情。一個(gè)通用的寫(xiě)法看起來(lái)是這樣子的:

- (void)updateTrackingAreas
{
    [self removeTrackingArea:self.trackingArea];
    self.trackingArea = [[NSTrackingArea alloc] initWithRect:CGRectZero 
                                                     options:NSTrackingMouseEnteredAndExited|NSTrackingInVisibleRect|NSTrackingActiveInActiveApp
                                                       owner:self 
                                                    userInfo:nil];
    [self addTrackingArea:self.trackingArea];
}

AppKit 的控件之前是由 NSCell 的子類驅(qū)動(dòng)的。不要混淆這些 cell 和 UIKit 里 table view 的 cell 及 collection view 的 cell。AppKit 最初區(qū)分 view 和 cell 是為了節(jié)省資源 - view 可以把所有的繪制工作代理給更輕量級(jí)的可以被所有同類型的 view 重用的 cell 對(duì)象。

Apple 正在一步步地拋棄這樣的實(shí)現(xiàn)方法了,但是你還是會(huì)時(shí)不時(shí)碰到這樣的問(wèn)題。舉個(gè)例子,如果你想創(chuàng)建一個(gè)自定義的按鈕,你首先要繼承 NSButton NSButtonCell,然后在這個(gè) cell 子類里面進(jìn)行你自定義的繪制,然后通過(guò)重寫(xiě) +[NSControl cellClass] 方法告訴自定義按鈕使用你的 cell 子類。

最后,如果你想知道在你自己的 drawRect: 方法里怎么獲取當(dāng)前的 Core Graphics 上下文,答案是 NSGraphicsContextgraphicsPort 屬性。詳細(xì)內(nèi)容請(qǐng)查看 Cocoa Drawing Guide

動(dòng)畫(huà)

歸結(jié)于上面提到的視圖系統(tǒng)的差異,動(dòng)畫(huà)在 Mac 上的運(yùn)作方式也十分不同。想要一個(gè)好的概述,請(qǐng)觀看 WWDC session:Best Practices for Cocoa Animation

如果你的 view 不是由 layer 驅(qū)動(dòng)的,那你的動(dòng)畫(huà)自然是完全由 CPU 處理,這意味著動(dòng)畫(huà)的每一步都必須相應(yīng)地繪制到 window-backing store 上。因?yàn)楝F(xiàn)今我們主要是對(duì) layer-backed view 做動(dòng)畫(huà)以獲得流暢的動(dòng)畫(huà)效果,所以我們?cè)谶@兒就專注于這種情況。

正如上面說(shuō)的,在 AppKit 中你不應(yīng)該修改 layer-backed view 中的 layer (看 Core Animation Programming Guide 這篇文檔底部 “Rules for Modifying Layers in OS X” 那一節(jié))。這些 layer 由 AppKit 管理,而且和 iOS 相反,view 的幾何屬性并不僅僅是對(duì)應(yīng)的 layer 的幾何屬性的映射,但 AppKit 卻會(huì)把 view 內(nèi)部的幾何屬性同步到 layer。

你可以用幾種不同的方法對(duì)一個(gè) view 進(jìn)行動(dòng)畫(huà)。第一種,你可以使用 animator proxy:

view.animator.alphaValue = .5;

在幕后,這句代碼會(huì)啟用 layer 的隱式動(dòng)畫(huà),設(shè)置其透明度,然后再次禁用 layer 的隱式動(dòng)畫(huà)。

你還可以把這句代碼封裝到一個(gè) animation context 中,這樣你就能得到它的結(jié)束回調(diào):

[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
    view.animator.alphaValue = .5;
} completionHandler:^{
    // ...
}]; 

如果想改變持續(xù)時(shí)間和緩動(dòng)類型,我們必須對(duì)其動(dòng)畫(huà)上下文進(jìn)行設(shè)置:

[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
    context.duration = 1;
    context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    view.animator.alphaValue = .5;
} completionHandler:^{
    // ...
}]; 

如果你不需要結(jié)束回調(diào),你可以用這種簡(jiǎn)化形式:

[NSAnimationContext currentContext].duration = 1;
view.animator.alphaValue = .5;    

最后,你可以啟用隱式動(dòng)畫(huà),這樣你就不必每次都明確地使用 animator proxy 了:

[NSAnimationContext currentContext].allowsImplicitAnimations = YES;
view.alphaValue = .5;

要更全面地控制動(dòng)畫(huà),你可以使用 CAAnimation 實(shí)例。和 iOS 相反,你不能直接把它們加到 layer 上(因?yàn)?layer 不應(yīng)該由你來(lái)修改),不過(guò)你可以使用 NSAnimatablePropertyContainer 協(xié)議中定義的 API,NSViewNSWindow 已經(jīng)實(shí)現(xiàn)了該協(xié)議。舉個(gè)例子:

CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.values = @[@1, @.9, @.8, @.7, @.6];
view.animations = @{@"alphaValue": animation};
view.animator.alphaValue = .5;

對(duì)于動(dòng)畫(huà)來(lái)說(shuō),把 view 的 layerContentsRedrawPolicy 設(shè)置為 NSViewLayerContentsRedrawOnSetNeedsDisplay 是非常重要的,不然的話 view 的內(nèi)容在每一幀都會(huì)被重繪。

很遺憾,NSView 沒(méi)有開(kāi)放 Core Animation layer 所有可以進(jìn)行動(dòng)畫(huà)的屬性,transform 是其中最重要的例子??纯?Jonathan Willings這篇文章,它描述了你可以如何解決這些限制。不過(guò)注意,文章中的解決方案是不受官方支持的。

上面提到的所有東西都適用于 layer-backed view。對(duì)于 layer-hosting view 來(lái)說(shuō),你可以直接對(duì) view 的 layer 或者子 layer 使用 CAAnimations,因?yàn)槟銚碛兴鼈兊目刂茩?quán)。

Collection View

盡管 AppKit 有 NSCollectionView 類,它的功能卻比 UIKit 里對(duì)應(yīng)的類滯后很多。鑒于 UICollectionView 是 iOS 上一個(gè)如此多功能的控件(當(dāng)然,這取決于你的 UI 觀念),AppKit 里對(duì)應(yīng)的控件一點(diǎn)都不像它這件事相當(dāng)難以忍受。所以當(dāng)你要規(guī)劃你的用戶界面的時(shí)候,要考慮構(gòu)建一個(gè)網(wǎng)格布局有可能會(huì)非常麻煩,相反,在 iOS 上這很容易實(shí)現(xiàn)。

圖像

來(lái)自 iOS 的你對(duì) UIImage 肯定非常熟悉,正巧,AppKit 也有一個(gè)對(duì)應(yīng)的 NSImage 類。不過(guò)很快你就會(huì)意識(shí)到這兩個(gè)類簡(jiǎn)直是天差地別。從很多方面來(lái)說(shuō),NSImage 都比 UIImage 強(qiáng)大很多,但這是建立在復(fù)雜性增加的代價(jià)上的。Apple 的 Cocoa Drawing Guide 很好地介紹了如何使用 AppKit 中的圖像。

概念上最重要的不同是 NSImage 由一個(gè)或者多個(gè)圖像表示(image representation,譯者注:這里的圖像表示為名詞,可以參考百度百科,本節(jié)下同)驅(qū)動(dòng),這些圖像表示在 AppKit 表現(xiàn)為一些 NSImageRep 的子類,像 NSBitmapImageRep,NSPDFImageRepNSEPSImageRep。舉個(gè)例子,一個(gè) NSImage 對(duì)象為了打印同樣的內(nèi)容可以持有縮略圖,全尺寸和 PDF 三個(gè)圖像表示。當(dāng)你繪制圖像時(shí),圖像表示會(huì)匹配當(dāng)前的圖形上下文,而繪圖尺寸會(huì)根據(jù)顏色空間,維度,分辨率以及繪圖深度得出。

此外,Mac 上的圖像除了尺寸還有分辨率的概念。圖像表示的分辨率由三個(gè)屬性構(gòu)成:size,pixelsWide 以及 pixelsHigh。size 屬性決定了圖像表示被渲染時(shí)的尺寸,而 pixelsWide 和 pixelsHigh 指定了源于圖像數(shù)據(jù)的原始尺寸。這三個(gè)屬性共同決定了圖像表示的分辨率。像素尺寸可以和圖像表示的尺寸不一樣,正如圖像表示的尺寸可以和它所屬的圖片的尺寸不一樣。

另外一個(gè)和 UIImage 不一樣的地方是當(dāng)它被繪制到屏幕上時(shí) NSImage 會(huì)緩存繪制結(jié)果(可以通過(guò) cacheMode 屬性配置)。當(dāng)你改變底層的圖像表示,你必須對(duì)圖像調(diào)用 recache 才能使其生效。

不過(guò)在 Mac 上面處理圖像并不總是比 iOS 復(fù)雜。NSImage 提供了一個(gè)很簡(jiǎn)單的方法去繪制一個(gè)新圖像,而在 iOS 上,你需要?jiǎng)?chuàng)建一個(gè)位圖上下文,然后用位圖上下文創(chuàng)建 CGImage,最終用該 CGImage 初始化一個(gè) UIImage 實(shí)例。用 NSImage 你僅需:

[NSImage imageWithSize:(NSSize)size 
            flipped:(BOOL)drawingHandlerShouldBeCalledWithFlippedContext 
     drawingHandler:^BOOL (NSRect dstRect) 
{
    // your drawing commands here...
}];

顏色

Mac 支持完全的 color-calibrated 工作流,所有跟顏色相關(guān)的任何東西都有可能變得更復(fù)雜。顏色管理是一個(gè)復(fù)雜的主題,我們也不精通這方面的東西。所以,我們希望你看看 Apple 關(guān)于這方面的指南: Introduction to Color Programming Topics for CocoaIntroduction to Color Management。

你經(jīng)常需要在你的應(yīng)用里使用一個(gè)你的設(shè)計(jì)師給你指定的顏色。要取得正確的顏色,設(shè)計(jì)模板使用的顏色空間和你以編程方式指定的顏色空間保持一致是非常重要的。系統(tǒng)標(biāo)準(zhǔn)的顏色選擇器有一個(gè)下拉菜單,你可以在這里選擇你想要的顏色空間。我們建議使用 device-independent sRGB 顏色空間,然后在代碼里面用 +[NSColor colorWithSRGBRed:green:blue:alpha:] 類方法來(lái)創(chuàng)建顏色。

http://wiki.jikexueyuan.com/project/objc/images/14-4.png" alt="" />

文字系統(tǒng)

有了 TextKit,iOS 7 終于有了和 Mac 上早就有了的 Cocoa Text System 等效的東西。但 Apple 并不僅僅是把文字系統(tǒng)從 Mac 上轉(zhuǎn)移到 iOS;相反,Apple 對(duì)其做了些顯著的改變。

舉個(gè)例子,AppKit 開(kāi)放 NSTypesetterNSGlyphGenerator,你可以通過(guò)繼承這兩者來(lái)自定義它們的一些特性。iOS 并不開(kāi)放這些類,但是你可以通過(guò) NSLayoutManagerDelegate 協(xié)議達(dá)到定制的目的。

總體來(lái)說(shuō),兩個(gè)平臺(tái)的文字系統(tǒng)還是非常相似的,所有你在 iOS 上能做的在 Mac 上都可以做(甚至更多),但對(duì)于一些東西,你必須從不同的地方尋找合適的方法實(shí)現(xiàn)。

沙盒

符合沙盒機(jī)制的 Mac 應(yīng)用才能通過(guò) Mac App Store 銷售。鑒于沙盒從一開(kāi)始就是 iOS 的基本規(guī)范(所以你會(huì)對(duì)它非常熟悉),你可能會(huì)好奇我們?yōu)槭裁匆谶@里提起它。然而,我們已經(jīng)習(xí)慣了沙盒機(jī)制還沒(méi)出現(xiàn)之前的 Mac 開(kāi)發(fā)環(huán)境,所以有時(shí)候會(huì)忽視一些你想要實(shí)現(xiàn)的功能會(huì)和沙盒的限制出現(xiàn)沖突。

Mac 的文件系統(tǒng)是一直對(duì)用戶開(kāi)放的,所以如果用戶明確表示,沙盒應(yīng)用可以訪問(wèn)自身應(yīng)用外的文件。同樣的機(jī)制同時(shí)引進(jìn)了 iOS 8。不過(guò),和通過(guò)這種方式放寬對(duì) iOS 的限制相反,它卻加強(qiáng)了對(duì) Mac 的限制。這讓它容易被忽視和遺忘。

對(duì)此我們也十分慚愧,所以希望能阻止你犯同樣的錯(cuò)誤。當(dāng)我們開(kāi)始開(kāi)發(fā) Deckset — 一款把簡(jiǎn)單 Markdown 文件轉(zhuǎn)換為演示幻燈片的應(yīng)用 — 時(shí),我們從來(lái)沒(méi)想過(guò)我們會(huì)碰到什么關(guān)于沙盒的問(wèn)題。畢竟,我們只需要讀 Markdown 文件的權(quán)限。

我們忘記了我們還要顯示 Markdown 文件中引用的圖片。盡管你在 Markdown 文件中輸入了圖片文件的路徑,但沙盒系統(tǒng)并不認(rèn)為這是用戶的意圖。最后,我們通過(guò)一個(gè)像通知中心一樣的 UI 來(lái)提示用戶授權(quán)我們?cè)L問(wèn) Markdown 文件中的所有圖片‘解決’了該問(wèn)題。

及早看一下 Apple 的 sandboxing guides 以防以后在相關(guān)的問(wèn)題上犯錯(cuò)誤。

獨(dú)有特性

有很多事情你只能在 Mac 上做,這主要是因?yàn)樗煌慕换ツP秃退鼮閷捤傻陌踩呗浴T诒酒谠掝}中,我們有一些文章深入探討了其中的一些內(nèi)容:進(jìn)程間通訊,使 Mac 應(yīng)用腳本化, 在沙盒中腳本化其他應(yīng)用, 為你的應(yīng)用構(gòu)建插件

當(dāng)然,這只是 Mac 獨(dú)有特性中很小的一部分,但這給了你一個(gè)很好的視角看待 iOS 8 從頭開(kāi)始打造其可擴(kuò)展性和 app 間通訊。最后,還有很多東西等待你去探索:Drag and Drop,Printing,Bindings,OpenCL 等等,這里僅僅是舉幾個(gè)例子。