1. 程式人生 > >iOS 7 : 隱藏技巧和變通之道

iOS 7 : 隱藏技巧和變通之道

當 iOS 7 剛釋出的時候,全世界的蘋果開發人員都立馬嘗試著去編譯他們的 app,接著再花上數月的時間來修復任何出現的錯誤,甚至從頭開始重建這個 app。這樣的結果,使得人們根本無暇去探究 iOS 7 所帶來的新思想。除開一些明顯而細微的更新,比如說 NSArray 的 firstObject 方法——這個方法可追溯到 iOS 4 時代,現在被提為公有 API——還有很多隱藏的技巧等著我們去挖掘。

平滑淡入淡出動畫

我在這裡要討論的並非新的彈性動畫 API 或者 UIDynamics,而是一些更細微的東西。CALayer 增加了兩個新方法:allowsGroupOpacityallowsEdgeAntialiasing

。現在,組不透明度(group opacity)不再是什麼新鮮的東西了。iOS 會多次使用存在於 Info.plist 中的鍵 UIViewGroupOpacity 並可在應用程式範圍內啟用或禁用它。對於大多數 app 而言,這(譯註:啟用)並非所期望的,因為它會降低整體效能。在 iOS 7 中,用 SDK 7 所連結的程式,這項屬性預設是啟用的。當它被啟用時,一些動畫將會變得不流暢,它也可以在 layer 層上被控制。

一個有趣的細節,如果 allowsGroupOpacity 啟用的話,_UIBackdropView(被用作 UIToolbar 或者 UIPopoverView 的背景檢視)不能對其模糊進行動畫處理,所以當你做一個 alpha 轉換時,你可能會臨時禁用這項屬性。因為這會降低動畫體驗,你可以回到舊的方式然後在動畫期間臨時啟用 shouldRasterize

。別忘了設定適當的 rasterizationScale,否則在 retina 的裝置上這些檢視會成鋸齒狀(pixelerated)。

如果你想要複製 Safari 顯示所有選項卡時的動畫,那麼邊緣抗鋸齒屬性將變得非常有用。

阻塞動畫

有一個小但是非常有用的新方法 [UIView performWithoutAnimation:]。它是一個簡單的封裝,先檢查動畫當前是否啟用,如果是則停用動畫,執行塊語句,然後重新啟用動畫。一個需要說明的地方是,它並 不會 阻塞基於 CoreAnimation 的動畫。因此,不用急於將你的方法呼叫從:

    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    view.frame = CGRectMake(...);
    [CATransaction commit];

替換成:

    [UIView performWithoutAnimation:^{
        view.frame = CGRectMake(...);
    }];

但是,絕大多數情況下這樣也能工作得很好,只要你不直接跟 CALayer 打交道。

iOS 7 中,我有很多程式碼路徑(主要是 UITableViewCells)需要額外保護以防止意外的動畫,例如,如果一個彈窗(popover)的大小調整了,與此同時其中的表檢視將因為高度的變化而載入新的 cell。我通常的做法是將整個 layoutSubviews 的程式碼包紮到一個動畫塊中:

- (void)layoutSubviews 
{
    // 否則在 iOS 7 的傳統模式下彈窗動畫會滲入我們的單元格
    [UIView performWithoutAnimation:^{
        [super layoutSubviews];
        _renderView.frame = self.bounds;
    }];
}

處理長的表檢視

UITableView 非常快速高效,除非你開始使用 tableView:heightForRowAtIndexPath:,它會開始為你表中 每一個 元素呼叫此方法,即便沒有可視物件——這是為了讓其下層的 UIScrollView 能獲取正確的 contentSize。此前有一些變通方法,但都不好用。iOS 7 中,蘋果公司終於承認這一問題,並添加了 tableView:estimatedHeightForRowAtIndexPath:,這個方法把絕大部分計算成本推遲到實際滾動的時候。如果你完全不知道一個 cell 的大小,返回 UITableViewAutomaticDimension 就行了。

對於段頭/尾(section headers/footers),現在也有類似的 API 了。

UISearchDisplayController

