iOS 7: 隱藏的特性和解決之道
當 iOS7 剛發(fā)布的時候,全世界的蘋果開發(fā)人員都立馬嘗試著去編譯他們的app,接著再花上數(shù)月的時間來修復任何出現(xiàn)的故障,甚至重做app。這樣的結果,使得人們根本無暇去探究 iOS7 所帶來的新東西。一些明顯而細微的更新,比如說[NSArray firstObject],這個方法可追溯到 iOS4 時代,現(xiàn)在被提為公有API,除此之外,還有很多隱藏的特性等著我們去挖掘。
平滑淡入淡出動畫
我這里要討論的并非新的彈性動畫APIs 或者 UIDynamics,而是一些更細微的東西。CALayer增加了兩個新方法:allowsGroupOpacity和allowsEdgeAntialiasing?,F(xiàn)在,組不透明度(group opacity)不再是什么新鮮的東西了。iOS會多次使用存在于 Info.plist 中的鍵UIViewGroupOpacity并可在應用程序范圍內啟用或禁用它。對于大多數(shù)apps而言,這(譯注:啟用)并非所期望的,因為它會降低整體性能。在 iOS7 中,用 SDK7 所鏈接的程序,這項屬性默認是啟用的。當它被啟用時,一些動畫將會變得不流暢,它也可以在layer層上被控制。
一個有趣的細節(jié),如果allowsGroupOpacity啟用的話,_UIBackdropView(在UIToolbar或者UIPopoverView中的背景視圖)不能對其模糊進行動畫處理,所以當你做一個alpha轉換時,你可能會臨時禁用這項屬性。因為這會降低動畫體驗,你可以回退到舊的方式然后在動畫期間臨時啟用shouldRasterize。別忘了設置適當?shù)膔asterizationScale,否則在retina的設備上這些視圖會成鋸齒狀。
如果你想要復制的 Safari 顯示所有選項卡時的動畫,那么邊緣抗鋸齒屬性將變得非常有用。
阻塞動畫
一個小但非常有用的新方法[UIView performWithoutAnimation:]。它是一個簡單的封裝,先檢查動畫當前是否啟用,然后禁止動畫,執(zhí)行塊語句,最后重新啟用動畫。一個需要說明的地方是,它并不會阻塞基于 CoreAnimation 的動畫。因此,不用急于將你的方法調用從:
[CATransaction begin];
[CATransaction setDisableActions:YES];
view.frame = CGRectMake(...);
[CATransaction commit];
替換為:
1
2
3[UIView performWithoutAnimation:^{
view.frame = CGRectMake(...);
}];
但是,絕大多數(shù)情況下這樣也能工作的很好,只要你不直接處理CALayers。
iOS7 中,我有很多代碼路徑(主要是 UITableViewCells)需要額外的保護,防止意外的動畫,例如,如果一個彈窗的大小調整了,那么同時顯示中的表視圖將因為高度的變化而加載新的cell。我通常的做法是將整個 layoutSubviews 的代碼包扎到一個動畫塊中:
(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即可。
對于節(jié)頭/尾(section headers/footers),現(xiàn)在也有類似的API了。
UISearchDisplayController
蘋果的 search controller 使用了新的技巧來簡化移動 search bar 到 navigation bar 的過程。啟用 displaysSearchBarInNavigationBar 就可以了(除非你還要用到 scope bar,我只能說你真不幸)。我倒是很喜歡這么做,但比較遺憾的是,iOS7 上的 UISearchDisplayController 貌似被摧殘的比較嚴重,尤其是iPad。蘋果公司看上去像是沒時間處理這個問題的樣子(原文:Apple seems to have run out of time),對于顯示的搜索結果并不會隱藏實際的表視圖。在 iOS7 之前,這并沒有問題,但是現(xiàn)在 searchResultsTableView 有一個透明的背景色,使它看上去相當糟糕。作為一種變通方法,你可以設置不透明色或者取道于富于技巧的手段來獲得你所期望的。關于這個控件會出現(xiàn)各種各樣的結果,當使用displaysSearchBarInNavigationBar時甚至不會展示搜索表視圖。
你的結果可能有所不同,但我是使用了一些手段來讓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 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,否則會發(fā)送crash。
記住這是唯一的解決辦法;可能有不少激進的方法不替換視圖本身,但這個問題確實應該由蘋果公司來修復。(TODO: RADAR!)[!--empirenews.page--]
分頁
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);
現(xiàn)在而言,雖然這可能并非對于大多數(shù)網站都有用,但它肯定是生成簡單的電子書閱讀器或顯示文本的一種更好的方式。加點樂子的話,請嘗試將它設置為UIWebPaginationModeBottomToTop。
會飛的 Popovers
想知道為什么你的popovers瘋了一樣到處亂飛?在UIPopoverControllerDelegate協(xié)議中有一個新的代理方法使你能控制它:
(void)popoverController:(UIPopoverController *)popoverController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view
當popover錨點是指向一個UIBarButtonItem時,UIPopoverController會有一些動作,但如果你讓它在一個view或者rect中顯示,你可能就需要實現(xiàn)此方法并正常返回。一個花費了我相當長的時間來驗證的問題——如果你通過改變preferredContentSize來動態(tài)調整你的popovers,那么這個方法就特別要求得以實現(xiàn)。蘋果公司現(xiàn)在對改變popovers大小的請求更嚴格,如果沒有預留足夠的空間,popover將會到處移動。
鍵盤支持
蘋果公司不只為我們提供了全新的framework用于游戲控制器,它也給了我們這些鍵盤愛好者一些提示!你會發(fā)現(xiàn)新定義的公用鍵像 UIKeyInputEscape 或 UIKeyInputUpArrow,可以使用所有新的 UIKeyCommand 類截查。在 iOS7 之前,只能通過一些難以言表的手段來處理鍵盤命令,現(xiàn)在,就讓我們操起藍牙鍵盤試試看我們能用這個做什么!
開始之前,你需要對責任者鏈有個了解。你的 UIApplication 繼承自 UIResponder,UIView 和 UIViewController 也是如此。如果你處理過 UIMenuItem 并且沒有使用我的基于塊的包裝的話,那么你已經了解了這些。事件先被發(fā)送到最上層的響應者,然后一級級往下傳遞直到 UIApplication 。為了捕獲按鍵命令,你需要告訴系統(tǒng)你關心哪些鍵命令(而不是全捕獲)。為了完成這個,你需要重寫keyCommands這個新屬性:
(NSArray *)keyCommands
{
return @[[UIKeyCommand keyCommandWithInput:@"f"
modifierFlags:UIKeyModifierCommand
action:@selector(searchKeyPressed:)]];
}
- (void)searchKeyPressed:(UIKeyCommand *)keyCommand
{
// Respond to the event
}
現(xiàn)在可別太激動,需要注意的是,這個方法只在鍵盤可見時有效(比如有類似 UITextView 這樣的對象作為第一響應者時)。對于全局熱鍵,你仍然需要用上面的方法。除卻那些,這個路徑還是很優(yōu)雅的。不要覆蓋類似 cmd-V 系統(tǒng)的快捷鍵,它會被自動映射為粘貼功能。
還有一些新的預定義的響應行為如:
1
2- (void)increaseSize:(id)sender NS_AVAILABLE_IOS(7_0);
- (void)decreaseSize:(id)sender NS_AVAILABLE_IOS(7_0);
它們分別對應著 cmd+ 和 cmd- 命令,用來放大/縮小內容。
匹配鍵盤背景
蘋果公司終于公開了 UIInputView,其中提供了一種方式——使用UIInputViewStyleKeyboard來匹配鍵盤樣式。這使得你可以編寫自定義的鍵盤或者帶默認樣式的默認鍵盤擴展(工具條)。這個類以前就存在了,不過現(xiàn)在我們終于可以繞過私有API的方式來使用它了。
如果 UIInputView 是一個 inputView 或者 inputAccessoryView 的根視圖,它將只顯示一個背景,否則它將是透明的。遺憾的是,這并不能讓你實現(xiàn)一個未填充的分離態(tài)的鍵盤,但它仍然比用一個簡單的 UIToolbar 要好。我還沒看到蘋果在何處使用這個新API,貌似它只作為一個 UIToolbar 使用在 Safari 上。
了解你的網絡
雖然早在 iOS4 的時候,關于網絡信息的大部分已經在 CTTelephony 暴露了,但它通常只用于特定場景并非十分有用。iOS7 中,蘋果公司為其添加了一個方法,其中最有用的:currentRadioAccessTechnology。這個使你能知曉手機是處于較慢的GPRS還是高速的LTE或者介于其中。目前還沒有方法得到連接速度(當然手機本身也無法獲取這個),但是這足以用來優(yōu)化一個下載管理器,讓其在EDGE下不用嘗試同時去下載6張圖片了。
現(xiàn)在還沒有currentRadioAccessTechnology的相關文檔,因此存在一些不正規(guī)或者錯誤的用法。當你想要獲取當前網絡信號值,你應當注冊一個CTRadioAccessTechnologyDidChangeNotification通知而不應該去輪詢這個屬性。為了獲取這些通知,你需要使用CTTelephonyNetworkInfo的一個實例,注意不要在通知中創(chuàng)建 CTTelephonyNetworkInfo 的實例,否則會 crash。
在這個簡單的例子中,我在block中捕獲并持有了 telephonyInfo,大家可以忽略這個:
1
2
3
4
5
6
7
8
9CTTelephonyNetworkInfo *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);[!--empirenews.page--]
}];
當手機從edge環(huán)境到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中出現(xiàn)了一個新的方法,它被用于私有調用已有數(shù)年時間:
CFTypeRef CFAutorelease(CFTypeRef CF_RELEASES_ARGUMENT arg)
它確實做了你所期望的事,讓人費解的是蘋果花了這么長時間才把它公開。ARC 下,大多數(shù)人在處理返回 Core Foundation 對象時是通過轉換成對等的 NS 對象來完成的,如 NSDictionary,即便它只是一個 CFDictionaryRef 然后簡單地 CFBridgingRelease() 。這樣通常沒問題,除非你返回的對等 NS 對象不可用時,如 CFBagRef。你要么使用 id,這樣會失去類型安全性,要么你將你的方法重命名為 createMethod 并考慮所有的內存語義,最后使用 CFRelease。還有一些手段,比如這個,用 non-ARC-file 標簽然后編譯,但終歸得使用CFAutorelease()。另外:不要編寫使用蘋果公司命名空間的代碼,所有這些自定義的 CF-宏將來都會被打破的。
圖片解壓縮
當通過 UIImage 展示一張圖時,在顯示之前需要解壓縮(除非源已經像素緩存了)。對于 JPG/PNG 文件這會占用相當可觀的時間并會造成卡頓。iOS6 以前,通常是創(chuàng)建一個位圖上下文,然后在其中畫圖來解決。(參見 AFNetworking 如何處理)。
iOS7 開始,你可以使用kCGImageSourceShouldCacheImmediately:來強制圖片在創(chuàng)建時立即解壓縮:
(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;
}
當我剛發(fā)現(xiàn)這一點時確實很興奮,但事實并非如此。在我的測試中,發(fā)現(xiàn)當開啟了即時緩存后性能有明顯的降低。要么這個方法是在主線程中調用的(不太可能),感覺上性能更糟,因為它在方法copyImageBlockSetJPEG中鎖住了,而同時在主線程中在顯示非加密的圖片所致。在我的程序中,我在主線程中加載小的預覽圖,在后臺線程中加載大型圖,使用了kCGImageSourceShouldCacheImmediately后小小的解壓縮阻塞了主線程,同時在后臺處理大量開銷昂貴的操作。
還有更多關于圖片解壓縮相關的卻不是 iOS7 中的新東西,像kCGImageSourceShouldCache,它用來控制系統(tǒng)自動卸載解壓縮的圖片數(shù)據(jù)的能力。確保你將它設置為YES,否則所有的工作都將沒有意義。有趣的是,蘋果在64bit運行時的系統(tǒng)中將kCGImageSourceShouldCache的默認值從 NO 改為了 YES。
盜版檢查
蘋果添加了一個方式,通過 NSBunble 上的新方法appStoreReceiptURL來評估Lion系統(tǒng)上 App Store 的收據(jù),同時也將其移植到了 iOS 上。這使得你可以檢查你的應用是在被合法購買或者已經被破解了。檢查收據(jù)還有一個重要的原因,它包含了初始購買日期,這點對于把你的應用從付費模型遷移到免費+應用內付費方式很有幫助意義。你可以根據(jù)這個初始購買日期來決定額外內容對于你的用戶是免費的還是收費的。
收據(jù)還允許你檢查應用程序是否通過批量購買計劃購買以及該許可證是否仍有效,有一個名為SKReceiptPropertyIsVolumePurchase的屬性顯示了該值。
當你調用appStoreReceiptURL時,你需要特別注意,因為在 iOS6 上,它還是一個私有API,你應該在用戶代碼中先調用doesNotRecognizeSelector:,在調用前檢查運行(基礎)版本。在開發(fā)期間,這個方法返回的 URL 不會是指向一個文件。你可能需要使用 StoreKit 的SKReceiptRefreshRequest,這也是 iOS7 中的新東西,用它來下載證書。使用一個至少購買過一次的測試用戶,否則它將沒法工作:
Refresh the Receipt
SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init];
[request setDelegate:self];
[request start];
驗證收據(jù)需要大量的代碼。你需要使用OpenSSL和內嵌的蘋果根證書,并且你還要了解一些基本的東西像是證書、PCKS容器以及ASN.1。這里有一些樣例代碼,但是你不應該讓它這么簡單——別只是拷貝現(xiàn)有的驗證方法,至少做點修改或者編寫你自己的,你應該不希望一個普通的補丁程序就能在數(shù)秒內瓦解你的努力吧。
你絕對應該讀讀蘋果的指南——驗證 Mac App 商店收據(jù),這里面的大多數(shù)都適用于 iOS。蘋果在 WWDC2013 的 Session308 “Using Receipts to Protect Your Digital Sales” 中詳述了“Grand Unified Receipt”的變動。
Comic Sans MS
iOS7 中,終于迎回了 Comic Sans MS?,F(xiàn)在,它以可下載的字體被添加到 iOS6 中,但當時的字體列表很少也不見得多么有趣。在 iOS7 中蘋果添加了不少字體,包括“famous”,它和 PT Sans 或 Comic Sans MS 有些類似。kCTFontDownloadableAttribute并沒有在 iOS6 中聲明,所以 iOS7 以前它并不真正可用,但蘋果確是在 iOS6 的時候就已經做了私有聲明了。
字體列表是動態(tài)變化的,以后可能就會發(fā)生變動。蘋果在 Tech Note HT5484 中羅列了一些可用的字體,但這個文檔已經過時了,同時也不能反映 iOS7 的變化。[!--empirenews.page--]
這里顯示了你該如何獲取一個用CTFontDescriptorRef標示可下載的字體數(shù)組:
CFDictionary *descriptorOptions = @{(id)kCTFontDownloadableAttribute : @YES};
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)descriptorOptions);
CFArrayRef fontDescriptors = CTFontDescriptorCreateMatchingFontDescriptors(descriptor, NULL);
系統(tǒng)不會檢查字體是否已存在于磁盤上而將直接返回同樣的列表。另外,這個方法可能會啟用網絡并造成阻塞,你不應該在主線程中使用它。
使用如下基于塊的 API 來下載字體:
bool CTFontDescriptorMatchFontDescriptorsWithProgressHandler(
CFArrayRef descriptors,
CFSetRef mandatoryAttributes,
CTFontDescriptorProgressHandler progressBlock)
這個方法能操作網絡并傳遞下載進度信息來調用你的progressBlock方法直到下載成功或者失敗。參考蘋果的 DownloadFont 樣例看如何使用它。
有一些值得注意的地方,這里的字體只在當前程序周期內有效,下次運行將被重新載入內存。因為字體存放在共享空間中,你不能依賴于它們是否可用。很有可能也不能保證的說,系統(tǒng)會清理這個目錄,或者你的程序被拷貝到新的設備環(huán)境中,而這時又沒有這個字體存在,同時當前處于沒有網絡的環(huán)境中。在 Mac 或是模擬器上,你能根據(jù)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。