App Transport Security(ATS)

来源:互联网 发布:为什么淘宝上没有慢充 编辑:程序博客网 时间:2024/04/30 21:15

最近下载iOS 9 GM版,然后跑了下我们的应用,发现有些网络请求失效了。先前在WWDC 2015上了解到iOS 9将要求网络请求使用HTTPS协议,但一直没有在iOS 9 beta版上跑过。现在这个问题突显出来了,所以搜了一些博文研究了一下。

我们知道,Apple在安全及用户隐私方面做了很多工作,包括沙盒机制、代码签名、禁用私有API等。而在今年6月份的WWDC 2015上,Apple又提出了App Transport Security(ATS)的概念。这一特性的主要意图是为我们的App与服务器之间提供一种安全的通信方式,以防止中间人窃听、篡改传输的数据。这一特性在iOS 9+和OS X 10.11+中是默认的支持项。这一概念的提出,也将意味着Apple将会慢慢转向支持HTTPS,而可能放弃HTTP。

App Transport Security技术要求

我们先来看看ATS的技术要求(参考App Transport Security Technote):

  • The server must support at least Transport Layer Security (TLS) protocol version 1.2.
  • Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.)
  • Certificates must be signed using a SHA256 or better signature hash algorithm, with either a 2048 bit or greater RSA key or a 256 bit or greater Elliptic-Curve (ECC) key.

可以看到服务端必须支持TLS 1.2或以上版本;必须使用支持前向保密的密码;证书必须使用SHA-256或者更好的签名hash算法来签名,如果证书无效,则会导致连接失败。

Apple认为这是目前保证通信安全性的最佳实践,特别是使用TLS 1.2和前向保密。当然,相信Apple也会与时俱进,不断的修正ATS,以保证网络通信的安全性。

默认配置

在iOS 9+和OS X 10.11+中,如果我们的App使用了NSURLConnectionCFURL 或者NSURLSession相关的API来进行数据通信的话,则默认是通过ATS的方式来传输数据。在此配置下,如果我们使用HTTP来进行通信,则会导致请求失败,并报以下错误:

1
The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

这样意味着如果使用ATS,将无法支持HTTP协议(我们测试了一下,由于我们的登录服务是使用HTTP协议,目前在iOS 9下已无法正常登录)。相信目前还有大量的应用是通过HTTP协议来访问服务器的。而要让所有的应用都转向支持HTTPS,显然是一件费时费力的事(与今年年头所有应用必须支持64位ARM不同,那次只是在客户端层面,而ATS涉及到服务端,影响面更大)。所以苹果提供了一种兼容方案,下面我们就来看看如何处理。

自定义配置

考虑到现实因素,我们可能并不想使用默认配置,或者至少需要一个过渡时期。为此,Apple允许我们在Info.plist文件中来自行配置以修改默认设置(Exceptions),下表是一些键值及对应的类型和说明:

 键类型说明  NSAppTransportSecurityDictionary配置ATS的顶层键值  NSAllowsArbitraryLoadsBoolean这是一个开关键,设置不在NSExceptionDomains列表中的其它域ATS特性。默认值是NO,如果设置为YES,则会关闭其它域的ATS特性。  NSExceptionDomainsDictionary特定域列表  <domain-name-for-exception-as-string>Dictionary需要自定义配置的域名,键是对应的域名,如www.apple.com  NSExceptionMinimumTLSVersionString指定域所需要的TLS的最低版本。有效值包括:TLSv1.0、TLSv1.1、TLSv1.2。默认值是TLSv1.2  NSExceptionRequiresForwardSecrecyBoolean指定域是否需要支持前向保密。默认值是YES  NSExceptionAllowsInsecureHTTPLoadsBoolean指定域的请求是否允许使用不安全的HTTP。使用这个键来访问没有证书,或者证书是自签名、过期或主机名不匹配的证书。默认值为NO,表示需要使用HTTPS。  NSIncludesSubdomainsBoolean指定自定义的值是否应用到域的所有子域中。默认值是NO  NSThirdPartyExceptionMinimumTLSVersionString类似于NSExceptionMinimumTLSVersion键,只不过指定的是应用本身无法控制的第三方组件的域所需要的TLS的最低版本。  NSThirdPartyExceptionRequiresForwardSecrecyBoolean同上。指定第三方组件的域是否需要支持前向保密  NSThirdPartyExceptionAllowsInsecureHTTPLoadsBoolean同上。指定第三方组件的域的请求是否使用HTTPS 

通过设置上面的这些值,就可以精确的配置应用中访问的不同域的ATS特性。如下是WORKING WITH APPLE’S APP TRANSPORT SECURITY中给出的一个配置示例:

image

另外,在这篇文章中,也为我们例举了几种常见的配置,我们一起来看一下:

Example A:所有请求均使用ATS

这当然是默认配置,只需要我们使用NSURLSession, NSURLConnection或者CFURL来做网络请求。当然只有iOS 9.0+以及OS X 10.11+才支持这一特性。

Example B:配置部分域不使用ATS

如果我们希望部分域的请求不使用ATS,则我们可以将这些域放在NSExceptionDomains列表中来进行配置,以修改这些域的ATS默认配置。如果我们希望指定域及其所有子域都禁用ATS,则设置NSExceptionAllowsInsecureHTTPLoads为YES并将NSIncludesSubdomains设置为YES,如下配置:

image

那当然,如果我们不想在指定域完全禁用ATS,则可以设置 NSExceptionRequiresForwardSecrecy 和NSExceptionMinimumTLSVersion 来指定更多的规则。

Example C:禁用ATS,但部分域使用ATS

如果我们想要在应用中禁用ATS特性,则可以设置NSAllowsArbitraryLoads的值为YES,这样所有的请求将不会使用ATS。而如果我们希望部分域使用ATS,则如同Example B中那样来设置指定域的 NSExceptionAllowsInsecureHTTPLoads 的值为NO,这样就要求指定域必须使用ATS来进行数据传输。如下配置:

image

Example D:降级ATS

在一些情况下,我们可能需要使用ATS,但可能现实情况并不完全能够支持ATS的最佳实践。比如我们的服务端支持TLS 1.2,但却不支持前向保密。这种情况下,我们可以让指定域支持ATS,但同时禁用前向保密,这种情况下就可以设置NSExceptionRequiresForwardSecrecy为NO。同样,如果我们希望使用前向保密,但可以TLS的版本只是1.1,则我们可以设置 NSExceptionMinimumTLSVersion 的值为TSLv1.1,如下配置:

image

Example E:完全禁用ATS的更友好的方式

如果想完全禁用ATS,我们可以在Info.plist中简单的设置NSAllowsArbitraryLoads为YES,如下配置:

image

以上几种情况基本上囊括了自定义ATS特性的所有情况。大家可以根据需要来自定义配置。

Certificate Transparency

对于ATS,大部分安全特性都是默认可用的,不过Certificate Transparency是必须配置的。Certificate Transparency的概念在wiki中的解释是:

1
Certificate Transparency (CT) is an experimental IETF open standard and open source framework for monitoring and auditing digital certificates. Through a system of certificate logs, monitors, and auditors, certificate transparency allows website users and domain owners to identify mistakenly or maliciously issued certificates and to identify certificate authorities (CAs) that have gone rogue.

它主要是让web站点的用户和域所有者可以识别出错误的或恶意的证书,以及识别出无效的证书颁发机构。

如果我们的证书支持certificate transparency,那么我们可以设置NSRequiresCertificateTransparency键来启用这一功能。而不如证书不支持certificate transparency,则该功能默认总是关闭的。

小结

Apple提出App Transport Security这一特性,是为了保证用户数据的安全传输。安全因素始终是网络开发中一个重要的因素,相信会有越来越多的站点会转向HTTPS。而Apple作为业内技术的一个风向标,也会带动这一趋势的发展。所以,还不支持HTTPS的筒子们可以行为起来了。

这篇文章更多的是对App开发文档App Transport Security Technote和WORKING WITH APPLE’S APP TRANSPORT SECURITY两篇文章的整理。iOS程序犭袁在他的iOS9AdaptationTips一文中有更多有意思的内容,大家可以参考。

参考

  1. App Transport Security Technote
  2. WORKING WITH APPLE’S APP TRANSPORT SECURITY
  3. WWDC 2015视频:Networking with NSURLSession
  4. App Transport Security
  5. iOS9AdaptationTips

翻译篇:10 Things You Need to Know About Cocoa Auto Layout

AUG 31ST, 2015 | COMMENTS

原文由Ole Begemann在2013.3.31发表于其个人博客,地址是10 Things You Need to Know About Cocoa Auto Layout

译注:原文发表的时间有点早,主要是针对Xcode 4.x时代的Auto Layout,特别是第二部分”Interface Builder中的Auto Layout”,所以有些内容已经过时了。不过还是有很多可借鉴的地方。特别感谢@叶孤城 叶大在微博中的分享,以及对译文的校对。