蘋果的 search controller 使用了新的技巧來簡化移動 search bar 到 navigation bar 的過程。啟用 displaysSearchBarInNavigationBar 就可以了(除非你還在用 scope bar,那你就太不幸了)。我倒是很喜歡這麼做,但遺憾的是,iOS 7 上的 UISearchDisplayController 貌似被破壞得相當嚴重,尤其在 iPad 上。蘋果公司看上去像是沒時間處理這個問題,對於顯示的搜尋結果並不會隱藏實際的表檢視。在 iOS 7 之前,這不算問題,但是現在 searchResultsTableView 有一個透明的背景色,使它看上去相當糟糕。作為一種變通方法,你可以設定不透明背景色或者採取一些更富於技巧的手段來獲得你期望的效果。關於這個控制元件我碰到過各種各樣的結果,當使用 displaysSearchBarInNavigationBar 時甚至 根本 不會顯示搜尋表檢視。

你的結果可能有所不同,但我依賴於一些手段(severe hacks)來讓 displaysSearchBarInNavigationBar 工作:

- (void)restoreOriginalTableView 
{
    if (PSPDFIsUIKitFlatMode() && self.originalTableView) {
        self.view = self.originalTableView;
    }
}

- (UITableView *)tableView 
{
    return self.originalTableView ?: [super tableView];
}

- (void)searchDisplayController:(UISearchDisplayController *)controller 
  didShowSearchResultsTableView:(UITableView *)tableView 
{
    // HACK: iOS 7 依賴於重度的變通來顯示搜尋表檢視
    if (PSPDFIsUIKitFlatMode()) {
        if (!self.originalTableView) self.originalTableView = self.tableView;
        self.view = controller.searchResultsTableView;
        controller.searchResultsTableView.contentInset = UIEdgeInsetsZero; // 移除 64 畫素的空白
    }
}

- (void)searchDisplayController:(UISearchDisplayController *)controller 
  didHideSearchResultsTableView:(UITableView *)tableView 
{
    [self restoreOriginalTableView];
}

另外,別忘了在 viewWillDisappear 中呼叫 restoreOriginalTableView,否則程式會 crash。 記住這只是一種解決辦法;可能還有不那麼激進的方法,不用替換檢視本身,但這個問題確實應該由蘋果公司來修復。(TODO: RADAR!)

分頁

UIWebView 現在可以對帶有 paginationMode 的網站進行自動分頁。有一大堆與此功能相關的新屬性:

@property (nonatomic) UIWebPaginationMode paginationMode NS_AVAILABLE_IOS(7_0);
@property (nonatomic) UIWebPaginationBreakingMode paginationBreakingMode NS_AVAILABLE_IOS(7_0);
@property (nonatomic) CGFloat pageLength NS_AVAILABLE_IOS(7_0);
@property (nonatomic) CGFloat gapBetweenPages NS_AVAILABLE_IOS(7_0);
@property (nonatomic, readonly) NSUInteger pageCount NS_AVAILABLE_IOS(7_0);

目前而言,雖然這不一定對大多數網站都有用,但它肯定是生成簡單的電子書閱讀器或者顯示文字的一種更好的方式。加點樂子的話,請嘗試將它設定為 UIWebPaginationModeBottomToTop

會飛的 Popover

想知道為什麼你的 popover 瘋了一樣到處亂飛?在 UIPopoverControllerDelegate 協議中有一個新的代理方法讓你能控制它:

-  (void)popoverController:(UIPopoverController *)popoverController
  willRepositionPopoverToRect:(inout CGRect *)rect 
                       inView:(inout UIView **)view

當 popover 錨點是指向一個 UIBarButtonItem 時,UIPopoverController 會做出合適的展現,但是如果你讓它在一個 view 或者 rect 中顯示,你可能就需要實現此方法並正常返回。一個花費了我相當長時間來驗證的問題——如果你通過改變 preferredContentSize 來動態調整你的 popover,那麼這個方法就尤其需要實現。蘋果公司現在對改變 popover 大小的請求更嚴格,如果沒有預留足夠的空間,popover 將會到處移動。

鍵盤支援

蘋果公司不只為我們提供了全新的 framework 用於遊戲控制器,它也給了我們這些鍵盤愛好者一些關注!你會發現新定義的公用鍵,比如 UIKeyInputEscapeUIKeyInputUpArrow,可以使用全新的 UIKeyCommand 類截查。在 iOS 7 之前,只能通過一些難以言表的手段來處理鍵盤命令,現在,就讓我們操起藍芽鍵盤試試看我們能用這個做什麼!

