Mac 不僅是一個強大的生產平臺,也十分值得你為其開發(fā)一些東西。去年我們開始構建我們的第一款 Mac 應用,成功為我們日常工作所在的平臺開發(fā)點東西是一次十分美好的體驗。但是,和為 iOS 系統(tǒng)開發(fā)應用相比,在我們了解 Mac 特性的過程中也遇到了一些困難。這篇文章總結了我們從這一過渡中得到的經驗,希望能啟發(fā)你們去開發(fā)自己的第一個 Mac 應用。
在這篇文章中,我們假定 OS X Yosemite 為我們默認使用的系統(tǒng)。今年,為了融合 iOS 和 OS X,蘋果站在開發(fā)者的角度對 OS X 做出了巨大的改進。不過,我們會指出哪些特性僅適用于 Yosemite,而哪些特性也適用于之前的系統(tǒng)版本。
盡管 iOS 和 OS X 是兩個獨立的系統(tǒng),它們卻有很多共性。先就開發(fā)環(huán)境而言,它們使用同樣的開發(fā)語言,同樣的IDE。所以你會對這一切都感到非常熟悉。
更重要的是,OS X 和你已經熟悉的 iOS 共用許多框架,像 Foundation,Core Data 和 Core Animation。今年,Apple 進一步整合兩個平臺,并給 Mac 帶來了一些之前僅能在 iOS 上面使用的框架,其中一個例子就是 Multipeer Connectivity。在更底層的地方,你立刻可以看到你熟悉的 API:Core Graphics,Core Text,libdispatch 等等。
真正開始有區(qū)別的是 UI 框架 — AppKit 早在 NeXT 時代就已面世并不斷進化,而 UIKit 就像是簡約版及現(xiàn)代版的 AppKit。出現(xiàn)這種情況的原因,是當 Apple 推出 iPhone 時可以從頭開始,并吸取 AppKit 的經驗:把已證實過可行的概念和部件拿過來用,并改進不夠精良的設計。
如果你對這個轉換是怎么發(fā)生的感興趣,請觀看前 Apple iOS 應用總監(jiān) Nitin Ganatra 播客上的精彩劇集:System 7 to Carbon,OS X to iOS,以及 iPhone to iPad。
考慮到這一點,也就不奇怪為什么 UIKit 和 AppKit 仍舊共享許多概念了。UI 是基于 window 和 view 構建起來的,消息像 iOS 一樣通過響應者鏈傳遞。此外,UIView
是 NSView
,UIControl
是 NSControl
,UIImage
是 NSImage
,UIViewController
是 NSViewController
,UITextView
是 NSTextView
...這樣的例子不勝枚舉。
看起來就像你僅需把 UI
前綴替換為 NS
前綴,你就可以用同樣的方法使用這些類。但事實是在很多情況下這并不奏效。它們在實現(xiàn)上并沒有在概念上那么相似。你在 iOS 上的經驗至多能幫你大致了解構建用戶界面的基礎,以及使用很多設計模式,比如代理,都是類似的。但是細節(jié)是魔鬼 — 你真的應該通過閱讀文檔來學習如何使用這些類。
下一節(jié),我們來看看那些常見的陷阱。
雖然在 iOS 上你幾乎從來不用與 window 交互(因為它們占據(jù)了整個屏幕),window 在 Mac 上卻是一個關鍵組件。從歷史上看, Mac 應用包含多個 window,每個 window 有其自己的角色,非常類似于 iOS 上面的 view controller。因此, AppKit 有 NSWindowController
,它接管很多在 iOS 上你會在 view controller 里面處理的任務。view controller 被添加到 AppKit 的時間并不長,而且直到現(xiàn)在,它們默認不接受 action,并且缺失很多生命周期的方法、view controller 容器,以及很多你在 UIKit 中熟悉的特性。
但 AppKit 框架已經改變,因為 Mac 應用越來越依賴于一個單一的 window。就 OS X 10.10 Yosemite 而言,NSViewController
在許多方面與 UIViewController
類似。它也默認是響應者鏈中的一環(huán)。但要記住,如果你的 Mac 應用需要兼容 OS X 10.9 或更早版本的系統(tǒng),Mac 上的 window controller 更類似于 iOS 上你熟悉的 view controller。正如 Mike Ash 所言,在 Mac 上實例化窗口的一個好的模式是:每個窗口類型對應一個 nib 文件和一個 window controller。
此外,NSWindow
并不像 UIWindow
一樣是一個 view 的子類。相反,每個 window 用 contentView
屬性持有一個指向其頂層 view 的引用。
如果你在為 OS X 10.9 或者更低版本的系統(tǒng)開發(fā),請注意在默認情況下 view controller 并不是響應者鏈的一環(huán)。相反,事件會沿著視圖樹向上傳遞然后直接到達 window 和 window controller。在這種情況下,如果你想在 view controller 處理事件,你需要手動把它添加到響應者鏈中。
除了在響應者鏈方面的不同,AppKit 在 action 的命名方法上還有一個嚴格的慣例,一個 action 方法看起來總是類似這樣子的:
- (void)performAction:(id)sender;
以上方法在 iOS 上面所允許的沒有參數(shù),或者有一個 sender 和一個 event 參數(shù),而這些變體在 OS X 上面是無法使用的。此外,控件(譯者注:指 NSControl 及其子類)在 AppKit 中通常對應一個 target 和一個 action,而不像在 iOS 上可以通過 addTarget:action:forControlEvents:
方法為一個控件關聯(lián)多個 target-action 對。
因為歷史遺留問題,Mac 的視圖系統(tǒng)和 iOS 的視圖系統(tǒng)有很大區(qū)別。iOS 上的 view 一開始就由 Core Animation layer 驅動。但是 AppKit 比 Core Animation 早出來了很久,當 Apple 設計 AppKit 時,我們現(xiàn)在熟知的 GPU 還沒有出現(xiàn)。因此,那時視圖系統(tǒng)相關的任務主要靠 CPU 處理。
當你要開始進行 Mac 相關的開發(fā)時,我們強烈推薦你查看 Apple 的 Introduction to View Programming Guide for Cocoa。此外,你還應該看一下這兩個精彩的 WWDC session:Layer-Backed Views: AppKit + Core Animation 和 Optimizing Drawing and Scrolling。
默認情況下,AppKit 的 view 不是由 Core Animation layer 驅動的;AppKit 整合 layer-backing 是 iOS 反哺的結果。一些在 AppKit 需要做的決定你在 UIKit 從來不需要關心。AppKit 區(qū)分 layer-backed view 和 layer-hosting view,可以在每個視圖樹的根節(jié)點啟用或者禁用 layer backing。
把窗口的 contentView 的 wantsLayer
屬性設置為 YES
是啟用 layer backing 最簡單的方法。這會導致 window 的視圖樹中所有的 view 都啟用 layer backing,這樣就沒必要反復設置每個 view 的 wantsLayer 屬性了。這個操作可以用代碼或者在 Interface Builder 的 View Effects Inspector 面板完成。
和 iOS 相比而言,在 Mac 上你應該把 backing layer 看做是一個實現(xiàn)細節(jié)。這意味著你不應該和這些 layer 直接交互,因為 AppKit 才是這些 layer 的擁有者。舉個例子,在 iOS 上你可以隨意編寫這樣的代碼:
self.layer.backgroundColor = [UIColor redColor].CGColor;
但是在 AppKit,你不應該直接修改這些 layer。如果想用這種方式和 layer 交互,你還有一步工作要做。重寫 NSView
的 wantsUpdateLayer
方法并返回 YES
,這能讓你可以改變 layer 的屬性。如果你這樣做,AppKit 將不會再調用 view 的 drawRect:
方法。取而代之,你應該在 updateLayer
里修改 Layer,這個方法會在 view 的更新周期中被調用。
舉個例子,你可以用這方法去實現(xiàn)一個非常簡單的有純色背景的 view(沒錯,NSView
沒有 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
這個例子的前提是這個 view 的父 view 已經為其視圖樹啟用了 layer backing。另一種可行的實現(xiàn)則只需要重寫 drawRect:
方法并在其中繪制背景顏色。
選擇使用眾多 layer-backed view 會帶來巨大的內存消耗(每一個 layer 有其自己的 backing store,還有可能和其他 view 的 backing store 重疊)而且會帶來潛在的合成這些 layer 的消耗。從 OS X 10.9 開始,你可以通過設置 canDrawSubviewsIntoLayer
屬性來讓 AppKit 合并一個視圖樹中所有 layer 的內容到一個共有的 layer。如果你不需要單獨對一個 view 中的子 view 做動畫,這將是一個很好的選擇。
所有隱式 layer-backed 的子 view(比如,你沒有顯式地對這些子 view 設置 wantsLayer = YES
)現(xiàn)在將會被繪制到同一個 layer 中。不過,wantsLayer
設置為 YES
的子 view 仍然持有它們自己的 backing layer, 而且不管 wantsUpdateLayer
返回什么,它們的 drawRect:
方法仍然會被調用。
另外一個需要注意的地方:layer-backed view 會默認設置重繪策略為 NSViewLayerContentsRedrawDuringViewResize
。在行為上,這個非 layer-backed view 是類似的,不過如果動畫的每一幀都引入一個繪制步驟的話可能會對動畫的性能造成不利影響。
為了避免這個問題,你可以把 layerContentsRedrawPolicy
屬性設置為 NSViewLayerContentsRedrawOnSetNeedsDisplay
。這樣子的話,便由你來決定 layer 的內容何時需要重繪。幀的改變將不再自動觸發(fā)重繪;現(xiàn)在你要負責調用 -setNeedsDisplay:
來觸發(fā)重繪操作。
一旦你這樣更改了重繪策略,你也許會想了解下 view 中和 layer 的 contentGravity
屬性等價的 layerContentsPlacement
屬性。這個屬性允許你指定在調整大小的時候當前的 layer 內容該怎么映射到 layer 上。
NSView
的 layer 故事并沒有完結。你可以用另一種完全不一樣的方式來使用 Core Animation layer — 稱為 layer-hosting view。簡單來說,你可以對一個 layer-hosting view 的 layer 及其子 layer 做任何操作,代價是你再也不能給該 view 添加任何子 view。layer-hosting view 是視圖樹中的葉子節(jié)點。
要創(chuàng)建一個 layer-hosting view,你首先要為 view 的 layer
屬性分配一個 layer 對象,然后把 wantsLayer
設置為 YES
。注意,這些步驟的順序是非常關鍵的:
- (instancetype)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.layer = [[CALayer alloc] init];
self.wantsLayer = YES;
}
}
在你設置了你自定義的 layer 之后才設置 wantsLayer
是非常重要的。
默認情況下,Mac 上視圖的坐標系統(tǒng)原點位于左下角,而不是像 iOS 的左上角。剛開始這可能會讓人混亂,不過你可以通過重寫 isFlipped
并返回 YES
來恢復到你熟悉的左上角。
由于 AppKit 中的 view 沒有背景顏色屬性可以讓你直接設置為 [NSColor clearColor]
來讓其變得透明,許多 NSView
的子類比如 NSTextView
和 NSScrollView
開放了一個 drawsBackground
屬性,如果你想讓這一類 view 透明,你必須設置該屬性為 NO
。
為了能接收光標進出一個 view 或者在 view 里面移動的事件,你需要創(chuàng)建一個追蹤區(qū)域。你可以在 NSView
中指定的 updateTrackingAreas
方法中來做這件事情。一個通用的寫法看起來是這樣子的:
- (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
的子類驅動的。不要混淆這些 cell 和 UIKit 里 table view 的 cell 及 collection view 的 cell。AppKit 最初區(qū)分 view 和 cell 是為了節(jié)省資源 - view 可以把所有的繪制工作代理給更輕量級的可以被所有同類型的 view 重用的 cell 對象。
Apple 正在一步步地拋棄這樣的實現(xiàn)方法了,但是你還是會時不時碰到這樣的問題。舉個例子,如果你想創(chuàng)建一個自定義的按鈕,你首先要繼承 NSButton
和 NSButtonCell
,然后在這個 cell 子類里面進行你自定義的繪制,然后通過重寫 +[NSControl cellClass]
方法告訴自定義按鈕使用你的 cell 子類。
最后,如果你想知道在你自己的 drawRect:
方法里怎么獲取當前的 Core Graphics 上下文,答案是 NSGraphicsContext
的 graphicsPort
屬性。詳細內容請查看 Cocoa Drawing Guide。
歸結于上面提到的視圖系統(tǒng)的差異,動畫在 Mac 上的運作方式也十分不同。想要一個好的概述,請觀看 WWDC session:Best Practices for Cocoa Animation
如果你的 view 不是由 layer 驅動的,那你的動畫自然是完全由 CPU 處理,這意味著動畫的每一步都必須相應地繪制到 window-backing store 上。因為現(xiàn)今我們主要是對 layer-backed view 做動畫以獲得流暢的動畫效果,所以我們在這兒就專注于這種情況。
正如上面說的,在 AppKit 中你不應該修改 layer-backed view 中的 layer (看 Core Animation Programming Guide 這篇文檔底部 “Rules for Modifying Layers in OS X” 那一節(jié))。這些 layer 由 AppKit 管理,而且和 iOS 相反,view 的幾何屬性并不僅僅是對應的 layer 的幾何屬性的映射,但 AppKit 卻會把 view 內部的幾何屬性同步到 layer。
你可以用幾種不同的方法對一個 view 進行動畫。第一種,你可以使用 animator proxy:
view.animator.alphaValue = .5;
在幕后,這句代碼會啟用 layer 的隱式動畫,設置其透明度,然后再次禁用 layer 的隱式動畫。
你還可以把這句代碼封裝到一個 animation context 中,這樣你就能得到它的結束回調:
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
view.animator.alphaValue = .5;
} completionHandler:^{
// ...
}];
如果想改變持續(xù)時間和緩動類型,我們必須對其動畫上下文進行設置:
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
context.duration = 1;
context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
view.animator.alphaValue = .5;
} completionHandler:^{
// ...
}];
如果你不需要結束回調,你可以用這種簡化形式:
[NSAnimationContext currentContext].duration = 1;
view.animator.alphaValue = .5;
最后,你可以啟用隱式動畫,這樣你就不必每次都明確地使用 animator proxy 了:
[NSAnimationContext currentContext].allowsImplicitAnimations = YES;
view.alphaValue = .5;
要更全面地控制動畫,你可以使用 CAAnimation
實例。和 iOS 相反,你不能直接把它們加到 layer 上(因為 layer 不應該由你來修改),不過你可以使用 NSAnimatablePropertyContainer
協(xié)議中定義的 API,NSView
和 NSWindow
已經實現(xiàn)了該協(xié)議。舉個例子:
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.values = @[@1, @.9, @.8, @.7, @.6];
view.animations = @{@"alphaValue": animation};
view.animator.alphaValue = .5;
對于幀
動畫來說,把 view 的 layerContentsRedrawPolicy
設置為 NSViewLayerContentsRedrawOnSetNeedsDisplay
是非常重要的,不然的話 view 的內容在每一幀都會被重繪。
很遺憾,NSView
沒有開放 Core Animation layer 所有可以進行動畫的屬性,transform
是其中最重要的例子??纯?Jonathan Willings 的這篇文章,它描述了你可以如何解決這些限制。不過注意,文章中的解決方案是不受官方支持的。
上面提到的所有東西都適用于 layer-backed view。對于 layer-hosting view 來說,你可以直接對 view 的 layer 或者子 layer 使用 CAAnimations
,因為你擁有它們的控制權。
盡管 AppKit 有 NSCollectionView
類,它的功能卻比 UIKit 里對應的類滯后很多。鑒于 UICollectionView
是 iOS 上一個如此多功能的控件(當然,這取決于你的 UI 觀念),AppKit 里對應的控件一點都不像它這件事相當難以忍受。所以當你要規(guī)劃你的用戶界面的時候,要考慮構建一個網格布局有可能會非常麻煩,相反,在 iOS 上這很容易實現(xiàn)。
來自 iOS 的你對 UIImage
肯定非常熟悉,正巧,AppKit 也有一個對應的 NSImage
類。不過很快你就會意識到這兩個類簡直是天差地別。從很多方面來說,NSImage
都比 UIImage
強大很多,但這是建立在復雜性增加的代價上的。Apple 的 Cocoa Drawing Guide 很好地介紹了如何使用 AppKit 中的圖像。
概念上最重要的不同是 NSImage
由一個或者多個圖像表示(image representation,譯者注:這里的圖像表示為名詞,可以參考百度百科,本節(jié)下同)驅動,這些圖像表示在 AppKit 表現(xiàn)為一些 NSImageRep
的子類,像 NSBitmapImageRep
,NSPDFImageRep
和 NSEPSImageRep
。舉個例子,一個 NSImage
對象為了打印同樣的內容可以持有縮略圖,全尺寸和 PDF 三個圖像表示。當你繪制圖像時,圖像表示會匹配當前的圖形上下文,而繪圖尺寸會根據(jù)顏色空間,維度,分辨率以及繪圖深度得出。
此外,Mac 上的圖像除了尺寸還有分辨率的概念。圖像表示的分辨率由三個屬性構成:size
,pixelsWide
以及 pixelsHigh
。size 屬性決定了圖像表示被渲染時的尺寸,而 pixelsWide 和 pixelsHigh 指定了源于圖像數(shù)據(jù)的原始尺寸。這三個屬性共同決定了圖像表示的分辨率。像素尺寸可以和圖像表示的尺寸不一樣,正如圖像表示的尺寸可以和它所屬的圖片的尺寸不一樣。
另外一個和 UIImage
不一樣的地方是當它被繪制到屏幕上時 NSImage
會緩存繪制結果(可以通過 cacheMode
屬性配置)。當你改變底層的圖像表示,你必須對圖像調用 recache
才能使其生效。
不過在 Mac 上面處理圖像并不總是比 iOS 復雜。NSImage
提供了一個很簡單的方法去繪制一個新圖像,而在 iOS 上,你需要創(chuàng)建一個位圖上下文,然后用位圖上下文創(chuàng)建 CGImage
,最終用該 CGImage 初始化一個 UIImage
實例。用 NSImage
你僅需:
[NSImage imageWithSize:(NSSize)size
flipped:(BOOL)drawingHandlerShouldBeCalledWithFlippedContext
drawingHandler:^BOOL (NSRect dstRect)
{
// your drawing commands here...
}];
Mac 支持完全的 color-calibrated 工作流,所有跟顏色相關的任何東西都有可能變得更復雜。顏色管理是一個復雜的主題,我們也不精通這方面的東西。所以,我們希望你看看 Apple 關于這方面的指南: Introduction to Color Programming Topics for Cocoa 和 Introduction to Color Management。
你經常需要在你的應用里使用一個你的設計師給你指定的顏色。要取得正確的顏色,設計模板使用的顏色空間和你以編程方式指定的顏色空間保持一致是非常重要的。系統(tǒng)標準的顏色選擇器有一個下拉菜單,你可以在這里選擇你想要的顏色空間。我們建議使用 device-independent sRGB 顏色空間,然后在代碼里面用 +[NSColor colorWithSRGBRed:green:blue:alpha:]
類方法來創(chuàng)建顏色。
http://wiki.jikexueyuan.com/project/objc/images/14-4.png" alt="" />
有了 TextKit,iOS 7 終于有了和 Mac 上早就有了的 Cocoa Text System 等效的東西。但 Apple 并不僅僅是把文字系統(tǒng)從 Mac 上轉移到 iOS;相反,Apple 對其做了些顯著的改變。
舉個例子,AppKit 開放 NSTypesetter
和 NSGlyphGenerator
,你可以通過繼承這兩者來自定義它們的一些特性。iOS 并不開放這些類,但是你可以通過 NSLayoutManagerDelegate
協(xié)議達到定制的目的。
總體來說,兩個平臺的文字系統(tǒng)還是非常相似的,所有你在 iOS 上能做的在 Mac 上都可以做(甚至更多),但對于一些東西,你必須從不同的地方尋找合適的方法實現(xiàn)。
符合沙盒機制的 Mac 應用才能通過 Mac App Store 銷售。鑒于沙盒從一開始就是 iOS 的基本規(guī)范(所以你會對它非常熟悉),你可能會好奇我們?yōu)槭裁匆谶@里提起它。然而,我們已經習慣了沙盒機制還沒出現(xiàn)之前的 Mac 開發(fā)環(huán)境,所以有時候會忽視一些你想要實現(xiàn)的功能會和沙盒的限制出現(xiàn)沖突。
Mac 的文件系統(tǒng)是一直對用戶開放的,所以如果用戶明確表示,沙盒應用可以訪問自身應用外的文件。同樣的機制同時引進了 iOS 8。不過,和通過這種方式放寬對 iOS 的限制相反,它卻加強了對 Mac 的限制。這讓它容易被忽視和遺忘。
對此我們也十分慚愧,所以希望能阻止你犯同樣的錯誤。當我們開始開發(fā) Deckset — 一款把簡單 Markdown 文件轉換為演示幻燈片的應用 — 時,我們從來沒想過我們會碰到什么關于沙盒的問題。畢竟,我們只需要讀 Markdown 文件的權限。
我們忘記了我們還要顯示 Markdown 文件中引用的圖片。盡管你在 Markdown 文件中輸入了圖片文件的路徑,但沙盒系統(tǒng)并不認為這是用戶的意圖。最后,我們通過一個像通知中心一樣的 UI 來提示用戶授權我們訪問 Markdown 文件中的所有圖片‘解決’了該問題。
及早看一下 Apple 的 sandboxing guides 以防以后在相關的問題上犯錯誤。
有很多事情你只能在 Mac 上做,這主要是因為它不同的交互模型和它更為寬松的安全策略。在本期話題中,我們有一些文章深入探討了其中的一些內容:進程間通訊,使 Mac 應用腳本化, 在沙盒中腳本化其他應用, 為你的應用構建插件。
當然,這只是 Mac 獨有特性中很小的一部分,但這給了你一個很好的視角看待 iOS 8 從頭開始打造其可擴展性和 app 間通訊。最后,還有很多東西等待你去探索:Drag and Drop,Printing,Bindings,OpenCL 等等,這里僅僅是舉幾個例子。