第一次使用Cocoa Auto Layout时,感觉它与Cocoa开发者所熟知的springs-struts模式有很大的不同。尽管Auto Layout有点复杂,但我发现只需要了解一些基本规则就可以使用它。本文就来列出这些规则。

Auto Layout通用概念

1.经验法则:每个维度至少有两个约束

在每个维度(水平与竖直)上,一个视图的位置和大小由三个值来定义:头部空间(leading space),大小和尾部空间(trailing space)[注释1]。一个视图的leading和trailing空间可以相对于父视图来定义,也可以相对于视图层级架构中的兄弟视图来定义。一般来说,我们的布局约束必须满足这两个值,以便计算第三个值(size)。其结果是,一个标准的视图在每个维度上必须至少有两个约束,以明确视图的布局。

2.拥抱固有大小(Intrinsic Size)

一些控件,如标签和按钮,都有一个所谓的固有大小(Intrinsic Size)。视控件的不同,固有大小可以在水平或竖直或同时两个方向上有效。当一个控件没有明确的宽度和高度约束时,就会使用它的固有大小作为约束。这允许我们在每个方向上只使用一个显式约束就可以创建明确的布局(相对于上面第1条规则),并让控件可以根据自身的内容来自动调整大小。这是在本地化中创建一个最佳布局的关键。

Interface Builder中的Auto Layout

更新于2014.3.3:当我写这篇文章时,Xcode的版本是4.x。到了Xcode 5时,Interface Builder对Auto Layout的处理已以有了显著的改变,所以下面的一些内容已经不再有效(特别是第3、4条)。Xcode现在允许Interface Builder在创建模棱两可的布局,并在编译时添加missing constraints来明确一个布局。这使得在开发过程中,原型的设计和快速变更来得更加简单。第5、6条在Xcode 5中仍然是有效的。

Interface Builder中的Auto Layout编辑器似乎有自己的想法。理解Xcode的工程师为什么这样设计,可以让我们使用它是不至于太过沮丧。

image

图1:如果某个约束会导致模棱两可的布局,IB是不允许我们删除它的

3.IB总是不让你创建一个模棱两可的布局

IB的主要目标是保护我们自己。它决不会让我们创建一个模棱两可的布局。这意味着IB在我们将一个视图放到一个布局中时,会自动为我们创建约束。沿着IB的自动引导来放置我们的视图,以帮助IB正确的猜测我们想把视图放哪。

4.在我们删除一个已存在的约束之前,必须创建另外一个约束

使用Size Inspector来查看一个指定视图的所有约束。当一个约束的Delete菜单项是置灰时,就表示删除这个约束会导致混乱,因此这是不允许的。在删除它之前,我们必须创建至少一个自定义约束来取代它。

image

图2:创建新的布局约束的IB界面

为了创建一个新的约束,在布局中选择一个或多个视图,然后使用画布右下角的三个不显眼按钮来创建约束。这都是很容易被忽视的。

5.不要显式地调整控件的大小

尝试不要显式地设置一个控件的大小。只要我们不手动去改变它们的大小,大部分控件都会根据它们的内容来调整自己的大小,并使用固有大小(intrinsic size)来创建一个完美的、内容敏感的布局。这对于需要做本地化的UI尤其重要。一旦我们(无意或有意地)手动调整了控件的大小,IB将创建一个很难摆脱的显式大小约束。为了回归到固有大小,可以使用Editor > Size to Fit Content命令。

6.避免过早优化

不幸的是,使用Interface Builder来做自动布局将迫使我们更加小心。例如,如果我们发现需要使用一个控件来替换另一个,从布局中删除原始控件可能导致IB自动创建一组新的约束,当我们插入新的控件时,需要再次手动修改这些约束。因此,在我们的布局仍处于不稳定状态时去优化我们的约束,可能并不是一个好主意。更好的是在它更稳定时再去优化它。

代码中的Auto Layout

在Interface Builder中使用Auto Layout中可能很快就会有种挫折感,因此更多的开发者喜欢在代码中使用Auto Layout。

7.忘记Frame吧

忘记frame属性吧。不要直接设置它。一个视图的frame在自动布局过程中会被自动设置,而不是一个输入结果。我们可以通过改变约束来改变frame。这将强迫我们改变看待UI的方式。不用再去考虑位置和大小了,而是考虑每个视图相对于它的兄弟视图和父视图的位置。这与CSS没有什么不同。

8.别忘了禁用Autoresizing Masks

为了保证代码的向后兼容性,sprints-struts模式仍然是默认的。对于每一个代码创建的需要使用Auto Layout的视图,请调用setTranslatesAutoresizingMaskIntoConstraints:NO

9.多留意Debugger控制台

当我们写约束时,应该多留意Debugger控制台。我发现Apple关于模棱两可的约束或未满足的约束的错误日志总是可以帮助我们快速定位问题。这个可以参考Apple’s debugging tips in the Cocoa Auto Layout Guide。

10.让约束动起来,而不是frame

在Auto Layout中,我们需要重新考虑动画。我们不再可以简单的动画一个视图的frame了;如果我们这样做了,视图将在动画完成后自动恢复到Auto Layout计算出来的位置和大小上。相反,我们需要直接动画布局的约束。要做到这一点,或者修改已存在的约束(我们可以为IB中创建的约束声明IBOutlet变量),也可以添加一个新的约束,然后在一个动画block中给我们的视图发送layoutIfNeeded消息。

注释

  1. 在垂直维度,leading和trailing空间分别表示为top和bottom空间。在水平维度,我们可以选择两个方向:“Leading to Trailing” 或者是 “Left to Right”。这两者的不同之处在于,如果本地语言是从右到左的,则”Leading to Trailing”表示的就是”Right to Left”。在大多数时候,我们需要的是“Leading to Trailing”。

iOS知识小集 第四期(2015.08.15)

AUG 15TH, 2015 | COMMENTS

又欠了一屁股债了。积累了一大堆的问题放在那,就是没有整理。不能怪别人,也能怪自己了,犯起懒来,啥事也不想做,连喜爱的户外运动也给拉下了,掐指一算,居然大半年没出去了。然后经常看到老驴子们出去玩耍,回来就是一通的美图,心里那个痒痒啊。

回到正题吧,这次的知识小集知识点不多,还是三个:

  1. ARC与MRC的性能对比
  2. Bitcode
  3. 在Swift中实现NS_OPTIONS

篇幅超过了预期,大家慢慢看,如有问题还请指正。

ARC与MRC的性能对比

MRC似乎已经是一个上古时代的话题了,不过我还是绕有兴致的把它翻出来。因为,今天我被一个问题问住了:ARC与MRC的性能方面孰优劣。确实,之前没有对比过。

先来做个测试吧。首先我们需要一个计时辅助函数,我选择使用mach_absolute_time,计算时间差的函数如下:

