【感謝 @小清新_style 的熱心翻譯(原稿連結)。如果其他朋友也有不錯的原創或譯文,可以嘗試提交到伯樂線上。】
當 iOS7 剛釋出的時候,全世界的蘋果開發人員都立馬嘗試著去編譯他們的app,接著再花上數月的時間來修復任何出現的故障,甚至重做app。這樣的結果,使得人們根本無暇去探究 iOS7 所帶來的新東西。一些明顯而細微的更新,比如說[NSArray firstObject]
,這個方法可追溯到 iOS4 時代,現在被提為公有API,除此之外,還有很多隱藏的特性等著我們去挖掘。
平滑淡入淡出動畫
我這裡要討論的並非新的彈性動畫APIs 或者 UIDynamics,而是一些更細微的東西。CALayer增加了兩個新方法:allowsGroupOpacity
和allowsEdgeAntialiasing
。現在,組不透明度(group opacity)不再是什麼新鮮的東西了。iOS會多次使用存在於 Info.plist 中的鍵UIViewGroupOpacity並可在應用程式範圍內啟用或禁用它。對於大多數apps而言,這(譯註:啟用)並非所期望的,因為它會降低整體效能。在 iOS7 中,用 SDK7 所連結的程式,這項屬性預設是啟用的。當它被啟用時,一些動畫將會變得不流暢,它也可以在layer層上被控制。
一個有趣的細節,如果allowsGroupOpacity
啟用的話,_UIBackdropView(在UIToolbar或者UIPopoverView中的背景檢視)不能對其模糊進行動畫處理,所以當你做一個alpha轉換時,你可能會臨時禁用這項屬性。因為這會降低動畫體驗,你可以回退到舊的方式然後在動畫期間臨時啟用shouldRasterize
。別忘了設定適當的rasterizationScale
,否則在retina的裝置上這些檢視會成鋸齒狀。
如果你想要複製的 Safari 顯示所有選項卡時的動畫,那麼邊緣抗鋸齒屬性將變得非常有用。
阻塞動畫
一個小但非常有用的新方法[UIView performWithoutAnimation:]
。它是一個簡單的封裝,先檢查動畫當前是否啟用,然後禁止動畫,執行塊語句,最後重新啟用動畫。一個需要說明的地方是,它並不會阻塞基於 CoreAnimation 的動畫。因此,不用急於將你的方法呼叫從:
1 2 3 4 |
[CATransaction begin]; [CATransaction setDisableActions:YES]; view.frame = CGRectMake(...); [CATransaction commit]; |
替換為:
1 2 3 |
[UIView performWithoutAnimation:^{ view.frame = CGRectMake(...); }]; |
但是,絕大多數情況下這樣也能工作的很好,只要你不直接處理CALayers。
iOS7 中,我有很多程式碼路徑(主要是 UITableViewCells)需要額外的保護,防止意外的動畫,例如,如果一個彈窗的大小調整了,那麼同時顯示中的表檢視將因為高度的變化而載入新的cell。我通常的做法是將整個 layoutSubviews 的程式碼包紮到一個動畫塊中:
1 2 3 4 5 6 7 8 |
- (void)layoutSubviews { // Otherwise the popover animation could leak into our cells on iOS 7 legacy mode. [UIView performWithoutAnimation:^{ [super layoutSubviews]; _renderView.frame = self.bounds; }]; } |
處理長表檢視
UITableView 非常快速高效,除非你開始使用tableView:heightForRowAtIndexPath:
,它會開始為你表中任意元素呼叫此方法,即便沒有可視物件,就比如其內在的UIScrollView只是去獲取正確的contentSize
。此前有一些變通方法,但都不好用。iOS7 中,蘋果公司終於承認這一問題,並新增tableView:estimatedHeightForRowAtIndexPath:
,這個方法延遲了實際滾動時間成本的大部分。如果你不知道一個cell的大小,返回UITableViewAutomaticDimension
即可。
對於節頭/尾(section headers/footers),現在也有類似的API了。
UISearchDisplayController
蘋果的 search controller 使用了新的技巧來簡化移動 search bar 到 navigation bar 的過程。啟用 displaysSearchBarInNavigationBar
就可以了(除非你還要用到 scope bar,我只能說你真不幸)。我倒是很喜歡這麼做,但比較遺憾的是,iOS7 上的 UISearchDisplayController 貌似被摧殘的比較嚴重,尤其是iPad。蘋果公司看上去像是沒時間處理這個問題的樣子(原文:Apple seems to have run out of time),對於顯示的搜尋結果並不會隱藏實際的表檢視。在 iOS7 之前,這並沒有問題,但是現在 searchResultsTableView 有一個透明的背景色,使它看上去相當糟糕。作為一種變通方法,你可以設定不透明色或者取道於富於技巧的手段來獲得你所期望的。關於這個控制元件會出現各種各樣的結果,當使用displaysSearchBarInNavigationBar
時甚至不會展示搜尋表檢視。
你的結果可能有所不同,但我是使用了一些手段來讓displaysSearchBarInNavigationBar
工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
- (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 requires a cruel workaround to show the search table view. if (PSPDFIsUIKitFlatMode()) { if (!self.originalTableView) self.originalTableView = self.tableView; self.view = controller.searchResultsTableView; controller.searchResultsTableView.contentInset = UIEdgeInsetsZero; // Remove 64 pixel gap } } - (void)searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView { [self restoreOriginalTableView]; } |
這裡,別忘了在viewWillDisappear
中呼叫restoreOriginalTableView
,否則會傳送crash。
記住這是唯一的解決辦法;可能有不少激進的方法不替換檢視本身,但這個問題確實應該由蘋果公司來修復。(TODO: RADAR!)
分頁
UIWebView 使用了新的技巧來自動分頁帶paginationMode
的網站。有一大堆與此功能相關的新屬性:
1 2 3 4 5 |
@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
。
會飛的 Popovers
想知道為什麼你的popovers瘋了一樣到處亂飛?在UIPopoverControllerDelegate
協議中有一個新的代理方法使你能控制它:
1 2 3 |
- (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView **)view |
當popover錨點是指向一個UIBarButtonItem時,UIPopoverController會有一些動作,但如果你讓它在一個view或者rect中顯示,你可能就需要實現此方法並正常返回。一個花費了我相當長的時間來驗證的問題——如果你通過改變preferredContentSize
來動態調整你的popovers,那麼這個方法就特別要求得以實現。蘋果公司現在對改變popovers大小的請求更嚴格,如果沒有預留足夠的空間,popover將會到處移動。
鍵盤支援
蘋果公司不只為我們提供了全新的framework用於遊戲控制器,它也給了我們這些鍵盤愛好者一些提示!你會發現新定義的公用鍵像 UIKeyInputEscape
或 UIKeyInputUpArrow
,可以使用所有新的 UIKeyCommand
類截查。在 iOS7 之前,只能通過一些難以言表的手段來處理鍵盤命令,現在,就讓我們操起藍芽鍵盤試試看我們能用這個做什麼!
開始之前,你需要對責任者鏈有個瞭解。你的 UIApplication 繼承自 UIResponder,UIView 和 UIViewController 也是如此。如果你處理過 UIMenuItem 並且沒有使用我的基於塊的包裝的話,那麼你已經瞭解了這些。事件先被髮送到最上層的響應者,然後一級級往下傳遞直到 UIApplication 。為了捕獲按鍵命令,你需要告訴系統你關心哪些鍵命令(而不是全捕獲)。為了完成這個,你需要重寫keyCommands
這個新屬性:
1 2 3 4 5 6 7 8 9 10 11 |
- (NSArray *)keyCommands { return @[[UIKeyCommand keyCommandWithInput:@"f" modifierFlags:UIKeyModifierCommand action:@selector(searchKeyPressed:)]]; } - (void)searchKeyPressed:(UIKeyCommand *)keyCommand { // Respond to the event } |
現在可別太激動,需要注意的是,這個方法只在鍵盤可見時有效(比如有類似 UITextView 這樣的物件作為第一響應者時)。對於全域性熱鍵,你仍然需要用上面的方法。除卻那些,這個路徑還是很優雅的。不要覆蓋類似 cmd-V 系統的快捷鍵,它會被自動對映為貼上功能。
還有一些新的預定義的響應行為如:
1 2 |
- (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,貌似它只作為一個 UIToolbar 使用在 Safari 上。
瞭解你的網路
雖然早在 iOS4 的時候,關於網路資訊的大部分已經在 CTTelephony 暴露了,但它通常只用於特定場景並非十分有用。iOS7 中,蘋果公司為其新增了一個方法,其中最有用的:currentRadioAccessTechnology
。這個使你能知曉手機是處於較慢的GPRS還是高速的LTE或者介於其中。目前還沒有方法得到連線速度(當然手機本身也無法獲取這個),但是這足以用來優化一個下載管理器,讓其在EDGE下不用嘗試同時去下載6張圖片了。
現在還沒有currentRadioAccessTechnology
的相關文件,因此存在一些不正規或者錯誤的用法。當你想要獲取當前網路訊號值,你應當註冊一個CTRadioAccessTechnologyDidChangeNotification
通知而不應該去輪詢這個屬性。為了獲取這些通知,你需要使用CTTelephonyNetworkInfo
的一個例項,注意不要在通知中建立 CTTelephonyNetworkInfo 的例項,否則會 crash。
在這個簡單的例子中,我在block中捕獲並持有了 telephonyInfo,大家可以忽略這個:
1 2 3 4 5 6 7 8 9 |
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,log輸出應該像這樣:
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中出現了一個新的方法,它被用於私有呼叫已有數年時間:
1 |
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 檔案這會佔用相當可觀的時間並會造成卡頓。iOS6 以前,通常是建立一個點陣圖上下文,然後在其中畫圖來解決。(參見 AFNetworking 如何處理)。
iOS7 開始,你可以使用kCGImageSourceShouldCacheImmediately:
來強制圖片在建立時立即解壓縮:
1 2 3 4 5 6 7 8 9 10 |
+ (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
中鎖住了,而同時在主執行緒中在顯示非加密的圖片所致。在我的程式中,我在主執行緒中載入小的預覽圖,在後臺執行緒中載入大型圖,使用了kCGImageSourceShouldCacheImmediately
後小小的解壓縮阻塞了主執行緒,同時在後臺處理大量開銷昂貴的操作。
還有更多關於圖片解壓縮相關的卻不是 iOS7 中的新東西,像kCGImageSourceShouldCache
,它用來控制系統自動解除安裝解壓縮的圖片資料的能力。確保你將它設定為YES,否則所有的工作都將沒有意義。有趣的是,蘋果在64bit執行時的系統中將kCGImageSourceShouldCache
的預設值從 NO 改為了 YES。
盜版檢查
蘋果新增了一個方式,通過 NSBunble 上的新方法appStoreReceiptURL
來評估Lion系統上 App Store 的收據,同時也將其移植到了 iOS 上。這使得你可以檢查你的應用是在被合法購買或者已經被破解了。檢查收據還有一個重要的原因,它包含了初始購買日期,這點對於把你的應用從付費模型遷移到免費+應用內付費方式很有幫助意義。你可以根據這個初始購買日期來決定額外內容對於你的使用者是免費的還是收費的。
收據還允許你檢查應用程式是否通過批量購買計劃購買以及該許可證是否仍有效,有一個名為SKReceiptPropertyIsVolumePurchase
的屬性顯示了該值。
當你呼叫appStoreReceiptURL
時,你需要特別注意,因為在 iOS6 上,它還是一個私有API,你應該在使用者程式碼中先呼叫doesNotRecognizeSelector:
,在呼叫前檢查執行(基礎)版本。在開發期間,這個方法返回的 URL 不會是指向一個檔案。你可能需要使用 StoreKit 的SKReceiptRefreshRequest
,這也是 iOS7 中的新東西,用它來下載證書。使用一個至少購買過一次的測試使用者,否則它將沒法工作:
1 2 3 4 |
// Refresh the Receipt SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init]; [request setDelegate:self]; [request start]; |
驗證收據需要大量的程式碼。你需要使用OpenSSL和內嵌的蘋果根證書,並且你還要了解一些基本的東西像是證書、PCKS容器以及ASN.1。這裡有一些樣例程式碼,但是你不應該讓它這麼簡單——別隻是拷貝現有的驗證方法,至少做點修改或者編寫你自己的,你應該不希望一個普通的補丁程式就能在數秒內瓦解你的努力吧。
你絕對應該讀讀蘋果的指南——驗證 Mac App 商店收據,這裡面的大多數都適用於 iOS。蘋果在 WWDC2013 的 Session308 “Using Receipts to Protect Your Digital Sales” 中詳述了“Grand Unified Receipt”的變動。
Comic Sans MS
iOS7 中,終於迎回了 Comic Sans MS。現在,它以可下載的字型被新增到 iOS6 中,但當時的字型列表很少也不見得多麼有趣。在 iOS7 中蘋果新增了不少字型,包括“famous”,它和 PT Sans 或 Comic Sans MS 有些類似。kCTFontDownloadableAttribute
並沒有在 iOS6 中宣告,所以 iOS7 以前它並不真正可用,但蘋果確是在 iOS6 的時候就已經做了私有宣告瞭。
字型列表是動態變化的,以後可能就會發生變動。蘋果在 Tech Note HT5484 中羅列了一些可用的字型,但這個文件已經過時了,同時也不能反映 iOS7 的變化。
這裡顯示了你該如何獲取一個用CTFontDescriptorRef
標示可下載的字型陣列:
1 2 3 |
CFDictionary *descriptorOptions = @{(id)kCTFontDownloadableAttribute : @YES}; CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)descriptorOptions); CFArrayRef fontDescriptors = CTFontDescriptorCreateMatchingFontDescriptors(descriptor, NULL); |
系統不會檢查字型是否已存在於磁碟上而將直接返回同樣的列表。另外,這個方法可能會啟用網路並造成阻塞,你不應該在主執行緒中使用它。
使用如下基於塊的 API 來下載字型:
1 2 3 4 |
bool CTFontDescriptorMatchFontDescriptorsWithProgressHandler( CFArrayRef descriptors, CFSetRef mandatoryAttributes, CTFontDescriptorProgressHandler progressBlock) |
這個方法能操作網路並傳遞下載進度資訊來呼叫你的progressBlock
方法直到下載成功或者失敗。參考蘋果的 DownloadFont 樣例看如何使用它。
有一些值得注意的地方,這裡的字型只在當前程式週期內有效,下次執行將被重新載入記憶體。因為字型存放在共享空間中,你不能依賴於它們是否可用。很有可能也不能保證的說,系統會清理這個目錄,或者你的程式被拷貝到新的裝置環境中,而這時又沒有這個字型存在,同時當前處於沒有網路的環境中。在 Mac 或是模擬器上,你能根據kCTFontURLAttribute
獲得字型的絕對路徑,載入速度也會提升,但是在 iOS 上是不可能的,因為這個目錄在你程式之外,你需要再次呼叫CTFontDescriptorMatchFontDescriptorsWithProgressHandler
。
你也可以註冊新的kCTFontManagerRegisteredFontsChangedNotification
通知來跟蹤新字型在何時載入到了字型登錄檔中。你可以在 WWDC2013 的 Session223 “Using Fonts with TextKit”中查詢更多資訊。
這還不夠?
沒關係,iOS7 的新東西遠不止如此!瞭解一下 NSHipster 你將明白語音合成相關的東西,base64、NSURLComponents、NSProgress、bar codes、reading lists 以及 CIDetectorEyeBlink。還有很多我們沒有涵蓋到的,比如蘋果 iOS7 的 API 變化,iOS 指南的新東西以及 Foundation Release Notes(這些都是服務於 OS X的,但是程式碼都是共享的,也同樣適用於 iOS)。很多方法都還沒形成文件,等著你來探究和 blog。