從 UIKit 到 AppKit

Mac 不只是一個強大的生產平臺,也十分值得你爲其開發一些東西。去年咱們開始構建咱們的第一款 Mac 應用,成功爲咱們平常工做所在的平臺開發點東西是一次十分美好的體驗。可是,和爲 iOS 系統開發應用相比,在咱們瞭解 Mac 特性的過程當中也遇到了一些困難。這篇文章總結了咱們從這一過渡中獲得的經驗,但願能啓發大家去開發本身的第一個 Mac 應用。html

在這篇文章中,咱們假定 OS X Yosemite 爲咱們默認使用的系統。今年,爲了融合 iOS 和 OS X,蘋果站在開發者的角度對 OS X 作出了巨大的改進。不過,咱們會指出哪些特性僅適用於 Yosemite,而哪些特性也適用於以前的系統版本。ios

小編這裏推薦一個羣:691040931 裏面有大量的書籍和麪試資料,不少的iOS開發者都在裏面交流技術

類似點

儘管 iOS 和 OS X 是兩個獨立的系統,它們卻有不少共性。先就開發環境而言,它們使用一樣的開發語言,一樣的IDE。因此你會對這一切都感到很是熟悉。面試

更重要的是,OS X 和你已經熟悉的 iOS 共用許多框架,像 Foundation,Core Data 和 Core Animation。今年,Apple 進一步整合兩個平臺,並給 Mac 帶來了一些以前僅能在 iOS 上面使用的框架,其中一個例子就是 Multipeer Connectivity。在更底層的地方,你馬上能夠看到你熟悉的 API:Core Graphics,Core Text,libdispatch 等等。編程

真正開始有區別的是 UI 框架 — AppKit 早在 NeXT 時代就已面世並不斷進化,而 UIKit 就像是簡約版及現代版的 AppKit。出現這種狀況的緣由,是當 Apple 推出 iPhone 時能夠從頭開始,並吸收 AppKit 的經驗:把已證明過可行的概念和部件拿過來用,並改進不夠精良的設計。windows

若是你對這個轉換是怎麼發生的感興趣,請觀看前 Apple iOS 應用總監 Nitin Ganatra 播客上的精彩劇集:System 7 to CarbonOS X to iOS,以及 iPhone to iPad設計模式

考慮到這一點,也就不奇怪爲何 UIKit 和 AppKit 仍舊共享許多概念了。UI 是基於 window 和 view 構建起來的,消息像 iOS 同樣經過響應者鏈傳遞。此外,UIView 是 NSViewUIControl 是 NSControlUIImage 是 NSImageUIViewController 是 NSViewControllerUITextView 是 NSTextView...這樣的例子不勝枚舉。緩存

看起來就像你僅需把 UI 前綴替換爲 NS 前綴,你就能夠用一樣的方法使用這些類。但事實是在不少狀況下這並不奏效。它們在實現上並無在概念上那麼類似。你在 iOS 上的經驗至多能幫你大體瞭解構建用戶界面的基礎,以及使用不少設計模式,好比代理,都是相似的。可是細節是魔鬼 — 你真的應該經過閱讀文檔來學習如何使用這些類。安全

下一節,咱們來看看那些常見的陷阱。bash

不一樣點

Window 和 Window Controller

雖然在 iOS 上你幾乎歷來不用與 window 交互(由於它們佔據了整個屏幕),window 在 Mac 上倒是一個關鍵組件。從歷史上看, Mac 應用包含多個 window,每一個 window 有其本身的角色,很是相似於 iOS 上面的 view controller。所以, AppKit 有 NSWindowController,它接管不少在 iOS 上你會在 view controller 裏面處理的任務。view controller 被添加到 AppKit 的時間並不長,並且直到如今,它們默認不接受 action,而且缺失不少生命週期的方法、view controller 容器,以及不少你在 UIKit 中熟悉的特性。session

但 AppKit 框架已經改變,由於 Mac 應用愈來愈依賴於一個單一的 window。就 OS X 10.10 Yosemite 而言,NSViewController 在許多方面與 UIViewController 相似。它也默認是響應者鏈中的一環。但要記住,若是你的 Mac 應用須要兼容 OS X 10.9 或更早版本的系統,Mac 上的 window controller 更相似於 iOS 上你熟悉的 view controller。正如 Mike Ash 所言,在 Mac 上實例化窗口的一個好的模式是:每一個窗口類型對應一個 nib 文件和一個 window controller。

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

響應者鏈