開始之前,你需要對響應鏈(responder chain)有個瞭解。你的 UIApplication 繼承自 UIResponderUIViewUIViewController 也是如此。如果你曾經處理過 UIMenuItem 並且沒有使用我的基於塊的包裝的話,那麼你對此已經有所瞭解。事件先被髮送到最上層的響應者,然後一級級往下傳遞直到 UIApplication。為了捕獲按鍵命令,你需要告訴系統你關心哪些按鍵命令(而不是全捕獲)。為了完成這個,你需要重寫 keyCommands 這個新屬性:

- (NSArray *)keyCommands 
{
    return @[[UIKeyCommand keyCommandWithInput:@"f"
                                 modifierFlags:UIKeyModifierCommand  
                                        action:@selector(searchKeyPressed:)]];
}

- (void)searchKeyPressed:(UIKeyCommand *)keyCommand 
{
    // 響應事件
}

現在可別太激動,需要注意的是,這個方法只在鍵盤可見時有效(比如有類似 UITextView 這樣的物件作為第一響應者時)。對於全域性熱鍵,你仍然需要用上面提到的 hack 方法。除去那些,這個解決途徑還是很優雅的。不要覆蓋類似 cmd-V 這種系統的快捷鍵,它會被自動對映到 paste: 方法。

還有一些新的預定義的響應者行為:

- (void)increaseSize:(id)sender NS_AVAILABLE_IOS(7_0);
- (void)decreaseSize:(id)sender NS_AVAILABLE_IOS(7_0);

它們分別對應 cmd+ 和 cmd- 命令,用來放大/縮小內容。

匹配鍵盤背景

蘋果公司終於公開了 UIInputView,其中提供了一種方式——使用 UIInputViewStyleKeyboard 來匹配鍵盤樣式。這使得你能編寫自定義的鍵盤或者適應預設樣式的預設鍵盤的擴充套件(工具條)。這個類一開始就存在了,不過現在我們終於可以繞過私有API的方式來使用它了。

如果 UIInputView 是一個 inputView 或者 inputAccessoryView根檢視,它將只顯示一個背景,否則它將是透明的。遺憾的是,這並不能讓你實現一個未填充的分離態的鍵盤,但它仍然比用一個簡單的 UIToolbar 要好。我還沒看到蘋果在何處使用這個新 API,看上去 Safari 裡仍然使用著 UIToolbar

瞭解你的無線電通訊

雖然早在 iOS 4 的時候,大部分的運營商資訊已經在 CTTelephony 暴露了,但它通常只用於特定場景並非十分有用。iOS 7 中,蘋果公司為其添加了一個方法,其中最有用的:currentRadioAccessTechnology。這個方法能告訴你手機是處於較慢的 GPRS 還是高速的 LTE 或者介於其中。目前還沒有方法得到連線速度(當然手機本身也無法獲取這個),但是這足以用來優化一個下載管理器,讓其在 EDGE 下不用嘗試 同時 去下載6張圖片了。

現在還沒有 currentRadioAccessTechnology 的相關文件,為了讓它工作,會遇到一些麻煩和錯誤。當你想要獲取當前網路訊號值,你應當註冊一個 CTRadioAccessTechnologyDidChangeNotification 通知而不是去輪詢這個屬性。為了確切的使 iOS 傳送這些通知,你需要持有一個 CTTelephonyNetworkInfo 的例項,但不要在通知中建立 CTTelephonyNetworkInfo 的例項,否則會 crash。

在這個簡單的例子中,因為在 block 中捕獲 telephonyInfo 將會持有它,所以我就這麼用了:

CTTelephonyNetworkInfo *telephonyInfo = [CTTelephonyNetworkInfo new];
NSLog(@"Current Radio Access Technology: %@", telephonyInfo.currentRadioAccessTechnology);
[NSNotificationCenter.defaultCenter addObserverForName:CTRadioAccessTechnologyDidChangeNotification 
                                                object:nil 
                                                 queue:nil 
                                            usingBlock:^(NSNotification *note) 
{
    NSLog(@"New Radio Access Technology: %@", telephonyInfo.currentRadioAccessTechnology);
}];