12345678910111213141516
double subtractTimes(uint64_t endTime, uint64_t startTime) {    uint64_t difference = endTime - startTime;    static double conversion = 0.0;    if(conversion == 0.0) {        mach_timebase_info_data_t info;        kern_return_t err = mach_timebase_info(&info);                       //Convert the timebaseinto seconds        if(err == 0)            conversion = 1e-9 * (double) info.numer / (double) info.denom;    }    return conversion * (double)difference;}

然后定义两个测试类,一个是ARC环境下的,一个是MRC环境下的,分别如下:

12345678910111213141516171819202122232425262728293031323334353637
// Test1.m+ (void)test {    uint64_t start,stop;    start = mach_absolute_time();    for (int i = 0; i < 1000000; i++) {        NSArray *array = [[NSArray alloc] init];    }    stop = mach_absolute_time();    double diff = subtractTimes(stop, start);    NSLog(@"ARC total time in seconds = %f\n", diff);}// Test2.m// 在target->Build Phases->Compile Sources中,添加编译标识-fno-objc-arc+ (void)test {    uint64_t start,stop;    start = mach_absolute_time();    for (int i = 0; i < 1000000; i++) {        NSArray *array = [[NSArray alloc] init];        [array release];    }    stop = mach_absolute_time();    double diff = subtractTimes(stop, start);    NSLog(@"MRC total time in seconds = %f\n", diff);}

多运行几组测试,然后挑两组吧来看看,数据如下:

1234567
// A组ARC total time in seconds = 0.077761MRC total time in seconds = 0.072469// B组ARC total time in seconds = 0.075722MRC total time in seconds = 0.101671

从上面的数据可以看到,ARC与MRC各有快慢的情况。即使上升到统计学的角度,ARC也只是以轻微的优势胜出。看来我的测试姿势不对,并没有证明哪一方占绝对的优势。

嗯,那我们再来看看官方文档是怎么说的吧。在Transitioning to ARC Release Notes中有这么一段话:

Is ARC slow?

It depends on what you’re measuring, but generally “no.” The compiler efficiently eliminates many extraneousretain/release calls and much effort has been invested in speeding up the Objective-C runtime in general. In particular, the common “return a retain/autoreleased object” pattern is much faster and does not actually put the object into the autorelease pool, when the caller of the method is ARC code.

One issue to be aware of is that the optimizer is not run in common debug configurations, so expect to see a lot more retain/release traffic at -O0 than at -Os.

再来看看别人的数据吧。Steffen Itterheim在Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)一文中给出了大量的测试数据。这篇文章是2013.3.20号发表的。Steffen Itterheim通过他的测试得出一个结论

ARC is generally faster, and ARC can indeed be slower

嗯,有些矛盾。不过在文章中,Steffen Itterheim指出大部分情况下,ARC的性能是更好的,这主要得益于一些底层的优化以及autorelease pool的优化,这个从官方文档也能看到。但在一些情况下,ARC确实是更慢,ARC会发送一些额外的retain/release消息,如一些涉及到临时变量的地方,看下面这段代码:

123456789101112131415
// this is typical MRC code:{    id object = [array objectAtIndex:0];    [object doSomething];    [object doAnotherThing];}// this is what ARC does (and what is considered best practice under MRC):{    id object = [array objectAtIndex:0];    [object retain]; // inserted by ARC    [object doSomething];    [object doAnotherThing];    [object release]; // inserted by ARC}

另外,在带对象参数的方法中,也有类似的操作:

123456789101112131415
// this is typical MRC code:-(void) someMethod:(id)object{    [object doSomething];    [object doAnotherThing];}// this is what ARC does (and what is considered best practice under MRC):-(void) someMethod:(id)object{    [object retain]; // inserted by ARC    [object doSomething];    [object doAnotherThing];    [object release]; // inserted by ARC}

这些些额外的retain/release操作也成了降低ARC环境下程序性能的罪魁祸首。但实际上,之所以添加这些额外的retain/release操作,是为了保证代码运行的正确性。如果只是在单线程中执行这些操作,可能确实没必要添加这些额外的操作。但一旦涉及以多线程的操作,问题就来了。如上面的方法中,object完全有可能在doSoming和doAnotherThing方法调用之间被释放。为了避免这种情况的发生,便在方法开始处添加了[object retain],而在方法结束后,添加了[object release]操作。

如果想了解更多关于ARC与MRC性能的讨论,可以阅读一下Are there any concrete study of the performance impact of using ARC?与ARC vs. MRC Performance,在此就不过多的摘抄了。

实际上,即便是ARC的性能不如MRC,我们也应该去使用ARC,因此它给我们带来的好处是不言而喻的。我们不再需要像使用MRC那样,去过多的关注内存问题(虽然内存是必须关注的),而将更多的时间放在我们真正关心的事情上。如果真的对性能非常关切的话,可以考虑直接用C或C++。反正我是不会再回到MRC时代了。

参考

  1. Are there any concrete study of the performance impact of using ARC?
  2. ARC vs. MRC Performance
  3. Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)
  4. Transitioning to ARC Release Notes

Bitcode

今天试着用Xcode 7 beta 3在真机(iOS 8.3)上运行一下我们的工程,结果发现工程编译不过。看了下问题,报的是以下错误:

ld: ‘/Users/**/Framework/SDKs/PolymerPay/Library/mobStat/lib**SDK.a(**ForSDK.o)’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

得到的信息是我们引入的一个第三方库不包含bitcode。嗯,不知道bitcode是啥,所以就得先看看这货是啥了。

Bitcode是什么?

找东西嘛,最先想到的当然是先看官方文档了。在App Distribution Guide – App Thinning (iOS, watchOS)一节中,找到了下面这样一个定义:

Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the store.

说的是bitcode是被编译程序的一种中间形式的代码。包含bitcode配置的程序将会在App store上被编译和链接。bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到App store上。

嗯,看着挺高级的啊。

继续看,在What’s New in Xcode-New Features in Xcode 7中,还有一段如下的描述

Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.

当我们提交程序到App store上时,Xcode会将程序编译为一个中间表现形式(bitcode)。然后App store会再将这个botcode编译为可执行的64位或32位程序。

再看看这两段描述都是放在App Thinning(App瘦身)一节中,可以看出其与包的优化有关了。喵大(@onevcat)在其博客开发者所需要知道的 iOS 9 SDK 新特性中也描述了iOS 9中苹果在App瘦身中所做的一些改进,大家可以转场到那去研读一下。

Bitcode配置

在上面的错误提示中,提到了如何处理我们遇到的问题:

You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

要么让第三方库支持,要么关闭target的bitcode选项。

实际上在Xcode 7中,我们新建一个iOS程序时,bitcode选项默认是设置为YES的。我们可以在”Build Settings”->”Enable Bitcode”选项中看到这个设置。

不过,我们现在需要考虑的是三个平台:iOS,Mac OS,watchOS。

  • 对应iOS,bitcode是可选的。

  • 对于watchOS,bitcode是必须的。

  • Mac OS不支持bitcode。

如果我们开启了bitcode,在提交包时,下面这个界面也会有个bitcode选项:

image

盗图,我的应用没办法在这个界面显示bitcode,因为依赖于第三方的库,而这个库不支持bitcode,暂时只能设置ENABLE_BITCODE为NO。

所以,如果我们的工程需要支持bitcode,则必要要求所有的引入的第三方库都支持bitcode。我就只能等着公司那些大哥大姐们啥时候提供一个新包给我们了。

题外话

如上面所说,bitcode是一种中间代码。LLVM官方文档有介绍这种文件的格式,有兴趣的可以移步LLVM Bitcode File Format。

参考

  1. App Distribution Guide – App Thinning (iOS, watchOS)
  2. What’s New in Xcode-New Features in Xcode 7
  3. 开发者所需要知道的 iOS 9 SDK 新特性
  4. LLVM Bitcode File Format

在Swift中实现NS_OPTIONS

从Xcode 4.5以后,我们在Objective-C中使用NS_ENUM和NS_OPTIONS来定义一个枚举,以替代C语言枚举的定义方式。其中NS_ENUM用于定义普通的枚举,NS_OPTIONS用于定义选项类型的枚举。

而到了Swift中,枚举增加了更多的特性。它可以包含原始类型(不再局限于整型)以及相关值。正是由于这些原因,枚举在Swift中得到了更广泛的应用。在Foundation中,Objective-C中的NS_ENUM类型的枚举,都会自动转换成Swift中enum,并且更加精炼。以Collection View的滚动方向为例,在Objective-C中,其定义如下:

1234
typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {  UICollectionViewScrollDirectionVertical,  UICollectionViewScrollDirectionHorizontal};

而在Swift中,其定义如下:

1234
enum UICollectionViewScrollDirection : Int {  case Vertical  case Horizontal}

精练多了吧,看着舒服多了,还能少码两个字。我们自己定义枚举时,也应该采用这种方式。

不过对于Objective-C中NS_OPTIONS类型的枚举,Swift中的实现似乎就没有那么美好了。

我们再来对比一下UICollectionViewScrollPosition的定义吧,在Objective-C中,其定义如下:

1234567891011121314
typedef NS_OPTIONS(NSUInteger, UICollectionViewScrollPosition) {    UICollectionViewScrollPositionNone                 = 0,    // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions.    // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException.    UICollectionViewScrollPositionTop                  = 1 << 0,    UICollectionViewScrollPositionCenteredVertically   = 1 << 1,    UICollectionViewScrollPositionBottom               = 1 << 2,    // Likewise, the horizontal positions are mutually exclusive to each other.    UICollectionViewScrollPositionLeft                 = 1 << 3,    UICollectionViewScrollPositionCenteredHorizontally = 1 << 4,    UICollectionViewScrollPositionRight                = 1 << 5};

而在Swift 2.0中,其定义如下:

12345678910111213141516
struct UICollectionViewScrollPosition : OptionSetType {    init(rawValue: UInt)    static var None: UICollectionViewScrollPosition { get }    // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions.    // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException.    static var Top: UICollectionViewScrollPosition { get }    static var CenteredVertically: UICollectionViewScrollPosition { get }    static var Bottom: UICollectionViewScrollPosition { get }    // Likewise, the horizontal positions are mutually exclusive to each other.    static var Left: UICollectionViewScrollPosition { get }    static var CenteredHorizontally: UICollectionViewScrollPosition { get }    static var Right: UICollectionViewScrollPosition { get }}

额,光看代码,不看实现,这也是化简为繁的节奏啊。

为什么要这样做呢?Mattt给了我们如下解释:

Well, the same integer bitmasking tricks in C don’t work for enumerated types in Swift. An enum represents a type with a closed set of valid options, without a built-in mechanism for representing a conjunction of options for that type. An enum could, ostensibly, define a case for all possible combinations of values, but for n > 3, the combinatorics make this approach untenable.

意思是Swift不支持C语言中枚举值的整型掩码操作的技巧。在Swift中,一个枚举可以表示一组有效选项的集合,但却没有办法支持这些选项的组合操作(“&”、”|”等)。理论上,一个枚举可以定义选项值的任意组合值,但对于n > 3这种操作,却无法有效的支持。

为了支持类NS_OPTIONS的枚举,Swift 2.0中定义了OptionSetType协议【在Swift 1.2中是使用RawOptionSetType,相比较而言已经改进了不少】,它的声明如下:

1234567891011121314151617181920212223242526
/// Supplies convenient conformance to `SetAlgebraType` for any type/// whose `RawValue` is a `BitwiseOperationsType`.  For example://////     struct PackagingOptions : OptionSetType {///       let rawValue: Int///       init(rawValue: Int) { self.rawValue = rawValue }///     ///       static let Box = PackagingOptions(rawValue: 1)///       static let Carton = PackagingOptions(rawValue: 2)///       static let Bag = PackagingOptions(rawValue: 4)///       static let Satchel = PackagingOptions(rawValue: 8)///       static let BoxOrBag: PackagingOptions = [Box, Bag]///       static let BoxOrCartonOrBag: PackagingOptions = [Box, Carton, Bag]///     }////// In the example above, `PackagingOptions.Element` is the same type/// as `PackagingOptions`, and instance `a` subsumes instance `b` if/// and only if `a.rawValue & b.rawValue == b.rawValue`.protocol OptionSetType : SetAlgebraType, RawRepresentable {    /// An `OptionSet`'s `Element` type is normally `Self`.    typealias Element = Self    /// Convert from a value of `RawValue`, succeeding unconditionally.    init(rawValue: Self.RawValue)}

从字面上来理解,OptionSetType是选项集合类型,它定义了一些基本操作,包括集合操作(union, intersect, exclusiveOr)、成员管理(contains, insert, remove)、位操作(unionInPlace, intersectInPlace, exclusiveOrInPlace)以及其它的一些基本操作。

作为示例,我们来定义一个表示方向的选项集合,通常我们是定义一个实现OptionSetType协议的结构体,如下所示:

123456789101112
struct Directions: OptionSetType {    var rawValue:Int    init(rawValue: Int) {        self.rawValue = rawValue    }    static let Up: Directions = Directions(rawValue: 1 << 0)    static let Down: Directions = Directions(rawValue: 1 << 1)    static let Left: Directions = Directions(rawValue: 1 << 2)    static let Right: Directions = Directions(rawValue: 1 << 3)}

所需要做的基本上就是这些。然后我们就可以创建Directions的实例了,如下所示:

1234
let direction: Directions = Directions.Leftif direction == Directions.Left {    // ...}

如果想同时支持两个方向,则可以如上处理:

1234
let leftUp: Directions = [Directions.Left, Directions.Up]if leftUp.contains(Directions.Left) && leftUp.contains(Directions.Up) {    // ...}

如果leftUp同时包含Directions.Left和Directions.Up,则返回true。

这里还有另外一种方法来达到这个目的,就是我们在Directions结构体中直接声明声明Left和Up的静态常量,如下所示:

1234567
struct Directions: OptionSetType {    // ...    static let LeftUp: Directions = [Directions.Left, Directions.Up]    static let RightUp: Directions = [Directions.Right, Directions.Up]    // ...}

这样,我们就可以以如下方式来执行上面的操作:

123
if leftUp == Directions.LeftUp {    // ...}

当然,如果单一选项较多,而要去组合所有的情况,这种方法就显示笨拙了,这种情况下还是推荐使用contains方法。

总体来说,Swift中的对选项的支持没有Objective-C中的NS_OPTIONS来得简洁方便。而且在Swift 1.2的时候,我们还是可以使用”&“和”|”操作符的。下面这段代码在Swift 1.2上是OK的:

123
UIView.animateWithDuration(0.3, delay: 1.0, options: UIViewAnimationOptions.CurveEaseIn | UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in    // ...}, completion: nil)

但到了Swift 2.0时,OptionSetType已经不再支持”&“和”|”操作了,因此,上面这段代码需要修改成:

123
UIView.animateWithDuration(0.3, delay: 1.0, options: [UIViewAnimationOptions.CurveEaseIn, UIViewAnimationOptions.CurveEaseInOut], animations: { () -> Void in        // ...}, completion: nil)

不过,慢慢习惯就好。

参考

  1. RawOptionSetType
  2. Exploring Swift 2.0 OptionSetTypes
  3. Notes from WWDC 2015: The Enumerated Delights of Swift 2.0 Option Sets​
  4. 《100个Swift开发必备Tip》— Tip 66. Options

零碎

静态分析中”Potential null dereference”的处理

我们在写一个方法时,如果希望在方法执行出错时,获取一个NSError对象,我们通常会像下面这样来定义我们的方法

123456789
+ (NSString )checkStringLength:(NSString *)str error:(NSError **)error {  if (str.length <= 0) {          *error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil];      return nil;  }  return str;}

这段代码看着没啥问题,至少在语法上是OK的,所以在编译时,编译器并不会报任何警告。

如果我们用以下方式去调用的话,也是一切正常的:

12
NSError *error = nil;[Test checkStringLength:@"" error:&error];

不过我们如果就静态分析器来分析一下,发现会在”*error = …“这行代码处报如下的警告:

Potential null dereference. According to coding standards in ‘Creating and Returning NSError Objects’ the parameter may be null

这句话告诉我们的是这里可能存在空引用。实际上,如果我们像下面这样调用方法的话,程序是会崩溃的:

1
[Test checkStringLength:@"" error:NULL];

因为此时在方法中,error实际上是NULL,*error这货啥也不是,对它赋值肯定就出错了。

这里正确的姿式是在使用error之前,先判断它是否为NULL,完整的代码如下:

1234567891011
+ (NSString )checkStringLength:(NSString *)str error:(NSError **)error {    if (str.length <= 0) {        if (error != NULL) {            *error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil];        }        return nil;    }    return str;}

实际上,对此这种方式的传值,我们始终需要去做非空判断。

Charles支持iOS模拟器

咬咬牙花了50刀买了一个Charles的License。

今天临时需要在模拟器上跑工程,想抓一下数据包,看一下请求Header里面的信息。工程跑起来时,发现Charles没有抓取到数据。嗯,本着有问题先问stackoverflow的原则,跑到上面搜了一下。找到了这个贴子:How to use Charles Proxy on the Xcode 6 (iOS 8) Simulator?。不过我的处理没有他这么麻烦,基本上两步搞定了:

1.在Charles的菜单中选择Help > SSL Proxying > Install Charles Root Certificate in iOS Simulators,直接点击就行。这时候会弹出一个提示框,点击OK就行。

2.如果这时候还不能抓取数据,就重启模拟器。

这样就OK了。在Keychain里面,真机和模拟器的证书是同一个。

至于stackoverflow里面提到的在3.9.3版本上还需要覆盖一个脚本文件,这个没有尝试过,哈哈,我的是最新的3.10.2。

还有个需要注意的是,在抓取模拟器数据时,如果关闭Charles,那么模拟器将无法再请求到网络数据。这时需要重新开启Charles,或者是重启模拟器。另外如果重置了模拟器的设置(Reset Content and Settings…),Charles也抓取不到模拟器的数据,需要重新来过。

参考

  1. How to use Charles Proxy on the Xcode 6 (iOS 8) Simulator?

UIApearance

JUL 20TH, 2015 | COMMENTS

文章开头先援引一下Mattt Thompson大神在UIApearance里的一句话吧:

Users will pay a premium for good-looking software.

就如同大多数人喜欢看帅哥美女一样,一款App能不能被接受,长得怎样很重要。虽然大家都明白“人不可貌相”这个理,但大多数人其实还是视觉动物。用户体验用户体验,如果都让用户看得不爽了,又何谈用户体验呢?所以…所以…哎,我也只能在这默默地码字了。

在iOS 5以前,我们想去自定义系统控件的外观是一件麻烦的事。如果想统一地改变系统控件的外观,我们可能会想各种办法,如去继承现有的控件类,并在子类中修改,或者甚至于动用method swizzling这样高大上的方法。不过,苹果在iOS 5之后为我们提供了一种新的方法:UIAppearance,让这些事简单了不少。在这里,我们就来总结一下吧。

UIApearance是作用

UIApearance实际上是一个协议,我们可以用它来获取一个类的外观代理(appearance proxy)。为什么说是一个类,而不明确说是一个视图或控件呢?这是因为有些非视图对象(如UIBarButtonItem)也可以实现这个协议,来定义其所包含的视图对象的外观。我们可以给这个类的外观代理发送一个修改消息,来自定义一个类的实例的外观。

我们以系统定义的控件UIButton为例,根据我们的使用方式,可以通过UIAppearance修改整个应用程序中所有UIButton的外观,也可以修改某一特定容器类中所有UIButton的外观(如UIBarButtonItem)。不过需要注意的是,这种修改只会影响到那些执行UIAppearance操作之后添加到我们的视图层级架构中的视图或控件,而不会影响到修改之前就已经添加的对象。因此,如果要修改特定的视图,先确保该视图在使用UIAppearance后才通过addSubview添加到视图层级架构中。

UIAppearance的使用

如上面所说,有两种方式来自定义对象的外观:针对某一类型的所有实例;针对包含在某一容器类的实例中的某一类型的实例。讲得有点绕,我把文档的原文贴出来吧。

for all instances, and for instances contained within an instance of a container class.

为此,UIAppearance声明了两个方法。如果我们想自定义一个类所有实例的外观,则可以使用下面这个方法:

// swiftstatic func appearance() -> Self// Objective-C+ (instancetype)appearance

例如,如果我们想修改UINavigationBar的所有实例的背影颜色和标题外观,则可以如下实现:

UINavigationBar.appearance().barTintColor = UIColor(red: 104.0/255.0, green: 224.0/255.0, blue: 231.0/255.0, alpha: 1.0)UINavigationBar.appearance().titleTextAttributes = [    NSFontAttributeName: UIFont.systemFontOfSize(15.0),    NSForegroundColorAttributeName: UIColor.whiteColor()]

我们也可以指定一类容器,在这个容器中,我们可以自定义一个类的所有实例的外观。我们可以使用下面这个方法:

+ (instancetype)appearanceWhenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ...

如,我们想修改导航栏中所有的按钮的外面,则可以如下处理:

[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil]   setBackgroundImage:myNavBarButtonBackgroundImage forState:state barMetrics:metrics];[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], [UIPopoverController class], nil]    setBackgroundImage:myPopoverNavBarButtonBackgroundImage forState:state barMetrics:metrics];[[UIBarButtonItem appearanceWhenContainedIn:[UIToolbar class], nil]    setBackgroundImage:myToolbarButtonBackgroundImage forState:state barMetrics:metrics];[[UIBarButtonItem appearanceWhenContainedIn:[UIToolbar class], [UIPopoverController class], nil]    setBackgroundImage:myPopoverToolbarButtonBackgroundImage forState:state barMetrics:metrics];

注意这个方法的参数是一个可变参数,因此,它可以同时设置多个容器。

我们仔细看文档,发现这个方法没有swift版本,至少我在iOS 8.x的SDK中没有找到对应的方法。呵呵,如果想在iOS 8.x以下的系统用swift来调用appearanceWhenContainedIn,那就乖乖地用混编吧。

不过在iOS 9的SDK中(记录一下,今天是2015.07.18),又把这个方法给加上了,不过这回参数换成了数组,如下所示:

@available(iOS 9.0, *)static func appearanceWhenContainedInInstancesOfClasses(containerTypes: [AnyObject.Type]) -> Self

嗯,这里有个问题,我在Xcode 7.0 beta 3版本上测试swift版本的这个方法时,把将其放在启动方法里面,如下所示:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {    // 此处会崩溃,提示EXC_BAD_ACCESS    let barButtonItemAppearance = UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses([UINavigationBar.self])    let attributes = [        NSFontAttributeName: UIFont.systemFontOfSize(13.0),        NSForegroundColorAttributeName: UIColor.whiteColor()    ]    barButtonItemAppearance.setTitleTextAttributes(attributes, forState: .Normal)    return true}

程序崩溃了,在appearanceWhenContainedInInstancesOfClasses这行提示EXC_BAD_ACCESS。既然是内存问题,那就找找吧。我做了如下几个测试:

1.拆分UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses,在其前面加了如下几行代码:

let appearance = UIBarButtonItem.appearance()let arr: [AnyObject.Type] = [UINavigationBar.self, UIToolbar.self]print(arr)

可以看到除了appearanceWhenContainedInInstancesOfClasses自身外,其它几个元素都是没问题的。

2.将这段拷贝到默认的ViewController中,运行。同样崩溃了。

3.在相同环境下(Xcode 7.0 beta 3 + iOS 9.0),用Objective-C对应的方法试了一下,如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]];    return YES;}

程序很愉快地跑起来了。

额,我能把这个归结为版本不稳定的缘故么?等到稳定版出来后再研究一下吧。

支持UIAppearance的组件

从iOS 5.0后,有很多iOS的API都已经支持UIAppearance的代理方法了,Mattt Thompson在UIApearance中,给我们提供了以下两行脚本代码,可以获取所有支持UI_APPEARANCE_SELECTOR的方法(我们将在下面介绍UI_APPEARANCE_SELECTOR):

$ cd /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks/UIKit.framework/Headers$ grep -H UI_APPEARANCE_SELECTOR ./* | sed 's/ __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0) UI_APPEARANCE_SELECTOR;//'

大家可以试一下,我这里列出部分输出:

./UIActivityIndicatorView.h:@property (readwrite, nonatomic, retain) UIColor *color NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;./UIAppearance.h:/* To participate in the appearance proxy API, tag your appearance property selectors in your header with UI_APPEARANCE_SELECTOR../UIAppearance.h:#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))./UIBarButtonItem.h:- (void)setBackgroundImage:(UIImage *)backgroundImage forState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;./UIBarButtonItem.h:- (UIImage *)backgroundImageForState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;./UIBarButtonItem.h:- (void)setBackgroundImage:(UIImage *)backgroundImage forState:(UIControlState)state style:(UIBarButtonItemStyle)style barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;./UIBarButtonItem.h:- (UIImage *)backgroundImageForState:(UIControlState)state style:(UIBarButtonItemStyle)style barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;./UIBarButtonItem.h:- (void)setBackgroundVerticalPositionAdjustment:(CGFloat)adjustment forBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; ......

大家还可以在这里查看iOS 7.0下的清单。

自定义类实现UIAppearance

我们可以自定义一个类,并让这个类支持UIAppearance。为此,我们需要做两件事:

  1. 让我们的类实现UIAppearanceContainer协议
  2. 如果是在Objective-C中,则将相关的方法用UI_APPEARANCE_SELECTOR来标记。而在Swift中,需要在对应的属性或方法前面加上dynamic。

当然,要让我们的类可以使用appearance(或appearanceWhenContainedInInstancesOfClasses)来获取自己的类,则还需要实现UIAppearance协议。

在这里,我们来定义一个带边框的Label,通过UIAppearance来设置它的默认边框。实际上,UIView已经实现了UIAppearance和UIAppearanceContainer协议。因此,我们在其子类中不再需要显式地去声明实现这两个接口。

我们的Label的声明如下:

// RoundLabel.h@interface RoundLabel : UILabel@property (nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;@property (nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;@property (nonatomic, assign) UIColor *borderColor UI_APPEARANCE_SELECTOR;@end

具体的实现如下:

@implementation RoundLabel- (void)drawRect:(CGRect)rect {    [super drawRect:rect];    self.layer.borderColor = _borderColor.CGColor;    self.layer.cornerRadius = _cornerRadius;    self.layer.borderWidth = _borderWidth;}- (void)setBorderWidth:(CGFloat)borderWidth {    _borderWidth = borderWidth;}- (void)setCornerRadius:(CGFloat)cornerRadius {    _cornerRadius = cornerRadius;}- (void)setRectColor:(UIColor *)rectColor {    _borderColor = rectColor;}@end

我们在drawRect:设置Label的边框,这样RoundLabel的所有实例就可以使用默认的边框配置属性了。

然后,我们可以在AppDelegate或者其它某个位置来设置RoundLabel的默认配置,如下所示:

UIColor *color = [UIColor colorWithRed:104.0/255.0 green:224.0/255.0 blue:231.0/255.0 alpha:1.0f];[RoundLabel appearance].cornerRadius = 5.0f;[RoundLabel appearance].borderColor = color;[RoundLabel appearance].borderWidth = 1.0f;

当然,我们在使用RoundLabel时,可以根据实际需要再修改这几个属性的值。

Swift的实现就简单多了,我们只需要如下处理:

class RoundLabel: UILabel {    dynamic func setBorderColor(color: UIColor) {        layer.borderColor = color.CGColor    }    dynamic func setBorderWidth(width: CGFloat) {        layer.borderWidth = width    }    dynamic func setCornerRadius(radius: CGFloat) {        layer.cornerRadius = radius    }}

在UIAppearanceContainer的官方文档中,有对支持UIAppearance的方法作格式限制,具体要求如下:

// Swiftfunc propertyForAxis1(axis1: IntegerType, axis2: IntegerType, axisN: IntegerType) -> PropertyTypefunc setProperty(property: PropertyType, forAxis1 axis1: IntegerType, axis2: IntegerType)// OBJECTIVE-C- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 … axisN:(IntegerType)axisN;- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 … axisN:(IntegerType)axisN;

其中的属性类型可以是iOS的任意类型,包括id, NSInteger, NSUInteger, CGFloat, CGPoint, CGSize, CGRect, UIEdgeInsets或UIOffset。而IntegerType必须是NSInteger或者NSUInteger。如果类型不对,则会抛出异常。

我们可以以UIBarButtonItem为例,它定义了以下方法:

setTitlePositionAdjustment:forBarMetrics:backButtonBackgroundImageForState:barMetrics:setBackButtonBackgroundImage:forState:barMetrics:

这些方法就是满足上面所提到的格式。

Trait Collection

我们查看UIAppearance的官方文档,可以看到在iOS 8后,这个协议又新增了两个方法:

// Swiftstatic func appearanceForTraitCollection(_ trait: UITraitCollection) -> Self// Objective-C+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait                         whenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ...

这两个方法涉及到Trait Collection,具体的内容我们在此不过多的分析。

一些深入的东西

了解了怎么去使用UIApearance,现在我们再来了解一下它是怎么运作的。我们跟着UIAppearance for Custom Views一文的思路来走。

我们在以下实现中打一个断点:

- (void)setBorderWidth:(CGFloat)borderWidth {    _borderWidth = borderWidth;}

然后运行程序。程序启动时,我们发现虽然在AppDelegate中调用了

[RoundLabel appearance].borderWidth = 1.0f;

但实际上,此时程序没有到在此断住。我们再进到Label所在的视图控制器,这时程序在断点处停住了。在这里,我们可以看看方法的调用栈。

image

在调用栈里面,我们可以看到_UIAppearance这个东东,我们从iOS-Runtime-Headers可以找到这个类的定义:

@interface _UIAppearance : NSObject {    NSMutableArray *_appearanceInvocations;    NSArray *_containerList;    _UIAppearanceCustomizableClassInfo *_customizableClassInfo;    NSMapTable *_invocationSources;    NSMutableDictionary *_resettableInvocations;}

其中_UIAppearanceCustomizableClassInfo存储的是外观对应的类的信息。我们可以看看这个类的声明:

@interface _UIAppearanceCustomizableClassInfo : NSObject {    NSString *_appearanceNodeKey;    Class _customizableViewClass;    Class _guideClass;    unsigned int _hash;    BOOL _isCustomizableViewClassRoot;    BOOL _isGuideClassRoot;}@property (nonatomic, readonly) NSString *_appearanceNodeKey;@property (nonatomic, readonly) Class _customizableViewClass;@property (nonatomic, readonly) Class _guideClass;@property (nonatomic, readonly) unsigned int _hash;+ (id)_customizableClassInfoForViewClass:(Class)arg1 withGuideClass:(Class)arg2;- (id)_appearanceNodeKey;- (Class)_customizableViewClass;- (Class)_guideClass;- (unsigned int)_hash;- (id)_superClassInfo;- (void)dealloc;- (id)description;- (unsigned int)hash;- (BOOL)isEqual:(id)arg1;@end

在_UIAppearance中,还有一个_appearanceInvocations变量,我们可以在Debug中尝试用以下命令来打印出它的信息:

po [[NSClassFromString(@"_UIAppearance") _appearanceForClass:[RoundLabel class] withContainerList:nil] valueForKey:@"_appearanceInvocations"]

我们可以得到以下的信息:

<__NSArrayM 0x7fd44a5c1f80>(<NSInvocation: 0x7fd44a5c1d20>return value: {v} voidtarget: {@} 0x10b545ae0selector: {:} setCornerRadius:argument 2: {d} 0.000000,<NSInvocation: 0x7fd44a5bf300>return value: {v} voidtarget: {@} 0x10b545ae0selector: {:} setBorderColor:argument 2: {@} 0x7fd44a5bbb80,<NSInvocation: 0x7fd44a50b8c0>return value: {v} voidtarget: {@} 0x10b545ae0selector: {:} setBorderWidth:argument 2: {d} 0.000000)

可以看到这个数组中存储的实际上是NSInvocation对象,每个对象就是我们在程序中设置的RoundLabel外观的方法信息。

在Peter Steinberger的文章中,有提到当我们设置了一个自定义的外观时,_UIAppearanceRecorder会去保存并跟踪这个设置。我们可以看看_UIAppearanceRecorder的声明:

@interface _UIAppearanceRecorder : NSObject {    NSString *_classNameToRecord;    NSArray *_containerClassNames;    NSMutableArray *_customizations;    Class _superclassToRecord;    NSArray *_unarchivedCustomizations;}

不过有点可惜的是,我没有从这里找到太多的信息。我用runtime检查了一下这个类中的数据,貌似没有太多东西。可能是姿势不对,我把代码和结果贴出来,大家帮我看看。

unsigned int outCount = 0;Class recorderClass = NSClassFromString(@"_UIAppearanceRecorder");id recorder = [recorderClass performSelector:NSSelectorFromString(@"_sharedAppearanceRecorderForClass::whenContainedIn:") withObject:[RoundLabel class] withObject:nil];NSLog(@"_UIAppearanceRecorder instance : %@", recorder);Ivar *variables = class_copyIvarList(recorderClass, &outCount);for (int i = 0; i < outCount; i++) {    Ivar variable = variables[i];    id value = object_getIvar(recorder, variable);    NSLog(@"variable's name: %s, value: %@", ivar_getName(variable), value);}free(variables);

打印结果:

UIAppearanceExample2[7600:381708] _UIAppearanceRecorder instance : <_UIAppearanceRecorder: 0x7fa29a718960>UIAppearanceExample2[7600:381708] variable's name: _classNameToRecord, value: RoundLabelUIAppearanceExample2[7600:381708] variable's name: _superclassToRecord, value: (null)UIAppearanceExample2[7600:381708] variable's name: _containerClassNames, value: (null)UIAppearanceExample2[7600:381708] variable's name: _customizations, value: ()UIAppearanceExample2[7600:381708] variable's name: _unarchivedCustomizations, value: (null)

我们回过头再来看看_UIAppearance的_appearanceInvocations,我们是否可以这样猜测:UIAppearance是否是通过类似于Swizzling Method这种方式,在运行时去更新视图的默认显示呢?求解。

遗留问题

这一小篇遗留下了两个问题:

  1. 在swift中如何正确地使用appearanceWhenContainedInInstancesOfClasses方法?我在stackoverflow中没有找到答案。
  2. iOS内部是如何用UIAppearance设置的信息来在运行时替换默认的设置的?

如果有答案,还请告知。

小结

使用UIAppearance,可以让我们方便地去修改一些视图或控件的默认显示。同样,如果我们打算开发一个视图库,也可能会用到相关的内容。我们可以在库的内部自定义一些UIAppearance的规则来代替手动去修改视图外观。这样,库外部就可以方便的通过UIAppearance来整体修改一个类中视图的外观了。

我在github中搜索UIAppearance相关的实例时,找到了UISS这个开源库,它提供了一种便捷的方式来定义程序的样式。这个库也是基于UIAppearance的。看其介绍,如果我们想自定义一个UIButton的外观,可以使用以下方式:

{    "UIButton":{        "titleColor:normal":["white", 0.8],        "titleColor:highlighted":"white",        "backgroundImage:normal": ["button-background-normal", [0,10,0,10]],        "backgroundImage:highlighted": ["button-background-highlighted", [0,10,0,10]],        "titleEdgeInsets": [1,0,0,0],        "UILabel":{            "font":["Copperplate-Bold", 18]        }    }}

看着像JSON吧?

具体的我也还没有看,回头抽空再研究研究这个库。

补充:文章中的示例代码已放到github中,可以在这里查看(不保证在iOS 9.0以下能正常进行,嘿嘿)

参考

  1. UIApearance
  2. UIAppearance Protocol Reference
  3. UIAppearanceContainer Protocol Reference
  4. UIAppearance for Custom Views

iOS知识小集 第三期 (2015.06.30)

JUN 30TH, 2015 | COMMENTS

Swift2出来了,还是得与时俱进啊,不然就成老古董了。再者它开源了,又有事情要做了。当个程序猿真是累啊,一直在追,可从来没追上,刚有那么点念想了,人家又踩了脚油门。

一个月又要过去了,说好的一月两到三篇的,看来希望也是有点渺茫了。本来想好好整理下僵尸对象的内容,看看时间也不多了,也只好放到后面了。这一期没啥好内容,质量也不高,大家凑合着看吧,有疏漏还请大家指出,我一定好好改正。

这一期主要有三个内容:

  1. Tint Color
  2. Build Configurations in Swift
  3. 键盘事件

Tint Color

在iOS 7后,UIView新增加了一个tintColor属性,这个属性定义了一个非默认的着色颜色值,其值的设置会影响到以视图为根视图的整个视图层次结构。它主要是应用到诸如app图标、导航栏、按钮等一些控件上,以获取一些有意思的视觉效果。

tintColor属性的声明如下:

var tintColor: UIColor!

默认情况下,一个视图的tintColor是为nil的,这意味着视图将使用父视图的tint color值。当我们指定了一个视图的tintColor后,这个色值会自动传播到视图层次结构(以当前视图为根视图)中所有的子视图上。如果系统在视图层次结构中没有找到一个非默认的tintColor值,则会使用系统定义的颜色值(蓝色,RGB值为[0,0.478431,1],我们可以在IB中看到这个颜色)。因此,这个值总是会返回一个颜色值,即我们没有指定它。

与tintColor属性相关的还有个tintAdjustmentMode属性,它是一个枚举值,定义了tint color的调整模式。其声明如下:

var tintAdjustmentMode: UIViewTintAdjustmentMode

枚举UIViewTintAdjustmentMode的定义如下:

enum UIViewTintAdjustmentMode : Int {    case Automatic          // 视图的着色调整模式与父视图一致    case Normal             // 视图的tintColor属性返回完全未修改的视图着色颜色    case Dimmed             // 视图的tintColor属性返回一个去饱和度的、变暗的视图着色颜色}

因此,当tintAdjustmentMode属性设置为Dimmed时,tintColor的颜色值会自动变暗。而如果我们在视图层次结构中没有找到默认值,则该值默认是Normal。

与tintColor相关的还有一个tintColorDidChange方法,其声明如下:

func tintColorDidChange()

这个方法会在视图的tintColor或tintAdjustmentMode属性改变时自动调用。另外,如果当前视图的父视图的tintColor或tintAdjustmentMode属性改变时,也会调用这个方法。我们可以在这个方法中根据需要去刷新我们的视图。

示例

接下来我们通过示例来看看tintColor的强大功能(示例盗用了Sam Davies写的一个例子,具体可以查看iOS7 Day-by-Day :: Day 6 :: Tint Color,我就负责搬砖,用swift实现了一下,代码可以在这里下载)。

先来看看最终效果吧(以下都是盗图,请见谅,太懒了):

image

这个界面包含的元素主要有UIButton, UISlider, UIProgressView, UIStepper, UIImageView, ToolBar和一个自定义的子视图CustomView。接下来我们便来看看修改视图的tintColor会对这些控件产生什么样的影响。

在ViewController的viewDidLoad方法中,我们做了如下设置:

override func viewDidLoad() {    super.viewDidLoad()    println("\(self.view.tintAdjustmentMode.rawValue)")         // 输出:1    println("\(self.view.tintColor)")                           // 输出:UIDeviceRGBColorSpace 0 0.478431 1 1    self.view.tintAdjustmentMode = .Normal    self.dimTintSwitch?.on = false    // 加载图片    var shinobiHead = UIImage(named: "shinobihead")    // 设置渲染模式    shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate)    self.tintedImageView?.image = shinobiHead    self.tintedImageView?.contentMode = .ScaleAspectFit}

首先,我们尝试打印默认的tintColor和tintAdjustmentMode,分别输出了[UIDeviceRGBColorSpace 0 0.478431 1 1]和1,这是在我们没有对整个视图层次结构设置任何tint color相关的值的情况下的输出。可以看到,虽然我们没有设置tintColor,但它仍然返回了系统的默认值;而tintAdjustmentMode则默认返回Normal的原始值。

接下来,我们显式设置tintAdjustmentMode的值为Normal,同时设置UIImageView的图片及渲染模式。

当我们点击”Change Color”按钮时,会执行以下的事件处理方法:

@IBAction func changeColorHandler(sender: AnyObject) {    let hue = CGFloat(arc4random() % 256) / 256.0    let saturation = CGFloat(arc4random() % 128) / 256.0 + 0.5    let brightness = CGFloat(arc4random() % 128) / 256.0 + 0.5    let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)    self.view.tintColor = color    updateViewConstraints()}private func updateProgressViewTint() {    self.progressView?.progressTintColor = self.view.tintColor}

这段代码主要是随机生成一个颜色值,并赋值给self.view的tintColor属性,同时去更新进度条的tintColor值。

注:有些控件的特定组成部件的tint color由特定的属性控制,例如进度就有2个tint color:一个用于进度条本身,另一个用于背景。

点击”Change Color”按钮,可得到以下效果:

image

可以看到,我们在示例中并有没手动去设置UIButton, UISlider, UIStepper, UIImageView, ToolBar等子视图的颜色值,但随着self.view的tintColor属性颜色值的变化,这些控件的外观也同时跟着改变。也就是说self.view的tintColor属性颜色值的变化,影响到了以self.view为根视图的整个视图层次结果中所有子视图的外观。

看来tintColor还是很强大的嘛。

在界面中还有个UISwitch,这个是用来开启关闭dim tint的功能,其对应处理方法如下:

@IBAction func dimTimtHandler(sender: AnyObject) {    if let isOn = self.dimTintSwitch?.on {        self.view.tintAdjustmentMode = isOn ? .Dimmed : .Normal    }    updateViewConstraints()}

当tintAdjustmentMode设置Dimmed时,其实际的效果是整个色值都变暗(此处无图可盗)。

另外,我们在子视图CustomView中重写了tintColorDidChange方法,以监听tintColor的变化,以更新我们的自定义视图,其实现如下:

override func tintColorDidChange() {    tintColorLabel.textColor = self.tintColor    tintColorBlock.backgroundColor = self.tintColor}

所以方框和”Tint color label”颜色是跟着子视图的tintColor来变化的,而子视图的tintColor又是继承自父视图的。

在这个示例中,比较有意思的是还是对图片的处理。对图像的处理比较简单粗暴,对一个像素而言,如果它的alpha值为1的话,就将它的颜色设置为tint color;如果不为1的话,则设置为透明的。示例中的忍者头像就是这么处理的。不过我们需要设置图片的imageWithRenderingMode属性为AlwaysTemplate,这样渲染图片时会将其渲染为一个模板而忽略它的颜色信息,如代码所示:

var shinobiHead = UIImage(named: "shinobihead")// 设置渲染模式shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate)

题外话

插个题外话,跟主题关系不大。

在色彩理论(color theory)中,一个tint color是一种颜色与白色的混合。与之类似的是shade color和tone color。shade color是将颜色与黑色混合,tone color是将颜色与灰色混合。它们都是基于Hues色调的。这几个色值的效果如下图所示:

image

一些基础的理论知识可以参考Hues, Tints, Tones and Shades: What’s the Difference?或更专业的一些文章。

小结

如果我们想指定整个App的tint color,则可以通过设置window的tint color。这样同一个window下的所有子视图都会继承此tint color。

当弹出一个alert或者action sheet时,iOS7会自动将后面视图的tint color变暗。此时,我们可以在自定义视图中重写tintColorDidChange方法来执行我们想要的操作。

有些复杂控件,可以有多个tint color,不同的tint color控件不同的部分。如上面提到的UIProgressView,又如navigation bars, tab bars, toolbars, search bars, scope bars等,这些控件的背景着色颜色可以使用barTintColor属性来处理。

参考

  1. UIView Class Reference
  2. iOS7 Day-by-Day :: Day 6 :: Tint Color
  3. Appearance and Behavior
  4. Tints and shades
  5. Hues, Tints, Tones and Shades: What’s the Difference?

Build Configurations in Swift

在Objective-C中,我们经常使用预处理指令来帮助我们根据不同的平台执行不同的代码,以让我们的代码支持不同的平台,如:

#if TARGET_OS_IPHONE    #define MAS_VIEW UIView             #elif TARGET_OS_MAC    #define MAS_VIEW NSView#endif

在swift中,由于对C语言支持没有Objective-C来得那么友好(暂时不知swift 2到C的支持如何),所以我们无法像在Objective-C中那样自如而舒坦地使用预处理指令。

不过,swift也提供了自己的方式来支持条件编译,即使用build configurations(构建配置)。Build configurations已经包含了字面量true和false,以及两个平台测试函数os()和arch()。

其中os()用于测试系统类型,可传入的参数包含OSX, iOS, watchOS,所以上面的代码在swift可改成:

#if os(iOS)    typealias MAS_VIEW = UIView#elseif os(OSX)    typealias MAS_VIEW = NSView#endif

注:在WWDC 2014的“Sharing code between iOS and OS X”一节(session 233)中,Elizabeth Reid将这种方式称为Shimming

遗憾的是,os()只能检测系统类型,而无法检测系统的版本,所以这些工作只能放在运行时去处理。关于如何检测系统的版本,Mattt Thompson老大在它的Swift System Version Checking一文中给了我们答案。

我们再来看看arch()。arch()用于测试CPU的架构,可传入的值包括x86_64, arm, arm64, i386。需要注意的是arch(arm)对于ARM 64的设备来说,不会返回true。而arch(i386)在32位的iOS模拟器上编译时会返回true。

如果我们想自定义一些在调试期间使用的编译配置选项,则可以使用-D标识来告诉编译器,具体操作是在”Build Setting”–>“Swift Compiler-Custom Flags”–>“Other Swift Flags”–>“Debug”中添加所需要的配置选项。如我们想添加常用的DEGUB选项,则可以在此加上”-D DEBUG”。这样我们就可以在代码中来执行一些debug与release时不同的操作,如

#if DEBUG    let totalSeconds = totalMinutes#else    let totalSeconds = totalMinutes * 60#endif

一个简单的条件编译声明如下所示:

#if build configuration    statements#else    statements#endif

当然,statements中可以包含0个或多个有效的swift的statements,其中可以包括表达式、语句、和控制流语句。另外,我们也可以使用&&和||操作符来组合多个build configuration,同时,可以使用!操作符来对build configuration取反,如下所示:

#if build configuration && !build configuration    statements#elseif build configuration    statements#else    statements#endif

需要注意的是,在swift中,条件编译语句必须在语法上是有效的,因为即使这些代码不会被编译,swift也会对其进行语法检查。

参考

  1. Cross-platform Swift
  2. Shimming in Swift
  3. Swift System Version Checking
  4. Interacting with C APIs

键盘事件

在涉及到表单输入的界面中,我们通常需要监听一些键盘事件,并根据实际需要来执行相应的操作。如,键盘弹起时,要让我们的UIScrollView自动收缩,以能看到整个UIScrollView的内容。为此,在UIWindow.h中定义了如下6个通知常量,来配合键盘在不同时间点的事件处理:

UIKeyboardWillShowNotification          // 键盘显示之前UIKeyboardDidShowNotification           // 键盘显示完成后UIKeyboardWillHideNotification          // 键盘隐藏之前UIKeyboardDidHideNotification           // 键盘消息之后UIKeyboardWillChangeFrameNotification   // 键盘大小改变之前UIKeyboardDidChangeFrameNotification    // 键盘大小改变之后

这几个通知的object对象都是nil。而userInfo字典都包含了一些键盘的信息,主要是键盘的位置大小信息,我们可以通过使用以下的key来获取字典中对应的值:

// 键盘在动画开始前的framelet UIKeyboardFrameBeginUserInfoKey: String// 键盘在动画线束后的framelet UIKeyboardFrameEndUserInfoKey: String// 键盘的动画曲线let UIKeyboardAnimationCurveUserInfoKey: String// 键盘的动画时间let UIKeyboardAnimationDurationUserInfoKey: String

在此,我感兴趣的是键盘事件的调用顺序和如何获取键盘的大小,以适当的调整视图的大小。

从定义的键盘通知的类型可以看到,实际上我们关注的是三个阶段的键盘的事件:显示、隐藏、大小改变。在此我们设定两个UITextField,它们的键盘类型不同:一个是普通键盘,一个是数字键盘。我们监听所有的键盘事件,并打印相关日志(在此就不贴代码了),直接看结果。

1) 当我们让textField1获取输入焦点时,打印的日志如下:

keyboard will changekeyboard will showkeyboard did changekeyboard did show

2) 在不隐藏键盘的情况下,让textField2获取焦点,打印的日志如下:

keyboard will changekeyboard will showkeyboard did changekeyboard did show

3) 再收起键盘,打印的日志如下:

keyboard will changekeyboard will hidekeyboard did changekeyboard did hide

从上面的日志可以看出,不管是键盘的显示还是隐藏,都会发送大小改变的通知,而且是在show和hide的对应事件之前。而在大小不同的键盘之间切换时,除了发送change事件外,还会发送show事件(不发送hide事件)。

另外还有两点需要注意的是:

  1. 如果是在两个大小相同的键盘之间切换,则不会发送任何消息
  2. 如果是普通键盘中类似于中英文键盘的切换,只要大小改变了,都会发送一组或多组与上面2)相同流程的消息

了解了事件的调用顺序,我们就可以根据自己的需要来决定在哪个消息处理方法中来执行操作。为此,我们需要获取一些有用的信息。这些信息是封装在通知的userInfo中,通过上面常量key来获取相关的值。通常我们关心的是UIKeyboardFrameEndUserInfoKey,来获取动画完成后,键盘的frame,以此来计算我们的scroll view的高度。另外,我们可能希望scroll view高度的变化也是通过动画来过渡的,此时UIKeyboardAnimationCurveUserInfoKey和UIKeyboardAnimationDurationUserInfoKey就有用了。

我们可以通过以下方式来获取这些值:

if let dict = notification.userInfo {    var animationDuration: NSTimeInterval = 0    var animationCurve: UIViewAnimationCurve = .EaseInOut    var keyboardEndFrame: CGRect = CGRectZero    dict[UIKeyboardAnimationCurveUserInfoKey]?.getValue(&animationCurve)    dict[UIKeyboardAnimationDurationUserInfoKey]?.getValue(&animationDuration)    dict[UIKeyboardFrameEndUserInfoKey]?.getValue(&keyboardEndFrame)    ......}

实际上,userInfo中还有另外三个值,只不过这几个值从iOS 3.2开始就已经废弃不用了。所以我们不用太关注。

最后说下表单。一个表单界面看着比较简单,但交互和UI总是能想出各种方法来让它变得复杂,而且其实里面设计到的细节还是很多的。像我们金融类的App,通常都会涉及到大量的表单输入,所以如何做好,还是需要花一番心思的。空闲时,打算总结一下,写一篇文章。

参考

  1. UIWindow Class Reference

零碎

自定义UIPickerView的行

UIPickerView的主要内容实际上并不多,主要是一个UIPickerView类和对应的UIPickerViewDelegate,UIPickerViewDataSource协议,分别表示代理和数据源。在此不细说这些,只是解答我们遇到的一个小需求。

通常,UIPickerView是可以定义多列内容的,比如年、月、日三列,这些列之间相互不干扰,可以自已滚自己的,不碍别人的事。不过,我们有这么一个需求,也是有三列,但这三列需要一起滚。嗯,这个就需要另行处理了。

在UIPickerViewDelegate中,声明了下面这样一个代理方法:

- (UIView *)pickerView:(UIPickerView *)pickerView        viewForRow:(NSInteger)row      forComponent:(NSInteger)component       reusingView:(UIView *)view

我们通过这个方法就可以来自定义行的视图。时间不早,废话就不多说了,直接上代码吧:

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {    PickerViewCell *pickerCell = (PickerViewCell *)view;    if (!pickerCell) {        NSInteger column = 3;        pickerCell = [[PickerViewCell alloc] initWithFrame:(CGRect){CGPointZero, [UIScreen mainScreen].bounds.size.width, 45.0f} column:column];    }    [pickerCell setLabelTexts:@[...]];    return pickerCell;}

我们定义了一个PickerViewCell视图,里面根据我们的传入的column参数来等分放置column个UILabel,并通过setLabelTexts来设置每个UILabel的文本。当然,我们也可以在PickerViewCell去定义UILabel的外观显示。就是这么简单。

不过,还有个需要注意的就是,虽然看上去是显示了3列,但实际上是按1列来处理的,所以下面的实现应该是返回1:

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {    return 1;}

参考

  1. UIPickerViewDelegate Protocol Reference

Constructing an object of class type ‘**’ with a metatype value must use a ‘required’ initializer.

参考 1. Implementing NSCopying in Swift with subclasses

Swift中”[AnyObject]? does not have a member named generator” 问题的处理

有个小需求,需要遍历当前导航控制器栈的所有ViewController。UINavigationController类自身的viewControllers属性返回的是一个[AnyObject]!数组,不过由于我的导航控制器本身有可能是nil,所以我获取到的ViewController数组如下:

var myViewControllers: [AnyObject]? = navigationController?.viewControllers

获取到的myViewControllers是一个[AnyObject]?可选类型,这时如果我直接去遍历myViewControllers,如下代码所示

for controller in myViewControllers {    ...}

编译器会报错,提示如下:

[AnyObject]? does not have a member named "Generator"

实际上,不管是[AnyObject]?还是其它的诸如[String]?类型,都会报这个错。其原因是可选类型只是个容器,它与其所包装的值是不同的类型,也就是说[AnyObject]是一个数组类型,但[AnyObject]?并不是数组类型。我们可以迭代一个数组,但不是迭代一个非集合类型。

在stackoverflow上有这样一个有趣的比方,我犯懒就直接贴出来了:

To understand the difference, let me make a real life example: you buy a new TV on ebay, the package is shipped to you, the first thing you do is to check if the package (the optional) is empty (nil). Once you verify that the TV is inside, you have to unwrap it, and put the box aside. You cannot use the TV while it's in the package. Similarly, an optional is a container: it is not the value it contains, and it doesn't have the same type. It can be empty, or it can contain a valid value.

所有,这里的处理应该是:

if let controllers = myViewControllers {    for controller in controllers {        ......    }}

参考

  1. Loop through [AnyObject]? results in does not have a member named generator
0 0
原创粉丝点击