若是你在爲 OS X 10.9 或者更低版本的系統開發,請注意在默認狀況下 view controller 並非響應者鏈的一環。相反,事件會沿着視圖樹向上傳遞而後直接到達 window 和 window controller。在這種狀況下,若是你想在 view controller 處理事件,你須要手動把它添加到響應者鏈中。

除了在響應者鏈方面的不一樣,AppKit 在 action 的命名方法上還有一個嚴格的慣例,一個 action 方法看起來老是相似這樣子的:

- (void)performAction:(id)sender;
複製代碼

以上方法在 iOS 上面所容許的沒有參數,或者有一個 sender 和一個 event 參數,而這些變體在 OS X 上面是沒法使用的。此外,控件(譯者注:指 NSControl 及其子類)在 AppKit 中一般對應一個 target 和一個 action,而不像在 iOS 上能夠經過 addTarget:action:forControlEvents: 方法爲一個控件關聯多個 target-action 對。

View

由於歷史遺留問題,Mac 的視圖系統和 iOS 的視圖系統有很大區別。iOS 上的 view 一開始就由 Core Animation layer 驅動。可是 AppKit 比 Core Animation 早出來了好久,當 Apple 設計 AppKit 時,咱們如今熟知的 GPU 尚未出現。所以,那時視圖系統相關的任務主要靠 CPU 處理。

當你要開始進行 Mac 相關的開發時,咱們強烈推薦你查看 Apple 的 Introduction to View Programming Guide for Cocoa。此外,你還應該看一下這兩個精彩的 WWDC session:Layer-Backed Views: AppKit + Core Animation 和 Optimizing Drawing and Scrolling

Layer-Backed View

默認狀況下,AppKit 的 view 不是由 Core Animation layer 驅動的;AppKit 整合 layer-backing 是 iOS 反哺的結果。一些在 AppKit 須要作的決定你在 UIKit 歷來不須要關心。AppKit 區分 layer-backed view 和 layer-hosting view,能夠在每一個視圖樹的根節點啓用或者禁用 layer backing。

把窗口的 contentView 的 wantsLayer 屬性設置爲 YES 是啓用 layer backing 最簡單的方法。這會致使 window 的視圖樹中全部的 view 都啓用 layer backing,這樣就不必反覆設置每一個 view 的 wantsLayer 屬性了。這個操做能夠用代碼或者在 Interface Builder 的 View Effects Inspector 面板完成。

和 iOS 相比而言,在 Mac 上你應該把 backing layer 看作是一個實現細節。這意味着你不該該和這些 layer 直接交互,由於 AppKit 纔是這些 layer 的擁有者。舉個例子,在 iOS 上你能夠隨意編寫這樣的代碼:

self.layer.backgroundColor = [UIColor redColor].CGColor;
複製代碼

可是在 AppKit,你不該該直接修改這些 layer。若是想用這種方式和 layer 交互,你還有一步工做要作。重寫 NSView 的 wantsUpdateLayer 方法並返回 YES,這能讓你能夠改變 layer 的屬性。若是你這樣作,AppKit 將不會再調用 view 的 drawRect: 方法。取而代之,你應該在 updateLayer 裏修改 Layer,這個方法會在 view 的更新週期中被調用。

舉個例子,你能夠用這方法去實現一個很是簡單的有純色背景的 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。另外一種可行的實現則只須要重寫 drawRect: 方法並在其中繪製背景顏色。

合併 Layer

選擇使用衆多 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)如今將會被繪製到同一個 layer 中。不過,wantsLayer 設置爲 YES 的子 view 仍然持有它們本身的 backing layer, 並且無論 wantsUpdateLayer 返回什麼,它們的 drawRect: 方法仍然會被調用。

Layer 重繪策略

另一個須要注意的地方:layer-backed view 會默認設置重繪策略爲 NSViewLayerContentsRedrawDuringViewResize。在行爲上,這個非 layer-backed view 是相似的,不過若是動畫的每一幀都引入一個繪製步驟的話可能會對動畫的性能形成不利影響。

爲了不這個問題,你能夠把 layerContentsRedrawPolicy 屬性設置爲 NSViewLayerContentsRedrawOnSetNeedsDisplay 。這樣子的話,便由你來決定 layer 的內容什麼時候須要重繪。幀的改變將再也不自動觸發重繪;如今你要負責調用 -setNeedsDisplay: 來觸發重繪操做。

一旦你這樣更改了重繪策略,你也許會想了解下 view 中和 layer 的 contentGravity 屬性等價的 layerContentsPlacement 屬性。這個屬性容許你指定在調整大小的時候當前的 layer 內容該怎麼映射到 layer 上。

Layer-Hosting View