當手機從 Edge 環境切換到 3G,日誌輸出應該像這樣:

iOS7Tests[612:60b] Current Radio Access Technology: CTRadioAccessTechnologyEdge
iOS7Tests[612:1803] New Radio Access Technology: (null)
iOS7Tests[612:1803] New Radio Access Technology: CTRadioAccessTechnologyHSDPA

蘋果匯出了所有字串符號,因此可以很簡單的比較和檢測當前的網路資訊。

Core Foundation,Autorelease 和你

Core Foundation 中出現了一個新的輔助方法,它被用於私有呼叫已有數年時間:

CFTypeRef CFAutorelease(CFTypeRef CF_RELEASES_ARGUMENT arg)

它的確做了你所期望的事,讓人費解的是蘋果花了這麼長時間才把它公開。ARC 下,大多數人在處理返回 Core Foundation 物件時是通過轉換成對等的 NS 物件來完成的,如返回一個 NSDictionary,雖然它是一個 CFDictionaryRef,簡單地使用 CFBridgingRelease() 就行了。這樣通常沒問題,除非你返回的沒有可用的對等 NS 物件,如 CFBagRef。你要麼使用 id,這樣會失去型別安全性,要麼你將你的方法重新命名為 createMethod 並考慮所有的記憶體語義,最後使用 CFRelease。還有一些手段,比如這個,使用 non-ARC-file 引數你才能編譯它,但終歸得使用 CFAutorelease()。另外:不要編寫使用蘋果公司名稱空間的程式碼,所有這些自定義的 CF-巨集將來都會被打破的。

圖片解壓縮

當通過 UIImage 展示一張圖片時,在顯示之前需要解壓縮(除非圖片源已經畫素快取了)。對於 JPG/PNG 檔案這會佔用相當可觀的時間並會造成卡頓。iOS 6 以前,通常是通過建立一個位圖上下文,然後在其中畫圖來解決。(參見 AFNetworking 如何處理這個問題)

從 iOS 7 開始,你可以使用 kCGImageSourceShouldCacheImmediately: 強制圖片在建立時直接解壓縮:

+ (UIImage *)decompressedImageWithData:(NSData *)data 
{
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCacheImmediately: @YES});

    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    CFRelease(source);
    return image;
}

剛發現這一點時我很很興奮,但不要高興得太早。在我的測試中,開啟即時快取後效能實際上有所 降低。要麼這個方法最終是在主執行緒中被呼叫的(好像不太可能),要麼感官上的效能下降是因為其在方法 copyImageBlockSetJPEG 中鎖住了,因為這個方法也被用在主執行緒顯示非加密的圖片時。在我的 app 中,我在主執行緒中載入小的預覽圖,在後臺執行緒中載入大型圖,使用了 kCGImageSourceShouldCacheImmediately 後小小的解壓縮阻塞了主執行緒,同時在後臺處理大量開銷昂貴的操作。

還有更多關於圖片解壓縮的卻不是 iOS 7 中的新東西,像 kCGImageSourceShouldCache,它用來控制系統自動解除安裝解壓縮圖片資料的能力。確保你將它設定為 YES,否則所有的工作都將沒有意義。有趣的是,蘋果在 64-bit 執行時的系統中將 kCGImageSourceShouldCache預設值 從 NO 改為了 YES。

盜版檢查

蘋果添加了一個方式,通過 NSBunble 上的新方法 appStoreReceiptURL 來獲取和驗證 Lion 系統上 App Store 的收據,現在終於也移植到了 iOS 上了。這使得你可以檢查你的應用是合法購買的還是被破解了的。檢查收據還有另一個重要的原因,它包含了 初始購買日期,這點對於把你的應用從付費模式遷移到免費+應用內付費模式很有幫助。你可以根據這個初始購買日期來決定額外內容對於你的使用者是免費(因為他們已經付過費了)還是收費的。

收據還允許你檢查應用程式是否通過批量購買計劃購買以及該許可證是否仍有效,有一個名為 SKReceiptPropertyIsVolumePurchase 的屬性標示了該值。

