iOS 7: 隐藏的特性和解决之道

转自:http://test-0x01.logdown.com/posts/159702-ios-7-hidden-gems-and-workarounds

Peter Steinberger / 著
小清新 / 译

当 iOS7 刚发布的时候,全世界的苹果开发人员都立马尝试着去编译他们的app,接着再花上数月的时间来修复任何出现的故障,甚至重建这个app。这样的结果,使得人们根本无暇去探究 iOS7 所带来的新思潮。一些明显而细微的更新,比如说[NSArray firstObject],这个方法可追溯到 iOS4 时代,现在被提为公有API,除此之外,还有很多隐藏的特性等着我们去挖掘。

平滑淡入淡出动画

我这里要讨论的并非新的弹性动画APIs 或者 UIDynamics,而是一些更细微的东西。CALayer增加了两个新方法:allowsGroupOpacityallowsEdgeAntialiasing。现在,组不透明度(group opacity)不再是什么新鲜的东西了。iOS会多次使用存在于 Info.plist 中的键UIViewGroupOpacity并可在应用程序范围内启用或禁用它。对于大多数apps而言,这(译注:启用)并非所期望的,因为它会降低整体性能。在 iOS7 中,用 SDK7 所链接的程序,这项属性默认是启用的。当它被启用时,一些动画将会变得不流畅,它也可以在layer层上被控制。

一个有趣的细节,如果allowsGroupOpacity启用的话,_UIBackdropView(在UIToolbar或者UIPopoverView中的背景视图)不能对其模糊进行动画处理,所以当你做一个alpha转换时,你可能会临时禁用这项属性。因为这会降低动画体验,你可以回退到旧的方式然后在动画期间临时启用shouldRasterize。别忘了设置适当的rasterizationScale,否则在retina的设备上这些视图会成锯齿状。

如果你想要复制的 Safari 显示所有选项卡时的动画,那么边缘抗锯齿属性将变得非常有用。

阻塞动画

一个小但非常有用的新方法[UIView performWithoutAnimation:]。它是一个简单的封装,先检查动画当前是否启用,然后禁止动画,执行块语句,最后重新启用动画。一个需要说明的地方是,它并不会阻塞基于 CoreAnimation 的动画。因此,不用急于将你的方法调用从:

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

替换为:

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

但是,绝大多数情况下这样也能工作的很好,只要你不直接处理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即可。

对于节头/尾(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工作的:

- (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的网站。有一大堆与此功能相关的新属性:

@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协议中有一个新的代理方法使你能控制它:

-     (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这个新属性:

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

- (void)searchKeyPressed:(UIKeyCommand *)keyCommand 
{
    // Respond to the event
}

responder-chain.png

现在可别太激动,需要注意的是,这个方法只在键盘可见时有效(比如有类似 UITextView 这样的对象作为第一响应者时)。对于全局热键,你仍然需要用上面的方法。除却那些,这个路径还是很优雅的。不要覆盖类似 cmd-V 系统的快捷键,它会被自动映射为粘贴功能。
还有一些新的预定义的响应行为如:

- (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,大家可以忽略这个:

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中出现了一个新的方法,它被用于私有调用已有数年时间:

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:来强制图片在创建时立即解压缩:

+ (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后小小的解压缩阻塞了主线程,同时在后台处理大量开销昂贵的操作。

image-decompression.png

还有更多关于图片解压缩相关的却不是 iOS7 中的新东西,像kCGImageSourceShouldCache,它用来控制系统自动卸载解压缩的图片数据的能力。确保你将它设置为YES,否则所有的工作都将没有意义。有趣的是,苹果在64bit运行时的系统中将kCGImageSourceShouldCache的默认值从 NO 改为了 YES。

盗版检查

苹果添加了一个方式,通过 NSBunble 上的新方法appStoreReceiptURL来评估Lion系统上 App Store 的收据,同时也将其移植到了 iOS 上。这使得你可以检查你的应用是在被合法购买或者已经被破解了。检查收据还有一个重要的原因,它包含了初始购买日期,这点对于把你的应用从付费模型迁移到免费+应用内付费方式很有帮助意义。你可以根据这个初始购买日期来决定额外内容对于你的用户是免费的还是收费的。

收据还允许你检查应用程序是否通过批量购买计划购买以及该许可证是否仍有效,有一个名为SKReceiptPropertyIsVolumePurchase的属性显示了该值。

当你调用appStoreReceiptURL时,你需要特别注意,因为在 iOS6 上,它还是一个私有API,你应该在用户代码中先调用doesNotRecognizeSelector:,在调用前检查运行(基础)版本。在开发期间,这个方法返回的 URL 不会是指向一个文件。你可能需要使用 StoreKit 的SKReceiptRefreshRequest,这也是 iOS7 中的新东西,用它来下载证书。使用一个至少购买过一次的测试用户,否则它将没法工作:

// 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 的时候就已经做了私有声明了。

comic-sans-ms.png

字体列表是动态变化的,以后可能就会发生变动。苹果在 Tech Note HT5484 中罗列了一些可用的字体,但这个文档已经过时了,同时也不能反映 iOS7 的变化。

这里显示了你该如何获取一个用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通知来跟踪新字体在何时载入到了字体注册表中。你可以在 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。


你可能感兴趣的:(ios7)