NSView 的 layer 故事並無完結。你能夠用另外一種徹底不同的方式來使用 Core Animation layer — 稱爲 layer-hosting view。簡單來講,你能夠對一個 layer-hosting view 的 layer 及其子 layer 作任何操做,代價是你不再能給該 view 添加任何子 view。layer-hosting view 是視圖樹中的葉子節點。

要建立一個 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 是很是重要的。

其餘與 View 相關的陷阱

默認狀況下,Mac 上視圖的座標系統原點位於左下角,而不是像 iOS 的左上角。剛開始這可能會讓人混亂,不過你能夠經過重寫 isFlipped 並返回 YES 來恢復到你熟悉的左上角。

因爲 AppKit 中的 view 沒有背景顏色屬性可讓你直接設置爲 [NSColor clearColor] 來讓其變得透明,許多 NSView 的子類好比 NSTextView 和 NSScrollView 開放了一個 drawsBackground 屬性,若是你想讓這一類 view 透明,你必須設置該屬性爲 NO。

爲了能接收光標進出一個 view 或者在 view 裏面移動的事件,你須要建立一個追蹤區域。你能夠在 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 最初區分 view 和 cell 是爲了節省資源 - view 能夠把全部的繪製工做代理給更輕量級的能夠被全部同類型的 view 重用的 cell 對象。

Apple 正在一步步地拋棄這樣的實現方法了,可是你仍是會時不時碰到這樣的問題。舉個例子,若是你想建立一個自定義的按鈕,你首先要繼承 NSButton  NSButtonCell,而後在這個 cell 子類裏面進行你自定義的繪製,而後經過重寫 +[NSControl cellClass] 方法告訴自定義按鈕使用你的 cell 子類。

最後,若是你想知道在你本身的 drawRect: 方法裏怎麼獲取當前的 Core Graphics 上下文,答案是 NSGraphicsContext 的 graphicsPort 屬性。詳細內容請查看 Cocoa Drawing Guide

動畫

歸結於上面提到的視圖系統的差別,動畫在 Mac 上的運做方式也十分不一樣。想要一個好的概述,請觀看 WWDC session:Best Practices for Cocoa Animation

若是你的 view 不是由 layer 驅動的,那你的動畫天然是徹底由 CPU 處理,這意味着動畫的每一步都必須相應地繪製到 window-backing store 上。由於現今咱們主要是對 layer-backed view 作動畫以得到流暢的動畫效果,因此咱們在這兒就專一於這種狀況。

正如上面說的,在 AppKit 中你不該該修改 layer-backed view 中的 layer (看 Core Animation Programming Guide 這篇文檔底部 「Rules for Modifying Layers in OS X」 那一節)。這些 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:^{
    // ...
}]; 
複製代碼

若是想改變持續時間和緩動類型,咱們必須對其動畫上下文進行設置:

