當 iOS 7 剛發(fā)布的時候,全世界的蘋果開發(fā)人員都立馬嘗試著去編譯他們的 app,接著再花上數月的時間來修復任何出現(xiàn)的錯誤,甚至從頭開始重建這個 app。這樣的結果,使得人們根本無暇去探究 iOS 7 所帶來的新思想。除開一些明顯而細微的更新,比如說 NSArray 的 firstObject 方法——這個方法可追溯到 iOS 4 時代,現(xiàn)在被提為公有 API——還有很多隱藏的技巧等著我們去挖掘。
我在這里要討論的并非新的彈性動畫 API 或者 UIDynamics,而是一些更細微的東西。CALayer 增加了兩個新方法:allowsGroupOpacity 和 allowsEdgeAntialiasing?,F(xiàn)在,組不透明度(group opacity)不再是什么新鮮的東西了。iOS 會多次使用存在于 Info.plist 中的鍵 UIViewGroupOpacity 并可在應用程序范圍內啟用或禁用它。對于大多數 app 而言,這(譯注:啟用)并非所期望的,因為它會降低整體性能。在 iOS 7 中,用 SDK 7 所鏈接的程序,這項屬性默認是啟用的。當它被啟用時,一些動畫將會變得不流暢,它也可以在 layer 層上被控制。
一個有趣的細節(jié),如果 allowsGroupOpacity 啟用的話,_UIBackdropView(被用作 UIToolbar 或者 UIPopoverView 的背景視圖)不能對其模糊進行動畫處理,所以當你做一個 alpha 轉換時,你可能會臨時禁用這項屬性。因為這會降低動畫體驗,你可以回到舊的方式然后在動畫期間臨時啟用 shouldRasterize。別忘了設置適當的 rasterizationScale,否則在 retina 的設備上這些視圖會成鋸齒狀(pixelerated)。
如果你想要復制 Safari 顯示所有選項卡時的動畫,那么邊緣抗鋸齒屬性將變得非常有用。
有一個小但是非常有用的新方法 [UIView performWithoutAnimation:]。它是一個簡單的封裝,先檢查動畫當前是否啟用,如果是則停用動畫,執(zhí)行塊語句,然后重新啟用動畫。一個需要說明的地方是,它并 不會 阻塞基于 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 的傳統(tǒng)模式下彈窗動畫會滲入我們的單元格
[UIView performWithoutAnimation:^{
[super layoutSubviews];
_renderView.frame = self.bounds;
}];
}
UITableView 非??焖俑咝В悄汩_始使用 tableView:heightForRowAtIndexPath:,它會開始為你表中 每一個 元素調用此方法,即便沒有可視對象——這是為了讓其下層的 UIScrollView 能獲取正確的 contentSize。此前有一些變通方法,但都不好用。iOS 7 中,蘋果公司終于承認這一問題,并添加了 tableView:estimatedHeightForRowAtIndexPath:,這個方法把絕大部分計算成本推遲到實際滾動的時候。如果你完全不知道一個 cell 的大小,返回 UITableViewAutomaticDimension 就行了。
對于段頭/尾(section headers/footers),現(xiàn)在也有類似的 API 了。
蘋果的 search controller 使用了新的技巧來簡化移動 search bar 到 navigation bar 的過程。啟用 displaysSearchBarInNavigationBar 就可以了(除非你還在用 scope bar,那你就太不幸了)。我倒是很喜歡這么做,但遺憾的是,iOS 7 上的 UISearchDisplayController 貌似被破壞得相當嚴重,尤其在 iPad 上。蘋果公司看上去像是沒時間處理這個問題,對于顯示的搜索結果并不會隱藏實際的表視圖。在 iOS 7 之前,這不算問題,但是現(xiàn)在 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 現(xiàn)在可以對帶有 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 瘋了一樣到處亂飛?在 UIPopoverControllerDelegate 協(xié)議中有一個新的代理方法讓你能控制它:
- (void)popoverController:(UIPopoverController *)popoverController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view
當 popover 錨點是指向一個 UIBarButtonItem 時,UIPopoverController 會做出合適的展現(xiàn),但是如果你讓它在一個 view 或者 rect 中顯示,你可能就需要實現(xiàn)此方法并正常返回。一個花費了我相當長時間來驗證的問題——如果你通過改變 preferredContentSize 來動態(tài)調整你的 popover,那么這個方法就尤其需要實現(xiàn)。蘋果公司現(xiàn)在對改變 popover 大小的請求更嚴格,如果沒有預留足夠的空間,popover 將會到處移動。
蘋果公司不只為我們提供了全新的 framework 用于游戲控制器,它也給了我們這些鍵盤愛好者一些關注!你會發(fā)現(xiàn)新定義的公用鍵,比如 UIKeyInputEscape 或 UIKeyInputUpArrow,可以使用全新的 UIKeyCommand 類截查。在 iOS 7 之前,只能通過一些難以言表的手段來處理鍵盤命令,現(xiàn)在,就讓我們操起藍牙鍵盤試試看我們能用這個做什么!
開始之前,你需要對響應鏈(responder chain)有個了解。你的 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
{
// 響應事件
}
http://wiki.jikexueyuan.com/project/objc/images/5-14.png" alt="" />
現(xiàn)在可別太激動,需要注意的是,這個方法只在鍵盤可見時有效(比如有類似 UITextView 這樣的對象作為第一響應者時)。對于全局熱鍵,你仍然需要用上面提到的 hack 方法。除去那些,這個解決途徑還是很優(yōu)雅的。不要覆蓋類似 cmd-V 這種系統(tǒng)的快捷鍵,它會被自動映射到 paste: 方法。
還有一些新的預定義的響應者行為:
- (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,看上去 Safari 里仍然使用著 UIToolbar。
雖然早在 iOS 4 的時候,大部分的運營商信息已經在 CTTelephony 暴露了,但它通常只用于特定場景并非十分有用。iOS 7 中,蘋果公司為其添加了一個方法,其中最有用的:currentRadioAccessTechnology。這個方法能告訴你手機是處于較慢的 GPRS 還是高速的 LTE 或者介于其中。目前還沒有方法得到連接速度(當然手機本身也無法獲取這個),但是這足以用來優(yōu)化一個下載管理器,讓其在 EDGE 下不用嘗試 同時 去下載6張圖片了。
現(xiàn)在還沒有 currentRadioAccessTechnology 的相關文檔,為了讓它工作,會遇到一些麻煩和錯誤。當你想要獲取當前網絡信號值,你應當注冊一個 CTRadioAccessTechnologyDidChangeNotification 通知而不是去輪詢這個屬性。為了確切的使 iOS 發(fā)送這些通知,你需要持有一個 CTTelephonyNetworkInfo 的實例,但不要在通知中創(chuàng)建 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 環(huán)境切換到 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 中出現(xiàn)了一個新的輔助方法,它被用于私有調用已有數年時間:
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 以前,通常是通過創(chuàng)建一個位圖上下文,然后在其中畫圖來解決。(參見 AFNetworking 如何處理這個問題)。
從 iOS 7 開始,你可以使用 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)這一點時我很很興奮,但不要高興得太早。在我的測試中,開啟即時緩存后性能實際上有所 降低。要么這個方法最終是在主線程中被調用的(好像不太可能),要么感官上的性能下降是因為其在方法 copyImageBlockSetJPEG 中鎖住了,因為這個方法也被用在主線程顯示非加密的圖片時。在我的 app 中,我在主線程中加載小的預覽圖,在后臺線程中加載大型圖,使用了 kCGImageSourceShouldCacheImmediately 后小小的解壓縮阻塞了主線程,同時在后臺處理大量開銷昂貴的操作。
http://wiki.jikexueyuan.com/project/objc/images/5-15.png" alt="" />
還有更多關于圖片解壓縮的卻不是 iOS 7 中的新東西,像 kCGImageSourceShouldCache,它用來控制系統(tǒng)自動卸載解壓縮圖片數據的能力。確保你將它設置為 YES,否則所有的工作都將沒有意義。有趣的是,蘋果在 64-bit 運行時的系統(tǒng)中將 kCGImageSourceShouldCache 的 默認值 從 NO 改為了 YES。
蘋果添加了一個方式,通過 NSBunble 上的新方法 appStoreReceiptURL 來獲取和驗證 Lion 系統(tǒng)上 App Store 的收據,現(xiàn)在終于也移植到了 iOS 上了。這使得你可以檢查你的應用是合法購買的還是被破解了的。檢查收據還有另一個重要的原因,它包含了 初始購買日期,這點對于把你的應用從付費模式遷移到免費+應用內付費模式很有幫助。你可以根據這個初始購買日期來決定額外內容對于你的用戶是免費(因為他們已經付過費了)還是收費的。
收據還允許你檢查應用程序是否通過批量購買計劃購買以及該許可證是否仍有效,有一個名為 SKReceiptPropertyIsVolumePurchase 的屬性標示了該值。
當你調用 appStoreReceiptURL 時,你需要特別注意,因為在 iOS 6 上,它還是一個私有 API,你應該在用戶代碼中先調用 doesNotRecognizeSelector:,在調用前檢查運行(基礎)版本。在開發(fā)期間,這個方法返回的 URL 不會指向一個文件。你可能需要使用 StoreKit 的 SKReceiptRefreshRequest,這也是 iOS 7 中的新東西,用它來下載證書。使用一個至少有過一次購買的測試用戶,否則它將沒法工作:
// 刷新收據
SKReceiptRefreshRequest *request = [[SKReceiptRefreshRequest alloc] init];
[request setDelegate:self];
[request start];
驗證收據需要大量的代碼。你需要使用 OpenSSL 和內嵌的蘋果根證書,并且你還要了解一些基本的東西像是證書、PCKS 容器以及 ASN.1。這里有一些樣例代碼,但是你不應該讓它這么簡單——尤其是對那些有“高尚意圖”的人,別只是拷貝現(xiàn)有的驗證方法,至少做點修改或者編寫你自己的,你應該不希望一個普通的補丁程序就能在數秒內瓦解你的努力吧。
你絕對應該讀讀蘋果的指南——驗證 Mac App 商店收據,這里面的大多數都適用于 iOS。蘋果在 WWDC 2013 的 Session 308 “Using Receipts to Protect Your Digital Sales” 中詳述了通過新加入的“Grand Unified Receipt”而帶來的變動。
承認吧,你是懷念 Comic Sans MS 的。在 iOS 7 中,Comic Sans MS 終于回來了。iOS 6 中添加了可下載字體,但那時的字體列表很少也不見得有趣。在 iOS 7 中蘋果添加了不少字體,包括 “famous”,它和 PT Sans 或 Comic Sans MS 有些類似。kCTFontDownloadableAttribute 并沒有在 iOS 6 中聲明,所以 iOS 7 之前它并不真正可用,但蘋果確是在 iOS 6 的時候就已經做了私有聲明了。
http://wiki.jikexueyuan.com/project/objc/images/5-16.png" alt="" />
字體列表是動態(tài)變化的,以后可能就會發(fā)生變動。蘋果在 Tech Note HT5484 中羅列了一些可用的字體,但這個文檔已經過時了,并不能反映 iOS 7 的變化。
這里顯示了你該如何獲取一個用 CTFontDescriptorRef 標示的可下載的字體數組:
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)會清理這個目錄,或者你的程序被拷貝到沒有這個字體的新設備中,同時你又沒有網絡。在 Mac 或是模擬器上,你能根據 kCTFontURLAttribute 獲得字體的絕對路徑,加載速度也會提升,但是在 iOS 上是不行的,因為這個目錄在你的程序之外,你需要再次調用 CTFontDescriptorMatchFontDescriptorsWithProgressHandler。
你也可以注冊新的 kCTFontManagerRegisteredFontsChangedNotification 通知來跟蹤新字體在何時被載入到了字體注冊表中。你可以在 WWDC 2013 的 Session 223 “Using Fonts with TextKit”中查找更多信息。
沒關系,iOS 7 的新東西遠不止如此!了解一下 NSHipster 你將明白語音合成相關的東西,base64、全新的 NSURLComponents、NSProgress、條形碼掃描、閱讀列表以及 CIDetectorEyeBlink。還有很多我們沒有涵蓋到的,比如蘋果的 iOS 7 API 變化,指南以及 Foundation Release Notes(這些都是基于 OS X的,但是代碼都是共享的,很多也同樣適用于 iOS)。很多新方法都還沒形成文檔,等著你來探究和寫成博客。