當你呼叫 appStoreReceiptURL 時,你需要特別注意,因為在 iOS 6 上,它還是一個私有 API,你應該在使用者程式碼中先呼叫 doesNotRecognizeSelector:,在呼叫前檢查執行(基礎)版本。在開發期間,這個方法返回的 URL 不會指向一個檔案。你可能需要使用 StoreKit 的 SKReceiptRefreshRequest,這也是 iOS 7 中的新東西,用它來下載證書。使用一個至少有過一次購買的測試使用者,否則它將沒法工作:

// 重新整理收據
SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init];
[request setDelegate:self];
[request start];

驗證收據需要大量的程式碼。你需要使用 OpenSSL 和內嵌的蘋果根證書,並且你還要了解一些基本的東西像是證書、PCKS 容器以及 ASN.1。這裡有一些樣例程式碼,但是你不應該讓它這麼簡單——尤其是對那些有“高尚意圖”的人,別隻是拷貝現有的驗證方法,至少做點修改或者編寫你自己的,你應該不希望一個普通的補丁程式就能在數秒內瓦解你的努力吧。

Comic Sans MS

承認吧,你是懷念 Comic Sans MS 的。在 iOS 7 中,Comic Sans MS 終於回來了。iOS 6 中添加了可下載字型,但那時的字型列表很少也不見得有趣。在 iOS 7 中蘋果添加了不少字型,包括 “famous”,它和 PT SansComic Sans MS 有些類似。kCTFontDownloadableAttribute 並沒有在 iOS 6 中宣告,所以 iOS 7 之前它並不真正可用,但蘋果確是在 iOS 6 的時候就已經做了私有聲明瞭。

字型列表是動態變化的,以後可能就會發生變動。蘋果在 Tech Note HT5484 中羅列了一些可用的字型,但這個文件已經過時了,並不能反映 iOS 7 的變化。

這裡顯示了你該如何獲取一個用 CTFontDescriptorRef 標示的可下載的字型陣列:

CFDictionary *descriptorOptions = @{(id)kCTFontDownloadableAttribute : @YES};
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)descriptorOptions);
CFArrayRef fontDescriptors = CTFontDescriptorCreateMatchingFontDescriptors(descriptor, NULL);

系統不會檢查字型是否已存在於磁碟上而將直接返回同樣的列表。另外,這個方法可能會啟用網路並造成阻塞,你不應該在主執行緒中使用它。

使用如下基於塊的 API 來下載字型:

bool CTFontDescriptorMatchFontDescriptorsWithProgressHandler(
         CFArrayRef                          descriptors,
         CFSetRef                            mandatoryAttributes,
         CTFontDescriptorProgressHandler     progressBlock)

這個方法能操作網路並傳遞下載進度資訊來呼叫你的 progressBlock 方法直到下載成功或者失敗。參考蘋果的 DownloadFont 樣例看看如何使用它。

有一些值得注意的地方,這裡的字型只在當前程式執行時有效,下次執行將被重新載入記憶體。因為字型存放在共享空間中,你不能依賴於它們是否可用。很有可能但不能保證地說,系統會清理這個目錄,或者你的程式被拷貝到沒有這個字型的新裝置中,同時你又沒有網路。在 Mac 或是模擬器上,你能根據 kCTFontURLAttribute 獲得字型的絕對路徑,載入速度也會提升,但是在 iOS 上是不行的,因為這個目錄在你的程式之外,你需要再次呼叫 CTFontDescriptorMatchFontDescriptorsWithProgressHandler

你也可以註冊新的 kCTFontManagerRegisteredFontsChangedNotification 通知來跟蹤新字型在何時被載入到了字型登錄檔中。你可以在 WWDC 2013 的 Session 223 “Using Fonts with TextKit”中查詢更多資訊。

這還不夠?

沒關係,iOS 7 的新東西遠不止如此!瞭解一下 NSHipster 你將明白語音合成相關的東西,base64、全新的 NSURLComponentsNSProgress、條形碼掃描、閱讀列表以及 CIDetectorEyeBlink。還有很多我們沒有涵蓋到的,比如蘋果的 iOS 7 API 變化What's new in iOS指南以及 Foundation Release Notes(這些都是基於 OS X的,但是程式碼都是共享的,很多也同樣適用於 iOS)。很多新方法都還沒形成文件,等著你來探究和寫成部落格。