[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 協議中定義的 API,NSView 和 NSWindow 已經實現了該協議。舉個例子:

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,由於你擁有它們的控制權。

Collection View

儘管 AppKit 有 NSCollectionView 類,它的功能卻比 UIKit 裏對應的類滯後不少。鑑於 UICollectionView 是 iOS 上一個如此多功能的控件(固然,這取決於你的 UI 觀念),AppKit 裏對應的控件一點都不像它這件事至關難以忍受。因此當你要規劃你的用戶界面的時候,要考慮構建一個網格佈局有可能會很是麻煩,相反,在 iOS 上這很容易實現。

圖像

來自 iOS 的你對 UIImage 確定很是熟悉,正巧,AppKit 也有一個對應的 NSImage 類。不過很快你就會意識到這兩個類簡直是天差地別。從不少方面來講,NSImage 都比 UIImage 強大不少,但這是創建在複雜性增長的代價上的。Apple 的 Cocoa Drawing Guide 很好地介紹瞭如何使用 AppKit 中的圖像。

概念上最重要的不一樣是 NSImage 由一個或者多個圖像表示(image representation,譯者注:這裏的圖像表示爲名詞,能夠參考百度百科,本節下同)驅動,這些圖像表示在 AppKit 表現爲一些 NSImageRep 的子類,像 NSBitmapImageRepNSPDFImageRep 和 NSEPSImageRep。舉個例子,一個 NSImage 對象爲了打印一樣的內容能夠持有縮略圖,全尺寸和 PDF 三個圖像表示。當你繪製圖像時,圖像表示會匹配當前的圖形上下文,而繪圖尺寸會根據顏色空間,維度,分辨率以及繪圖深度得出。

此外,Mac 上的圖像除了尺寸還有分辨率的概念。圖像表示的分辨率由三個屬性構成:sizepixelsWide 以及 pixelsHigh。size 屬性決定了圖像表示被渲染時的尺寸,而 pixelsWide 和 pixelsHigh 指定了源於圖像數據的原始尺寸。這三個屬性共同決定了圖像表示的分辨率。像素尺寸能夠和圖像表示的尺寸不同,正如圖像表示的尺寸能夠和它所屬的圖片的尺寸不同。

另一個和 UIImage 不同的地方是當它被繪製到屏幕上時 NSImage 會緩存繪製結果(能夠經過 cacheMode屬性配置)。當你改變底層的圖像表示,你必須對圖像調用 recache 才能使其生效。

不過在 Mac 上面處理圖像並不老是比 iOS 複雜。NSImage 提供了一個很簡單的方法去繪製一個新圖像,而在 iOS 上,你須要建立一個位圖上下文,而後用位圖上下文建立 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

你常常須要在你的應用裏使用一個你的設計師給你指定的顏色。要取得正確的顏色,設計模板使用的顏色空間和你以編程方式指定的顏色空間保持一致是很是重要的。系統標準的顏色選擇器有一個下拉菜單,你能夠在這裏選擇你想要的顏色空間。咱們建議使用 device-independent sRGB 顏色空間,而後在代碼裏面用 +[NSColor colorWithSRGBRed:green:blue:alpha:] 類方法來建立顏色。

文字系統

有了 TextKit,iOS 7 終於有了和 Mac 上早就有了的 Cocoa Text System 等效的東西。但 Apple 並不只僅是把文字系統從 Mac 上轉移到 iOS;相反,Apple 對其作了些顯著的改變。

舉個例子,AppKit 開放 NSTypesetter 和 NSGlyphGenerator,你能夠經過繼承這二者來自定義它們的一些特性。iOS 並不開放這些類,可是你能夠經過 NSLayoutManagerDelegate 協議達到定製的目的。

整體來講,兩個平臺的文字系統仍是很是類似的,全部你在 iOS 上能作的在 Mac 上均可以作(甚至更多),但對於一些東西,你必須從不一樣的地方尋找合適的方法實現。

沙盒

符合沙盒機制的 Mac 應用才能經過 Mac App Store 銷售。鑑於沙盒從一開始就是 iOS 的基本規範(因此你會對它很是熟悉),你可能會好奇咱們爲何要在這裏提起它。然而,咱們已經習慣了沙盒機制還沒出現以前的 Mac 開發環境,因此有時候會忽視一些你想要實現的功能會和沙盒的限制出現衝突。

Mac 的文件系統是一直對用戶開放的,因此若是用戶明確表示,沙盒應用能夠訪問自身應用外的文件。一樣的機制同時引進了 iOS 8。不過,和經過這種方式放寬對 iOS 的限制相反,它卻增強了對 Mac 的限制。這讓它容易被忽視和遺忘。

對此咱們也十分慚愧,因此但願能阻止你犯一樣的錯誤。當咱們開始開發 Deckset — 一款把簡單 Markdown 文件轉換爲演示幻燈片的應用 — 時,咱們歷來沒想過咱們會碰到什麼關於沙盒的問題。畢竟,咱們只須要讀 Markdown 文件的權限。

咱們忘記了咱們還要顯示 Markdown 文件中引用的圖片。儘管你在 Markdown 文件中輸入了圖片文件的路徑,但沙盒系統並不認爲這是用戶的意圖。最後,咱們經過一個像通知中心同樣的 UI 來提示用戶受權咱們訪問 Markdown 文件中的全部圖片‘解決’了該問題。

及早看一下 Apple 的 sandboxing guides 以防之後在相關的問題上犯錯誤。

獨有特性

有不少事情你只能在 Mac 上作,這主要是由於它不一樣的交互模型和它更爲寬鬆的安全策略。在本期話題中,咱們有一些文章深刻探討了其中的一些內容:進程間通信使 Mac 應用腳本化在沙盒中腳本化其餘應用爲你的應用構建插件

固然,這只是 Mac 獨有特性中很小的一部分,但這給了你一個很好的視角看待 iOS 8 從頭開始打造其可擴展性和 app 間通信。最後,還有不少東西等待你去探索:Drag and Drop,Printing,Bindings,OpenCL 等等,這裏僅僅是舉幾個例子。


小編這裏推薦一個羣:691040931 裏面有大量的書籍和麪試資料,不少的iOS開發者都在裏面交流技術

原文 AppKit for UIKit Developers

相關文章
相關標籤/搜索