南峰子的技术博客

攀登,一步一个脚印,方能知其乐


  • 首页

  • 知识小集

  • Swift

  • Objective-C

  • Cocoa

  • 翻译

  • 源码分析

  • 杂项

  • 归档

使用_ObjectiveCBridgeable协议实现Objective-C类与Swift结构体的无缝互转

发表于 2015-10-26   |   分类于 Swift

我们知道在Swift中,可以在NSArray与Array之间做无缝的转换,如下所示:

1
2
3
4
5
6
7
8
9
10
11
let mobile = ["iPhone", "Nokia", "小米Note"]
let mobile1 = (mobile as NSArray).objectAtIndex(1)
print(mobile1)
let animalArray = NSArray(objects: "lion", "tiger", "monkey")
var animalCount = (animalArray as Array).count
print(animalCount)
// 输出
// "Nokia"
// ["lion", "tiger", "monkey"]

编译器会为了我们完成所有转换,我们只需要拿来即用就行。当然,除了数组外,还有字典(Dictionary)、集合(Set)、字符串(String)也是一样。

问题

不过仔细一想,会发现,NSArray是类类型,而Array是结构体类型。一个是引用类型,一个是值类型,它们是怎样实现无缝转换的呢?这让我们想到了Cocoa Foundation与Core Foundation之间转换的toll-free bridging技术。那NSArray与Array之间是不是也应该有类似的桥接实现呢?

Objective-C Bridge

我们将鼠标移动到Array上,然后"cmd+鼠标点击",进入到Swift的声明文件中,在Array的注释中,可以看到下面这段:

1
2
3
4
5
6
/// Objective-C Bridge
/// ==================
/// The main distinction between Array and the other array types is that it interoperates seamlessly and efficiently with Objective-C.
/// Array<Element> is considered bridged to Objective-C iff Element is bridged to Objective-C.
// ......

可以看到Array与Objective-C的数组之间确实存在某种桥接技术,我们暂且称之为"Objective-C Bridge"桥接。那这又是如何实现的呢?

我们在当前文件中搜索bridge,会发现有这样一个协议:_ObjectiveCBridgeable。我们先来看看它的声明:

1
2
3
/// A Swift Array or Dictionary of types conforming to `_ObjectiveCBridgeable` can be passed to Objective-C as an NSArray or NSDictionary, respectively. The elements of the resulting NSArray or NSDictionary will be the result of calling `_bridgeToObjectiveC` on each element of the source container.
public protocol _ObjectiveCBridgeable {
}

即一个Swift数组或字典,如果其元素类型实现了_ObjectiveCBridgeable协议,则该数组或字典可以被转换成Objective-C的数组或字典。对于_ObjectiveCBridgeable协议,我们目前所能得到的文档就只有这些,也看不到它里面声明了什么属性方法。不过,可以看到这个协议是访问控制权限是public,也就意味着可以定义类来实现这个接口。这就好办了,下面就来尝试实现这样一个转换。

Objective-C类与Swift结构体的互转示例

在此先定义一个Objective-C类,如下所示:

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
// Mobile.h
@interface Mobile : NSObject
@property (nonatomic, strong) NSString *brand;
@property (nonatomic, strong) NSString *system;
- (instancetype)initWithBrand:(NSString *)brand system:(NSString *)system;
@end
// Mobole.m
@implementation Mobile
- (instancetype)initWithBrand:(NSString *)brand system:(NSString *)system {
self = [super init];
if (self) {
_brand = brand;
_system = system;
}
return self;
}
@end

同样,我定义一个Swift结构体,如下所示:

1
2
3
4
5
struct SwiftMobile {
let brand: String
let system: String
}

要想实现Mobile类与SwiftMobile结构体之间的互转,则SwiftMobile结构体需要实现_ObjectiveCBridgeable协议,如下所示:

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
29
30
31
32
extension SwiftMobile: _ObjectiveCBridgeable {
typealias _ObjectiveCType = Mobile
// 判断是否能转换成Objective-C对象
static func _isBridgedToObjectiveC() -> Bool {
return true
}
// 获取转换的目标类型
static func _getObjectiveCType() -> Any.Type {
return _ObjectiveCType.self
}
// 转换成Objective-C对象
func _bridgeToObjectiveC() -> _ObjectiveCType {
return Mobile(brand: brand, system: system)
}
// 强制将Objective-C对象转换成Swift结构体类型
static func _forceBridgeFromObjectiveC(source: _ObjectiveCType, inout result: SwiftMobile?) {
result = SwiftMobile(brand: source.brand, system: source.system)
}
// 有条件地将Objective-C对象转换成Swift结构体类型
static func _conditionallyBridgeFromObjectiveC(source: _ObjectiveCType, inout result: SwiftMobile?) -> Bool {
_forceBridgeFromObjectiveC(source, result: &result)
return true
}
}

可以看到SwiftMobile结构体主要实现了_ObjectiveCBridgeable接口的5个方法,从方法名基本上就能知道每个方法的用途。这里需要注意的是在本例中的_conditionallyBridgeFromObjectiveC只是简单地调用了_forceBridgeFromObjectiveC,如果需要指定条件,则需要更详细的实现。

让我们来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
let mobile = Mobile(brand: "iPhone", system: "iOS 9.0")
let swiftMobile = mobile as SwiftMobile
print("\(swiftMobile.brand): \(swiftMobile.system)")
let swiftMobile2 = SwiftMobile(brand: "Galaxy Note 3 Lite", system: "Android 5.0")
let mobile2 = swiftMobile2 as Mobile
print("\(mobile2.brand): \(mobile2.system)")
// 输出:
// iPhone: iOS 9.0
// Galaxy Note 3 Lite: Android 5.0

可以看到只需要使用as,就能实现Mobile类与SwiftMobile结构体的无缝转换。是不是很简单?

集合类型的无缝互换

回到数组的议题上来。

我们知道NSArray的元素类型必须是类类型的,它不支持存储结构体、数值等类型。因此,Array转换成NSArray的前提是Array的元素类型能被NSArray所接受。如果存储在Array中的元素的类型是结构体,且该结构体实现了_ObjectiveCBridgeable接口,则转换成NSArray时,编译器会自动将所有的元素转换成对应的类类型对象。以上面的SwiftMobile为例,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let sm1 = SwiftMobile(brand: "iPhone", system: "iOS 9.0")
let sm2 = SwiftMobile(brand: "Galaxy Note 3", system: "Android 5.0")
let sm3 = SwiftMobile(brand: "小米", system: "Android 4.0")
let mobiles = [sm1, sm2, sm3]
let mobileArray = mobiles as NSArray
print(mobileArray)
for i in 0..<mobiles.count {
print("\(mobileArray.objectAtIndex(i).brand): \(mobileArray.objectAtIndex(i).system)")
}
// 输出:
// (
// "<Mobile: 0x100c03f30>",
// "<Mobile: 0x100c03940>",
// "<Mobile: 0x100c039c0>"
// )
// iPhone: iOS 9.0
// Galaxy Note 3: Android 5.0
// 小米: Android 4.0

可以看到打印mobileArray数组时,其元素已经转换成了类Mobile的对象。一切都是那么的自然。而如果我们的SwiftMobile并没有实现_ObjectiveCBridgeable接口,则会报编译器错误:

1
'[SwiftMobile]' is not convertible to 'NSArray'

实际上,像Bool,Int, UInt,Float,Double,CGFloat这些数值类型也实现了_ObjectiveCBridgeable接口。我们可以从文档OS X v10.11 API Diffs - Swift Changes for Swift中找到一些线索:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Bool : _ObjectiveCBridgeable {
init(_ number: NSNumber)
}
extension Int : _ObjectiveCBridgeable {
init(_ number: NSNumber)
}
extension Float : _ObjectiveCBridgeable {
init(_ number: NSNumber)
}
// ... Double, UInt ...

(注意:整型类型只有Int与UInt实现了接口,而其它诸如Int16,Uint32,Int8等则没有)

它们的目标类型都是NSNumber类型,如下代码所示:

1
2
3
4
5
6
7
let numbers = [1, 29, 40]
let numberArray = (numbers as NSArray).objectAtIndex(2)
print(numberArray.dynamicType)
// 输出:
// __NSCFNumber

当然,要想实现Array与NSArray无缝切换,除了元素类型需要支持这种操作外,Array本身也需要能支持Objective-C Bridge,即它也需要实现_ObjectiveCBridgeable接口。在Swift文件的Array声明中并没有找到相关的线索:

1
public struct Array<Element> : CollectionType, Indexable, SequenceType, MutableCollectionType, MutableIndexable, _DestructorSafeContainer

线索依然在OS X v10.11 API Diffs - Swift Changes for Swift中,有如下声明:

1
2
3
extension Array : _ObjectiveCBridgeable {
init(_fromNSArray source: NSArray, noCopy noCopy: Bool = default)
}

因此,Array与NSArray相互转换需要两个条件:

  1. Array自身实现Objective-C Bridge桥接,这个Swift已经帮我们实现了。
  2. Array中的元素如果是数值类型或结构类型,必须实现Objective-C Bridge桥接。而如果是类类型或者是@objc protocol类型,则不管这个类型是Objective-C体系中的,还是纯Swift类型(不继承自NSObject),都可以直接转换。

另外,Array只能转换成NSArray,而不能转换成NSArray的子类,如NSMutableArray或NSOrderedArray。如下所示:

1
2
3
4
5
var objects = [NSObject(), NSObject(), NSObject()]
var objectArray = objects as NSMutableArray
// 编译器错误:
// '[NSObject]' is not convertible to 'NSMutableArray'

当然,反过来却是可以的。这个应该不需要太多的讨论。

小结

在Swift中,我们更多的会使用Array,Dictionary,Set这几个集合类型来存储数据,当然也会遇到需要将它们与Objective-C中对应的集合类型做转换的情况,特别是在混合编程的时候。另外,String也是可能经常切换的一个地方。不过,Apple已经帮我们完成了大部分的工作。如果需要实现自定义的结构体类型与Objective-C类的切换,则可以让结构体实现_ObjectiveCBridgeable接口。

这里还有个小问题,在Objective-C中实际上是有两个类可以用来包装数值类型的值:NSNumber与NSValue。NSNumber我们就不说了,NSValue用于包装诸如CGPoint,CGSize等,不过Swift并没有实现CGPoint类的值到NSValue的转换,所以这个需要我们自己去处理。

在Swift与Objective-C的集合类型相互转换过程中,还涉及到一些性能问题,大家可以看看对应的注释说明。在后续的文章中,会涉及到这一主题。

本文的部分实例代码已上传至github,可以在这里下载。

参考

  1. Easy Cast With _ObjectiveCBridgeable
  2. OS X v10.11 API Diffs - Swift Changes for Swift

iOS知识小集 第6期(2015.10.20)

发表于 2015-10-20   |   分类于 知识小集

天气有点冷啊,冬天快来了~~然后貌似互联网的冬天也来了啊。阿里缩减校招名额,美团融资失败,大众点评与美团报团,百度腾讯调整招聘,一丝丝的凉意啊~~再然后就是网易邮箱密码泄漏,这又是要搞哪样?不过话说我都不记得自己是不是有网易邮箱啊。额,不知道这个冬天的第一场雪什么时候会来。不管怎样,这个冬天还是窝一窝,等来年春暖花开之时再出去浪了。

这一期的主要内容还是三点:

  1. Xcode 7中Playground中导入并使用图片
  2. Playground中的字面量(Xcode 7.1)
  3. CAEmitterLayer实现粒子动画

内容不是很多,都是些小东西,主要还是一些知识碎片,这也是知识小集的出发点。所以大家就当是饭后的小点心吧。以后争取勤快一点,至少每个月出个两篇吧(what?不是说过好多次了么?)。

Xcode 7中Playground中导入并使用图片

在Playground中做测试时,可能需要显示图片,这时我们就需要导入一些图片资源。在Playground中,没有像普通工程那样有个单独的Images.xcassets文件夹来存储图片,不过添加图片也是件非常简单的事情。

如果没有显示project navigator,则可以使用快捷键cmd + 0打开。默认情况下,我们可以在project navigator看到两个group。一个是Source,另一个是Resources。其中Resources这个group就是用来放置资源的(包括图片资源),如下图所示:

image

选中这个group,然后点击左下角的+按钮,在弹出的菜单中选择Add Files to 'Resource',然后选择要添加的文件,点确定。这样就把资源文件添加到我们的Playground了。

添加完成后,我们就可以使用这些资源了。如要显示图片,则可以使用以下代码:

1
let image: UIImage = UIImage(named: "test.png")!

在Playground中显示如下:

image

另外,初始情况下,Playground的包里面并没有Resources文件夹,在我们添加资源后,会自动创建这个文件夹。然后我们可以在File Inspector中查看文件夹的具体位置,如下所示:

image

参考

  1. Playground Help - Adding Resources to a Playground
  2. Swift playgrounds with UIImage
  3. XCode 6: How To Add Image Assets To Your Playground

Playground中的字面量(Xcode 7.1)

Xcode 7.1新增了一项特性,让我们可以在playground代码中嵌入文件、图片和颜色的字面量。

以图片字面量为例,以往如果需要在playground使用图片资源,我们总是需要通过文件名来指定图片,如下代码所示:

1
let image = UIImage(named: "swift.png")

其效果如下:

image

而在Xcode 7.1中,我们无需在编辑器中键入"swift.png",而只需将图片从Finder或是资源中拖到我们的代码里面,就可以直接生成一个UIImage对象,如下所示:

image

可以看到,代码中=右侧的那个类似于小图标的东东就是一个图片字面量。是不是很酷来着?

与图片字面量类似,我们同样可以添加颜色字面量和文件字面量,添加方法可以参考Adding Color Literals和Adding File Literals。

当然,除了看上去很酷之外,这也让我们在playground中写代码时能够更快地去编辑这些资源。我们可以通过颜色选择器来插入我们想要的颜色,可以直接从Finder中将文件或图片拖到我们的代码中,而不再需要手动输入颜色值或文件名等。而如果我们想替换资源,只需要双击这些字面量就可以轻松地选择其它的资源。

字面量的表示

这里有一个问题,在代码中,这些字面量是如何表示的呢?

我们还是以图片字面量为例。选中一个图片字面量,cmd+C一下,然后找个文本编辑器,再cmd+V一下,发现拷贝出来的是如下信息:

1
[#Image(imageLiteral: "swift.png")#]

类似于一个构造器。我们再到UIImage中找找,可以看到UIImage有一个扩展,如下所示:

1
2
3
extension UIImage : _ImageLiteralConvertible {
required public convenience init(imageLiteral name: String)
}

这个扩展让UIImage类实现了_ImageLiteralConvertible协议,看这命名,貌似是一个私有的协议。我们来看看它的字义,如下所示:

1
2
3
4
5
/// Conforming types can be initialized with image literals (e.g.
/// `[#Image(imageLiteral: "hi.png")#]`).
public protocol _ImageLiteralConvertible {
public init(imageLiteral: String)
}

可以看到,实现这个协议的类型就可以使用图片字面量来初始化,即我们上面所看到。当然,我们没办法看到源码是怎么实现的,只能到此为止。

实际上,这些字面量会被转换成平台指定的类型,在官方的swift blog中列出了一个清单,如下所示:

image

还有件看起来很酷但似乎并不太实用的事是:这些字面量不但可以用在playground中,而且还有可用在工程代码中。不过之所有不太实用,是因为在工程代码中只能以纯文本的形式来展现,而不是像在playground中那样能直观的显示。这种纯文本形式即我们上面拷贝出来的信息,我们再贴一次:

1
[#Image(imageLiteral: "swift.png")#]

我们码段代码:

1
2
3
4
5
6
let image = [#Image(imageLiteral: "swift.png")#]
let imageView = UIImageView(image: image)
imageView.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
self.view.addSubview(imageView)

其效果如下所示:

image

从代码实践的角度来看,这种写法看上去并不是那么美啊。不过由于这种写法是与平台相关的,所以如果工程需要同时支持OSX、iOS和tvOS,还是可以考虑用一下的。

小结

总之,在playground中使用图片、颜色、文件字面量还是一件很酷的事,它大大提高了我们使用资源的效率,同时也更加直观,用起来还是满爽的。

这里附上官方的实例:Literals.playground

参考

  1. Literals in Playgrounds
  2. Adding Image Literals
  3. Adding Color Literals
  4. Adding File Literals

CAEmitterLayer实现粒子动画

前段时间@MartinRGB做了个带粒子效果的删除单元格动画,今天问他具体的实现方式,然后他把参考的原始工程发我看了一下。于是就找到了这个:UIEffectDesignerView。是github上的一个粒子动画的开源代码。其效果如下图所示:

image

这个动画的实现基于CAEmitterLayer类,它继承自CALayer。这个类是Core Animation提供的用于实现一个粒子发射器系统的类。这个类主要提供了一些属性来设置粒子系统的几何特性。我们可以如下处理:

1
2
3
4
5
6
7
8
9
let emitter = CAEmitterLayer()
// setup the emitter metrics
emitter.emitterPosition = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.height / 2)
emitter.emitterSize = self.bounds.size
// setup the emitter type and mode
let kEmitterModes = [ kCAEmitterLayerUnordered, kCAEmitterLayerAdditive, kCAEmitterLayerOldestLast, kCAEmitterLayerOldestFirst ]
emitter.emitterMode = kEmitterModes[ Int(valueFromEffect("emitterMode")) ]

需要注意的就是粒子系统会被绘制到层的背影颜色及边框之上。

当然,要想发射粒子,就需要有粒子源。一个粒子源定义了发射的粒子的方向及其它属性。在Core Animation中,使用CAEmitterCell对象来表示一个粒子源。CAEmitterCell定义了大量的属性来设置一个粒子源的特性,如粒子的显示特性(color, scale, style)、运动特性(如spin, emissionLatitude)、时间特性(如lifetime, birthRate, velocity)等。我们可以手动来设置这些值,也可以从文件中获取。在UIEffectDesignerView工程中,粒子发射器的信息是放在一个ped文件中,这个文件以JSON格式存储了粒子信息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"latitude": 0,
"alphaSpeed": 0,
"scaleSpeed": 0,
"blueRange": 0.33,
"width": 120,
"texture": "....",
"spinRange": 0,
"lifetime": 5,
"greenSpeed": 0,
"aux3": null,
"emitterType": 0,
"version": 0.1,
"zAcceleration": 0,
"velocity": 100,
"velocityRange": 150,
...
"y": 390,
"aux2": null
}

我们从文件中把粒子信息读取出来放到一个字典中,然后再将值赋给一个CAEmitterCell对象,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
// create new emitter cell
let emitterCell: CAEmitterCell = CAEmitterCell()
let effect: [String: AnyObject] = loadFile(filename)
...
emitterCell.birthRate = effect("birthRate")
emitterCell.lifetime = effect("lifetime")
emitterCell.lifetimeRange = effect("lifetimeRange")
emitterCell.velocity = effect("velocity")
emitterCell.velocityRange = effect("velocityRange")
...

之后,我们便可以把这个粒子源添加到粒子系统中,如下所示:

1
emitter.emitterCells = [ emitterCell ]

这样就可以发射粒子了。

当然对于一个粒子的特性,除了受粒子源设置的属性影响外,同样也还受粒子系统的一些属性的影响,如下代码所示:

1
emitter.scale = 0.5

其效果如下图所示:

image

另外,一个粒子源也可以包含一个子粒子源的数组,每个子源都可以作为一个独立的发射源。

参考

  1. UIEffectDesignerView
  2. CAEmitterLayer Class Reference
  3. CAEmitterCell Class Reference

零碎

C语言的int类型在Swift中的对应类型

一言以蔽之,C语言的int类型在Swift中的对应类型是CInt,它是Int32的一个别名。今天一哥们用Swift写了一段测试代码来调用C方法,方法中有个参数是int类型,类似于如下代码:

1
2
3
4
5
let str = "Hello, World!"
let a = str.characters.count
Test.test(a) // test方法接受一个int类型的参数
// 编译器错误:Cannot convert value of type 'Distance'(aka 'Int') to expected argument type Int32

可以看到我们需要传入一个Int32类型的参数。

之所以使用Int32,是因为在C语言中,int是4个字节,而Swift中的Int则依赖于平台,可能是4个字节,也可能是8个字节。嗯,这个问题是凑数的,点到为止吧。

Swift中获取类型的大小

在C语言中,如果我们想获取一个变量或数据类型的大小,则可以使用sizeof函数。如下所示:

1
2
3
4
int a = 10;
printf("%lu, %lu", sizeof(int), sizeof(a));
// 输出:4,4

在Swift中,也提供了相应的函数。我们可以使用sizeof来获取给定类型的大小,使用sizeofValue来获取给定值的类型的大小。如下所示:

1
2
3
4
sizeof(Int) // 8
let c: Int = 10
sizeofValue(c) // 8

不过,与C语言中的sizeof不同的是,Siwft中的sizeof与sizeofValue不包含任何内存对齐的填充部分。如timeval结构体,在C语言中的大小是16,而在Swift中,则是12,并未包含4个填充字节。

1
sizeof(timeval) // 12

不过,Swift提供了两个对应的函数,来计算经过内存对齐的类型的大小,即strideof秘strideofValue,如下所示:

1
2
3
4
let time = timeval(tv_sec: 10, tv_usec: 10)
strideof(timeval) // 16
strideofValue(time) // 16

参考

  1. Using Swift with Cocoa and Objective-C (Swift 2)

问题:纯Playground中使用Objective-C/C代码

今天想在纯Playground中测试一下CC_MD5方法,发现没招。因为CC_MD5实际上是一个C方法,需要导入<CommonCrypto/CommonCrypto.h>头文件。这就涉及到Swift与Objective-C混编,需要创建一个桥接文件。但是纯Playground貌似并不支持这么做(搜了一下没搜着解决方法)。于是只能采取曲线救国策略,建立一个基于Swift的工程,在这里面创建桥接文件,导入头文件。然后在工程中创建一个Playground来做测试了。

iOS知识小集 第5期(2015.09.26)

发表于 2015-09-26   |   分类于 知识小集

忽悠一个月又过去了,今年的9月还是挺精彩的。苹果发布iPhone 6s,iPad Pro,iOS 9,让我们又有很多活可做了;然后是XCodeGoast搅得圈内沸沸扬扬的,居然还惊动了CCAV;再然后苏宁的小伙伴们给大家送上了一份中秋礼物,虽然是老代码,但这是要搞哪样啊?不过,正是这些事情为我等屌丝的生活平添了许多的乐趣,还是挺酸爽的嘛。

嗯,回到正题上来。这期的知识小集主要是Swift开发的一些内容,主要的内容有三点:

  1. Swift中随机数的使用
  2. Swift中String与CChar数组的转换
  3. Swift中Selector方法的访问权限控制问题

相对Objective-C来说,个人觉得Swift写起来大多数时候还是挺爽的,简洁多了,以后有事没事还是多撸撸Swift。

Swift中随机数的使用

在我们开发的过程中,时不时地需要产生一些随机数。这里我们总结一下Swift中常用的一些随机数生成函数。这里我们将在Playground中来做些示例演示。

整型随机数

如果我们想要一个整型的随机数,则可以考虑用arc4random系列函数。我们可以通过man arc4random命令来看一下这个函数的定义:

The arc4random() function uses the key stream generator employed by the arc4 cipher, which uses 8*8 8 bit S-Boxes. The S-Boxes can be inabout (2^1700) states. The arc4random() function returns pseudo-random numbers in the range of 0 to (2^32)-1, and therefore has twice the range of rand(3) and random(3).

arc4random使用了arc4密码加密的key stream生成器(请脑补),产生一个[0, 2^32)区间的随机数(注意是左闭右开区间)。这个函数的返回类型是UInt32。如下所示:

1
arc4random() // 2,919,646,954

如果我们想生成一个指定范围内的整型随机数,则可以使用arc4random() % upper_bound的方式,其中upper_bound指定的是上边界,如下处理:

1
arc4random() % 10 // 8

不过使用这种方法,在upper_bound不是2的幂次方时,会产生一个所谓Modulo bias(模偏差)的问题。

我们在控制台中通过man arc4random命令,可以查看arc4random的文档,有这么一条:

arc4random_uniform() will return a uniformly distributed random number less than upper_bound. arc4random_uniform() is recommended over constructions like ‘’arc4random() % upper_bound’’ as it avoids “modulo bias” when the upper bound is not a power of two.

因此可以使用arc4random_uniform,它接受一个UInt32类型的参数,指定随机数区间的上边界upper_bound,该函数生成的随机数范围是[0, upper_bound),如下所示:

1
arc4random_uniform(10) // 6

而如果想指定区间的最小值(如随机数区间在[5, 100)),则可以如下处理:

1
2
3
let max: UInt32 = 100
let min: UInt32 = 5
arc4random_uniform(max - min) + min // 82

当然,在Swift中也可以使用传统的C函数rand与random。不过这两个函数有如下几个缺点:

  1. 这两个函数都需要初始种子,通常是以当前时间来确定。
  2. 这两个函数的上限在RAND_MAX=0X7fffffff(2147483647),是arc4random的一半。
  3. rand函数以有规律的低位循环方式实现,更容易预测

我们以rand为例,看看其使用:

1
2
3
srand(UInt32(time(nil))) // 种子,random对应的是srandom
rand() // 1,314,695,483
rand() % 10 // 8

64位整型随机数

在大部分应用中,上面讲到的几个函数已经足够满足我们获取整型随机数的需求了。不过我们看看它们的函数声明,可以发现这些函数主要是针对32位整型来操作的。如果我们需要生成一个64位的整型随机数呢?毕竟现在的新机器都是支持64位的了。

目前貌似没有现成的函数来生成64位的随机数,不过jstn在stackoverflow上为我们分享了他的方法。我们一起来看看。

他首先定义了一个泛型函数,如下所示:

1
2
3
4
5
func arc4random <T: IntegerLiteralConvertible> (type: T.Type) -> T {
var r: T = 0
arc4random_buf(&r, UInt(sizeof(T)))
return r
}

这个函数中使用了arc4random_buf来生成随机数。让我们通过man arc4random_buf来看看这个函数的定义:

arc4random_buf() function fills the region buf of length nbytes with ARC4-derived random data.

这个函数使用ARC4加密的随机数来填充该函数第二个参数指定的长度的缓存区域。因此,如果我们传入的是sizeof(UInt64),该函数便会生成一个随机数来填充8个字节的区域,并返回给r。那么64位的随机数生成方法便可以如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extension UInt64 {
static func random(lower: UInt64 = min, upper: UInt64 = max) -> UInt64 {
var m: UInt64
let u = upper - lower
var r = arc4random(UInt64)
if u > UInt64(Int64.max) {
m = 1 + ~u
} else {
m = ((max - (u * 2)) + 1) % u
}
while r < m {
r = arc4random(UInt64)
}
return (r % u) + lower
}
}

我们来试用一下:

1
UInt64.random() // 4758246381445086013

当然jstn还提供了Int64,UInt32,Int32的实现,大家可以脑补一下。

浮点型随机数

如果需要一个浮点值的随机数,则可以使用drand48函数,这个函数产生一个[0.0, 1.0]区间中的浮点数。这个函数的返回值是Double类型。其使用如下所示:

1
2
srand48(Int(time(nil)))
drand48() // 0.396464773760275

记住这个函数是需要先调用srand48生成一个种子的初始值。

一个小示例

最近写了一个随机键盘,需要对0-9这几个数字做个随机排序,正好用上了上面的arc4random函数,如下所示:

1
2
3
4
5
let arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
let numbers = arr.sort { (_, _) -> Bool in
arc4random() < arc4random()
}

在闭包中,随机生成两个数,比较它们之间的大小,来确定数组的排序规则。还是挺简单的。

小结

其实如果翻看一下Swift中关于C函数的API,发现还有许多跟随机数相关的函数,如arc4random_addrandom,erand48等。上面的只是我们经常用到的一些函数,这几个函数基本上够用了。当然,不同场景有不同的需求,我们需要根据实际的需求来选择合适的函数。

以上的代码已上传到github,地址是Random.playground有需要的可以参考一下。

参考

  1. rand(3) / random(3) / arc4random(3) / et al.
  2. Random Swift
  3. How does one generate a random number in Apple’s Swift language?

Swift中String与CChar数组的转换

在现阶段Swift的编码中,我们还是有很多场景需要调用一些C函数。在Swift与C的混编中,经常遇到的一个问题就是需要在两者中互相转换字符串。在C语言中,字符串通常是用一个char数组来表示,在Swift中,是用CChar数组来表示。从CChar的定义可以看到,其实际上是一个Int8类型,如下所示:

1
2
3
4
5
/// The C 'char' type.
///
/// This will be the same as either `CSignedChar` (in the common
/// case) or `CUnsignedChar`, depending on the platform.
public typealias CChar = Int8

如果我们想将一个String转换成一个CChar数组,则可以使用String的cStringUsingEncoding方法,它是String扩展中的一个方法,其声明如下:

1
2
3
4
/// Returns a representation of the `String` as a C string
/// using a given encoding.
@warn_unused_result
public func cStringUsingEncoding(encoding: NSStringEncoding) -> [CChar]?

参数指定的是编码格式,我们一般指定为NSUTF8StringEncoding,因此下面这段代码:

1
2
3
4
let str: String = "abc1个"
// String转换为CChar数组
let charArray: [CChar] = str.cStringUsingEncoding(NSUTF8StringEncoding)!

其输出结果是:

1
[97, 98, 99, 49, -28, -72, -86, 0]

可以看到"个"字由三个字节表示,这是因为Swift的字符串是Unicode编码格式,一个字符可能由1个或多个字节组成。另外需要注意的是CChar数组的最后一个元素是0,它表示的是一个字符串结束标志符\n。

我们知道,在C语言中,一个数组还可以使用指针来表示,所以字符串也可以用char *来表示。在Swift中,指针是使用UnsafePointer或UnsafeMutablePointer来包装的,因此,char指针可以表示为UnsafePointer<CChar>,不过它与[CChar]是两个不同的类型,所以以下代码会报编译器错误:

1
2
// Error: Cannot convert value of type '[CChar]' to specified type 'UnsafePointer<CChar>'
let charArray2: UnsafePointer<CChar> = str.cStringUsingEncoding(NSUTF8StringEncoding)!

不过有意思的是我们可以直接将String字符串传递给带有UnsafePointer<CChar>参数的函数或方法,如以下代码所示:

1
2
3
4
5
6
7
func length(s: UnsafePointer<CChar>) {
print(strlen(s))
}
length(str)
// 输出:7\n

而String字符串却不能传递给带有[CChar]参数的函数或方法,如以下代码会报错误:

1
2
3
4
5
6
func length2(s: [CChar]) {
print(strlen(s))
}
// Error: Cannot convert value of type 'String' to expected argument type '[CChar]'
length2(str)

实际上,在C语言中,我们在使用数组参数时,很少以数组的形式来定义参数,则大多是通过指针方式来定义数组参数。

如果想从[CChar]数组中获取一上String字符串,则可以使用String的fromCString方法,其声明如下:

1
2
3
4
5
6
7
/// Creates a new `String` by copying the nul-terminated UTF-8 data
/// referenced by a `CString`.
///
/// Returns `nil` if the `CString` is `NULL` or if it contains ill-formed
/// UTF-8 code unit sequences.
@warn_unused_result
public static func fromCString(cs: UnsafePointer<CChar>) -> String?

从注释可以看到,它会将UTF-8数据拷贝以新字符串中。如下示例:

1
2
3
4
let chars: [CChar] = [99, 100, 101, 0]
let str2: String = String.fromCString(chars)!
// 输出:cde

这里需要注意的一个问题是,CChar数组必须以0结束,否则会有不可预料的结果。在我的Playground示例代码中,如果没有0,报了以下错误:

1
Execution was interrupted. reason: EXC_BAD_INSTRUCTION

还有可能出现的情况是CChar数组的存储区域正好覆盖了之前某一对象的区域,这一对象有一个可以表示字符串结尾的标识位,则这时候,str2输出的可能是"cde1一"。

小结

在Swift中,String是由独立编码的Unicode字符组成的,即Character。一个Character可能包括一个或多个字节。所以将String字符串转换成C语言的char *时,数组元素的个数与String字符的个数不一定相同(即在Swift中,与str.characters.count计算出来的值不一定相等)。这一点需要注意。另外还需要注意的就是将CChar数组转换为String时,数组最后一个元素应当为字符串结束标志符,即0。

参考

  1. UTF8String
  2. String Structure Reference
  3. The Swift Programming Language中文版

Swift中Selector方法的访问权限控制问题

今天用Swift写了个视图,在视图上加个手势,如下所示:

1
2
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: "beginDragged:")
addGestureRecognizer(panGestureRecognizer)

运行了下程序,然后崩溃了。崩溃日志如下:

1
[**.SwipeCardView beginDragged:]: unrecognized selector sent to instance 0x125e5bc10

而我已经在SwipeCardView类中定义了beginDragged:方法,如下所示:

1
2
3
private func beginDragged(gestureRecognizer: UIPanGestureRecognizer) {
// ....
}

由于我并不想将beginDragged:方法暴露出去,所以将其定义为一个private方法。方法的定义一切正常,手势的Selector方法也设置正常,却报了未找到方法的异常。那问题可能就应该在访问权限问题上了。

我们知道Selector是Objective-C的产物,它用于在运行时作为一个键值去找到对应方法的实现。一个Objective-C的方法是由objc_method结束体定义的,其声明如下:

1
2
3
4
5
6
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

这就要求selector引用的方法必须对ObjC运行时是可见的。而Swift是静态语言,虽然继承自NSObject的类默认对ObjC运行时是可见的,但如果方法是由private关键字修饰的,则方法默认情况下对ObjC运行时并不是可见的,所以就导致了以上的异常:运行时并没找到SwipeCardView类的beginDragged:方法。

所以,我们必须将private修饰的方法暴露给运行时。正确的做法是在 private 前面加上 @objc 关键字,这样就OK了。

1
2
3
@objc private func beginDragged(gestureRecognizer: UIPanGestureRecognizer) {
// ....
}

另外需要注意的是,如果我们的类是纯Swift类,而不是继承自NSObject,则不管方法是private还是internal或public,如果要用在Selector中,都需要加上@objc修饰符。

参考

  1. SELECTOR
  2. @selector() in Swift?

零碎

Swift中枚举项设置相同的值

在Objective-C及C语言中,在枚举中我们可以设置两个枚举项的值相等,如下所示:

1
2
3
4
5
6
typedef NS_ENUM(NSUInteger, Type) {
TypeIn = 0,
TypeOut = 1,
TypeInOut = 2,
TypeDefault = TypeIn
};

在上例中,我们让枚举项TypeDefault的值等于TypeIn。

而在Swift中,要求枚举项的rawValue是唯一的,如果像下面这样写,则编译器会报错:

1
2
3
4
5
6
enum Type: UInt {
case In = 0
case Out = 1
case InOut = 2
case Default = 0 // Error: Raw value for enum case is not unique
}

那如果我们希望上面枚举中Default的值与In的值一样,那应该怎么做呢?这时候就需要充分利用Swift中enum的特性了。我们知道,Swift中的enum与结构体、类一样,可以为其定义属性和方法,所以我们可以如下处理:

1
2
3
4
5
6
7
8
9
10
11
enum Type: UInt {
case In = 0
case Out = 1
case InOut = 2
static var Default: Type {
get {
return .In
}
}
}

我们将Default定义为Type的一个静态只读属性,这个属性与枚举的其它枚举项的调用方式是一样的,可以如下调用:

1
let type: Type = .Default

参考

  1. Swift enum multiple cases with the same value

Swift中如何实现IBOutletCollection

在使用IB做界面开发时,我们经常需要将界面上的元素连接到我们的代码中。IBOutlet和IBAction就是专门用来做这事的两个关键字。另外在Objective-C还提供了一个伪关键字IBOutletCollection,它的实际作用是将界面上的一组相同的控件连接到一个数组中。具体可以参考iOS知识小集 第一期(2015.05.10)中的IBOutletCollection一节。

在Swift中,同样提供了@IBOutlet和@IBAction实现Objective-C中对应的功能,不过却没提供@IBOutletCollection来将一组相同控件连接到一个数组中。那如果我们想实现类似的功能,需要怎么处理呢?

实际上,我们在IB中选中一组相同的控件,然后将其连到到代码中时,会生成一个IBOutlet修饰的控件数组,类似于如下代码:

1
@IBOutlet var numberButtons: [UIButton]!

这就是Swift中类IBOutletCollection的处理。如果需要往数组中添加新建的对应的控件,则只需要在代码前面的小圆点与UI上的控件做个连线就OK了。而如果要想将控件从数组中移除,则只需要将对应的连接关系移除就可以了。

参考

  1. iOS知识小集 第一期(2015.05.10)
  2. Swift - IBOutletCollection equivalent

MMTweenAnimation实现分析

发表于 2015-09-23   |   分类于 源码分析

先来个效果图吧:

image

这是MMTweenAnimation库实现的一个弹跳动画。MMTweenAnimation基于Facebook的pop动画库,它提供了10套自定义的动画曲线,分别是:Back、Bounce、Circ、Cubic、Elastic、Expo、Quad、Quart、Quint、Sine。具体的效果可以参考MMTweenAnimation。

在这里,我们主要来MMTweenAnimation的具体实现及使用。

我们知道,动画实际上是许多帧静止的画面,以一定的速度连续播放,由于肉眼视觉残象产生的错觉,因此我们感觉画面是活动的。这就是动画的基本原理。所以,我们要做的就是按一定的速率去播放帧,在每一帧中计算曲线的路径,并将其绘制到界面上。这主要涉及到曲线的插值算法。

主要部件

MMTweenAnimation的主体类主要有两个:MMTweenAnimation和MMTweenFunction。MMTweenFunction类主要定义各种插值算法,MMTweenAnimation主要是实现动画操作。

MMTweenFunction类

MMTweenFunction类主要是实现各种插值算法。这些插值算法分别10类,即上面列出的10套动画。而每套根据不同的缓动方式,又分为EaseIn、EaseOut、EaseInOut三种,因此MMTweenAnimation库实际上是实现了30种动画。每个插值算法都实现为一个闭包函数,其定义如下:

1
2
3
4
typealias MMTweenFunctionBlock = (t: CFTimeInterval, // 当前时间与起始时间的差值
b: Double, // 起点
c: Double, // 起点与终点的差值
d: CFTimeInterval) -> Double // 动画持续时间

而每个动画的插值都是根据数学公式算法出来的,我们以图例中的Bounce-EaseOut动画为例,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let bounceOut: MMTweenFunctionBlock = { (t, b, c, d) -> Double in
let k: Double = 2.75
var t1 = t / d
if t1 < (1 / k) {
return c * (7.5625 * t1 * t1) + b
} else if t1 < (2 / k) {
t1 -= 1.5 / k
return c * (7.5625 * t1 * t1 + 0.75) + b
} else if t1 < (2.5 / k) {
t1 -= 2.25 / k
return c * (7.5625 * t1 * t1 + 0.9375) + b
} else {
t1 -= 2.625 / k
return c * (7.5625 * t1 * t1 + 0.984375) + b
}
}

计算出来的插值将会用于计算当前帧的终点值。

MMTweenAnimation类

MMTweenAnimation是实现动画的主体类。这个类继承自pop的POPCustomAnimation,POPCustomAnimation 直接继承自PopAnimation类,用于创建自定义动画的基类,它基本上是一个 display link的方便的转换,来在动画的每一个tick的回调block中驱动自定义的动画。

MMTweenAnimation定义了几个基本属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MMTweenAnimation: POPCustomAnimation {
var animationBlock: MMTweenAnimationBlock? // 动画回调
var fromValue: [CGFloat]? // 起点数组
var toValue: [CGFloat]? // 终点数组
var duration: Double = 0.3 // 动画时长
// ......
var functionBlock: MMTweenFunctionBlock? // 动画插值Block
// ......
// ......
var functionType: MMTweenFunctionType // 动画插值类型
// ......
// ......
var easingType: MMTweenEasingType // 动画缓动类型
// ......
}

而MMTweenAnimation类最关键的是定义它的回调block。MMTweenAnimation类定义了一个类方法animation(),在这个方法中,通过调用从父类继承来的便捷初始化方法

1
public convenience init!(block: POPCustomAnimationBlock!)

来创建一个MMTweenAnimation对象。其实现如下所示:

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
29
30
31
32
33
class func animation() -> MMTweenAnimation? {
let tweaner: MMTweenAnimation = MMTweenAnimation { (target, animation) -> Bool in
let anim: MMTweenAnimation = animation as! MMTweenAnimation
let t = animation.currentTime - animation.beginTime // 当前时间与起始时间的差值
let d = anim.duration
assert(anim.fromValue!.count == anim.toValue!.count, "fromValue.count != toValue.count")
if t < d { // 确保在动画持续时间类才处理
var values: [CGFloat] = [CGFloat]()
for i in 0..<anim.fromValue!.count {
if let functionBlock = anim.functionBlock { // 计算插值
values.append(CGFloat(functionBlock(t: t, b: Double(anim.fromValue![i]), c: Double(anim.toValue![i]) - Double(anim.fromValue![i]), d: d)))
}
}
if let animationBlock = anim.animationBlock { // 动画回调,以实现绘制操作
animationBlock(time: t, duration: d, values: values, target: target, animation: anim)
}
return true
} else {
return false
}
}
return tweaner
}

其中动画回调的定义如下:

1
typealias MMTweenAnimationBlock = (time: CFTimeInterval, duration: CFTimeInterval, values: [CGFloat], target: AnyObject, animation: MMTweenAnimation) -> Void

以上两个类便是MMTweenAnimation的主要部件。

动画示例

有了主要部件,我们就来看看怎么去使用它。MMTweenAnimation给了一个示例,其效果就是开头的图例。为此,MMTweenAnimation定义了类MMPaintView,这个类的主要目的就是绘制上面的曲线,其主要操作如下:

1
2
3
4
5
6
func addDot(point: CGPoint) {
__dots.append(point)
// __path = __interpolateCGPointsWithHermite(__dots)
__path = __interpolateCGPointsWithCatmullRom(__dots)
setNeedsDisplay()
}

这个方法首先是将参数中的点(即每一帧计算出来的终点值)添加到对象的__dots数组中,然后再通过__interpolateCGPointsWithCatmullRom方法创建一条Bezier曲线,最后调用setNeedsDisplay()来重新绘制曲线。

我们先来看看这个点是如何获取到的。在MMAnimationController类,我们定义动画对象时,设置了其动画回调,如下所示:

1
2
3
4
5
6
7
8
__anim!.animationBlock = { [unowned self] (diff: CFTimeInterval, duration: CFTimeInterval, values: [CGFloat], target: AnyObject, animation: MMTweenAnimation) -> Void in
let value: CGFloat = values[0] // 获取当前时间结束点的值
self.__dummy!.center = CGPoint(x: self.__dummy!.center.x, y: value) // 计算小红点的中心位置
self.__ball!.center = CGPoint(x: 50.0 + (CGRectGetWidth(UIScreen.mainScreen().bounds) - 150.0) * CGFloat(diff / duration), y: value)
self.__paintView!.addDot(self.__ball!.center)
}

这个动画回调获取当前时间结束点的值,用于设置小红点的中心位置,同时将这个中心位置的值丢给MMPaintView对象去生成Bezier曲线。

动画渲染操作执行的时间点

知道了MMTweenAnimation库的主要部件,我们现在来看看动画是如何被驱动的。我们在MMTweenAnimation类的animation()方法中,在动画回调的起始位置打个断点,运行一下程序,看看调用栈,如下所示:

image

可以看到在Run Loop中执行了一个观察者回调,在这个回调中调用了POPAnimator对象的_scheduleProcessPendingList方法的一个block回调,一直追溯到我们的动画操作。也就是说,是在Run Loop的某个时刻执行了一次动画的渲染。

我们再从代码入手,来看看动画执行代码是什么时候添加到Run Loop中的。在MMAnimationController的viewDidAppear方法中,有如下调用:

1
__dummy!.pop_addAnimation(__anim, forKey: "center")

其中pop_addAnimation方法是POPAnimator类中定义的。顺着代码,我们最终可以找到_scheduleProcessPendingList的定义,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)_scheduleProcessPendingList
{
// ......
if (!_pendingListObserver) {
__weak POPAnimator *weakSelf = self;
// 添加Run Loop监听器
_pendingListObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopExit, false, POPAnimationApplyRunLoopOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[weakSelf _processPendingList];
});
if (_pendingListObserver) {
CFRunLoopAddObserver(CFRunLoopGetMain(), _pendingListObserver, kCFRunLoopCommonModes);
}
}
// ......
}

可以看到在这个方法中创建了一个Run Loop的观察者,这个观察者在Run Loop的kCFRunLoopBeforeWaiting或kCFRunLoopExit阶段时会执行监听回调处理函数。回调函数中调用了_processPendingList方法,然后从调用栈里面可以看到,一直会执行到MMTweenAnimation的动画闭包中,即我们打断点的地方。

OK,动画渲染时间点找着了,那整个流程就可以完整拼接起来了。

小结

MMTweenAnimation的实现并不复杂,只要了解了动画的基本原理和其中的插值算法,再加上一些pop动画的基础知识,基本上就OK了。要想做出很牛B的动画,还是需要大量的数学知识。其实在MMTweenAnimation库中,除了那10套插值算法外,在MMPaintView类中,计算Bezier的控制点时,还用到了Catmull-Rom样条与Hermite样条,大家有兴趣可以研究一下。

MMTweenAnimation初始源码是Objective-C实现的,我将它用Swift重写了一遍,并放在github上,地址是MMTweenAnimation-Swift,有兴趣可以看一下。

本想放在知识小集中,但由于篇幅稍长,所以独立成篇。

参考

  1. MMTweenAnimation
  2. 交互式动画

App Transport Security(ATS)

发表于 2015-09-14   |   分类于 杂项

最近下载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使用了NSURLConnection、CFURL 或者NSURLSession相关的API来进行数据通信的话,则默认是通过ATS的方式来传输数据。在此配置下,如果我们使用HTTP来进行通信,则会导致请求失败,并报以下错误:

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),下表是一些键值及对应的类型和说明:

键 类型 说明
NSAppTransportSecurity Dictionary 配置ATS的顶层键值
NSAllowsArbitraryLoads Boolean 这是一个开关键,设置不在NSExceptionDomains列表中的其它域ATS特性。默认值是NO,如果设置为YES,则会关闭其它域的ATS特性。
NSExceptionDomains Dictionary 特定域列表
Dictionary 需要自定义配置的域名,键是对应的域名,如www.apple.com
NSExceptionMinimumTLSVersion String 指定域所需要的TLS的最低版本。有效值包括:TLSv1.0、TLSv1.1、TLSv1.2。默认值是TLSv1.2
NSExceptionRequiresForwardSecrecy Boolean 指定域是否需要支持前向保密。默认值是YES
NSExceptionAllowsInsecureHTTPLoads Boolean 指定域的请求是否允许使用不安全的HTTP。使用这个键来访问没有证书,或者证书是自签名、过期或主机名不匹配的证书。默认值为NO,表示需要使用HTTPS。
NSIncludesSubdomains Boolean 指定自定义的值是否应用到域的所有子域中。默认值是NO
NSThirdPartyExceptionMinimumTLSVersion String 类似于NSExceptionMinimumTLSVersion键,只不过指定的是应用本身无法控制的第三方组件的域所需要的TLS的最低版本。
NSThirdPartyExceptionRequiresForwardSecrecy Boolean 同上。指定第三方组件的域是否需要支持前向保密
NSThirdPartyExceptionAllowsInsecureHTTPLoads Boolean 同上。指定第三方组件的域的请求是否使用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中的解释是:

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

发表于 2015-08-31   |   分类于 翻译

原文由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知识小集 第4期(2015.08.15)

发表于 2015-08-15   |   分类于 知识小集

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

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

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

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

ARC与MRC的性能对比

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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环境下的,分别如下:

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
29
30
31
32
33
34
35
36
37
// 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);
}

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

1
2
3
4
5
6
7
// A组
ARC total time in seconds = 0.077761
MRC total time in seconds = 0.072469
// B组
ARC total time in seconds = 0.075722
MRC 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.

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

ARC is generally faster, and ARC can indeed be slower

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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会再将这个bitcode编译为可执行的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程序时,bit code选项默认是设置为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中,其定义如下:

1
2
3
4
typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {
UICollectionViewScrollDirectionVertical,
UICollectionViewScrollDirectionHorizontal
};

而在Swift中,其定义如下:

1
2
3
4
enum UICollectionViewScrollDirection : Int {
case Vertical
case Horizontal
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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中,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,相比较而言已经改进了不少】,它的声明如下:

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
/// 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协议的结构体,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
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的实例了,如下所示:

1
2
3
4
let direction: Directions = Directions.Left
if direction == Directions.Left {
// ...
}

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

1
2
3
4
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的静态常量,如下所示:

1
2
3
4
5
6
7
struct Directions: OptionSetType {
// ...
static let LeftUp: Directions = [Directions.Left, Directions.Up]
static let RightUp: Directions = [Directions.Right, Directions.Up]
// ...
}

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

1
2
3
if leftUp == Directions.LeftUp {
// ...
}

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

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

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

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

1
2
3
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对象,我们通常会像下面这样来定义我们的方法

1
2
3
4
5
6
7
8
9
+ (NSString )checkStringLength:(NSString *)str error:(NSError **)error {
if (str.length <= 0) {
*error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil];
return nil;
}
return str;
}

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

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

1
2
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,完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
+ (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?

UIKit: UIApearance

发表于 2015-07-20   |   分类于 Cocoa

文章开头先援引一下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声明了两个方法。如果我们想自定义一个类所有实例的外观,则可以使用下面这个方法:

1
2
3
4
5
// swift
static func appearance() -> Self
//objc
+ (instancetype)appearance

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

1
2
3
4
5
6
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()
]

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

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

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

1
2
3
4
5
6
7
8
9
10
11
[[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),又把这个方法给加上了,不过这回参数换成了数组,如下所示:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,在其前面加了如下几行代码:

1
2
3
4
5
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对应的方法试了一下,如下:

1
2
3
4
5
6
- (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):

1
2
3
$ 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;//'

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

1
2
3
4
5
6
7
8
9
./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的声明如下:

1
2
3
4
5
6
7
8
9
// 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

具体的实现如下:

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
@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的默认配置,如下所示:

1
2
3
4
5
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的实现就简单多了,我们只需要如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的方法作格式限制,具体要求如下:

1
2
3
4
5
6
7
// Swift
func propertyForAxis1(axis1: IntegerType, axis2: IntegerType, axisN: IntegerType) -> PropertyType
func setProperty(property: PropertyType, forAxis1 axis1: IntegerType, axis2: IntegerType)
//objc
- (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为例,它定义了以下方法:

1
2
3
4
5
setTitlePositionAdjustment:forBarMetrics:
backButtonBackgroundImageForState:barMetrics:
setBackButtonBackgroundImage:forState:barMetrics:

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

Trait Collection

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

1
2
3
4
5
6
7
8
// Swift
static func appearanceForTraitCollection(_ trait: UITraitCollection) -> Self
//objc
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait
whenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ...

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

一些深入的东西

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

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

1
2
3
4
- (void)setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
}

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

1
[RoundLabel appearance].borderWidth = 1.0f;

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

image

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

1
2
3
4
5
6
7
@interface _UIAppearance : NSObject {
NSMutableArray *_appearanceInvocations;
NSArray *_containerList;
_UIAppearanceCustomizableClassInfo *_customizableClassInfo;
NSMapTable *_invocationSources;
NSMutableDictionary *_resettableInvocations;
}

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

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
@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中尝试用以下命令来打印出它的信息:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<__NSArrayM 0x7fd44a5c1f80>(
<NSInvocation: 0x7fd44a5c1d20>
return value: {v} void
target: {@} 0x10b545ae0
selector: {:} setCornerRadius:
argument 2: {d} 0.000000
,
<NSInvocation: 0x7fd44a5bf300>
return value: {v} void
target: {@} 0x10b545ae0
selector: {:} setBorderColor:
argument 2: {@} 0x7fd44a5bbb80
,
<NSInvocation: 0x7fd44a50b8c0>
return value: {v} void
target: {@} 0x10b545ae0
selector: {:} setBorderWidth:
argument 2: {d} 0.000000
)

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

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

1
2
3
4
5
6
7
@interface _UIAppearanceRecorder : NSObject {
NSString *_classNameToRecord;
NSArray *_containerClassNames;
NSMutableArray *_customizations;
Class _superclassToRecord;
NSArray *_unarchivedCustomizations;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);

打印结果:

1
2
3
4
5
6
7
UIAppearanceExample2[7600:381708] _UIAppearanceRecorder instance : <_UIAppearanceRecorder: 0x7fa29a718960>
UIAppearanceExample2[7600:381708] variable's name: _classNameToRecord, value: RoundLabel
UIAppearanceExample2[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的外观,可以使用以下方式:

1
2
3
4
5
6
7
8
9
10
11
12
{
"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知识小集 第3期(2015.06.30)

发表于 2015-06-30   |   分类于 知识小集

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

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

这一期主要有三个内容:

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

Tint Color

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

tintColor属性的声明如下:

1
var tintColor: UIColor!

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

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

1
var tintAdjustmentMode: UIViewTintAdjustmentMode

枚举UIViewTintAdjustmentMode的定义如下:

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

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

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

1
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方法中,我们做了如下设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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“按钮时,会执行以下的事件处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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的功能,其对应处理方法如下:

1
2
3
4
5
6
7
8
@IBAction func dimTimtHandler(sender: AnyObject) {
if let isOn = self.dimTintSwitch?.on {
self.view.tintAdjustmentMode = isOn ? .Dimmed : .Normal
}
updateViewConstraints()
}

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

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

1
2
3
4
override func tintColorDidChange() {
tintColorLabel.textColor = self.tintColor
tintColorBlock.backgroundColor = self.tintColor
}

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

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

1
2
3
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中,我们经常使用预处理指令来帮助我们根据不同的平台执行不同的代码,以让我们的代码支持不同的平台,如:

1
2
3
4
5
#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可改成:

1
2
3
4
5
#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时不同的操作,如

1
2
3
4
5
#if DEBUG
let totalSeconds = totalMinutes
#else
let totalSeconds = totalMinutes * 60
#endif

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

1
2
3
4
5
#if build configuration
statements
#else
statements
#endif

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

1
2
3
4
5
6
7
#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个通知常量,来配合键盘在不同时间点的事件处理:

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

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

1
2
3
4
5
6
7
8
9
10
11
// 键盘在动画开始前的frame
let UIKeyboardFrameBeginUserInfoKey: String
// 键盘在动画线束后的frame
let UIKeyboardFrameEndUserInfoKey: String
// 键盘的动画曲线
let UIKeyboardAnimationCurveUserInfoKey: String
// 键盘的动画时间
let UIKeyboardAnimationDurationUserInfoKey: String

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

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

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

1
2
3
4
keyboard will change
keyboard will show
keyboard did change
keyboard did show

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

1
2
3
4
keyboard will change
keyboard will show
keyboard did change
keyboard did show

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

1
2
3
4
keyboard will change
keyboard will hide
keyboard did change
keyboard did hide

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
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中,声明了下面这样一个代理方法:

1
2
3
4
- (UIView *)pickerView:(UIPickerView *)pickerView
viewForRow:(NSInteger)row
forComponent:(NSInteger)component
reusingView:(UIView *)view

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

1
2
3
4
5
6
7
8
9
10
11
12
- (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:

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

参考

  1. UIPickerViewDelegate Protocol Reference

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

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

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

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

1
2
3
for controller in myViewControllers {
...
}

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

1
[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.

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

1
2
3
4
5
if let controllers = myViewControllers {
for controller in controllers {
......
}
}

​

参考

  1. Loop through [AnyObject]? results in does not have a member named generator

iOS知识小集 第2期(2015.05.31)

发表于 2015-05-31   |   分类于 知识小集

换了个厂子,还不到1个月。哎,着实是累啊,基本上是996.5的节奏,只会更多。加班把我快加吐了,但人在江湖,身不由已啊。为了讨口饭吃,命也不要了。谁让咱只是个臭写代码的呢。不过加班是多,只是长得太丑,所有没办法,没时间也得抽时间来学习。不然,饭都没得吃了,还得养家糊口呢。

本期总结的内容不是很多,主要有以下几个问题:

  1. 使用UIVisualEffectView为视图添加特殊效果
  2. Nullability Annotations
  3. weak的生命周期

使用UIVisualEffectView为视图添加特殊效果

在iOS 8后,苹果开放了不少创建特效的接口,其中就包括创建毛玻璃(blur)的接口。

通常要想创建一个特殊效果(如blur效果),可以创建一个UIVisualEffectView视图对象,这个对象提供了一种简单的方式来实现复杂的视觉效果。这个可以把这个对象看作是效果的一个容器,实际的效果会影响到该视图对象底下的内容,或者是添加到该视图对象的contentView中的内容。

我们举个例子来看看如果使用UIVisualEffectView:

1
2
3
4
5
6
7
8
let bgView: UIImageView = UIImageView(image: UIImage(named: "visual"))
bgView.frame = self.view.bounds
self.view.addSubview(bgView)
let blurEffect: UIBlurEffect = UIBlurEffect(style: .Light)
let blurView: UIVisualEffectView = UIVisualEffectView(effect: blurEffect)
blurView.frame = CGRectMake(50.0, 50.0, self.view.frame.width - 100.0, 200.0)
self.view.addSubview(blurView)

这段代码是在当前视图控制器上添加了一个UIImageView作为背景图。然后在视图的一小部分中使用了blur效果。其效果如下所示:

image

我们可以看到UIVisualEffectView还是非常简单的。需要注意是的,不应该直接添加子视图到UIVisualEffectView视图中,而是应该添加到UIVisualEffectView对象的contentView中。

另外,尽量避免将UIVisualEffectView对象的alpha值设置为小于1.0的值,因为创建半透明的视图会导致系统在离屏渲染时去对UIVisualEffectView对象及所有的相关的子视图做混合操作。这不但消耗CPU/GPU,也可能会导致许多效果显示不正确或者根本不显示。

我们在上面看到,初始化一个UIVisualEffectView对象的方法是UIVisualEffectView(effect: blurEffect),其定义如下:

1
init(effect effect: UIVisualEffect)

这个方法的参数是一个UIVisualEffect对象。我们查看官方文档,可以看到在UIKit中,定义了几个专门用来创建视觉特效的,它们分别是UIVisualEffect、UIBlurEffect和UIVibrancyEffect。它们的继承层次如下所示:

1
2
3
4
NSObject
| -- UIVisualEffect
| -- UIBlurEffect
| -- UIVibrancyEffect

UIVisualEffect是一个继承自NSObject的创建视觉效果的基类,然而这个类除了继承自NSObject的属性和方法外,没有提供任何新的属性和方法。其主要目的是用于初始化UIVisualEffectView,在这个初始化方法中可以传入UIBlurEffect或者UIVibrancyEffect对象。

一个UIBlurEffect对象用于将blur(毛玻璃)效果应用于UIVisualEffectView视图下面的内容。如上面的示例所示。不过,这个对象的效果并不影响UIVisualEffectView对象的contentView中的内容。

UIBlurEffect主要定义了三种效果,这些效果由枚举UIBlurEffectStyle来确定,该枚举的定义如下:

1
2
3
4
5
enum UIBlurEffectStyle : Int {
case ExtraLight
case Light
case Dark
}

其主要是根据色调(hue)来确定特效视图与底部视图的混合。

与UIBlurEffect不同的是,UIVibrancyEffect主要用于放大和调整UIVisualEffectView视图下面的内容的颜色,同时让UIVisualEffectView的contentView中的内容看起来更加生动。通常UIVibrancyEffect对象是与UIBlurEffect一起使用,主要用于处理在UIBlurEffect特效上的一些显示效果。接上面的代码,我们看看在blur的视图上添加一些新的特效,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
let vibrancyView: UIVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(forBlurEffect: blurEffect))
vibrancyView.setTranslatesAutoresizingMaskIntoConstraints(false)
blurView.contentView.addSubview(vibrancyView)
var label: UILabel = UILabel()
label.setTranslatesAutoresizingMaskIntoConstraints(false)
label.text = "Vibrancy Effect"
label.font = UIFont(name: "HelveticaNeue-Bold", size: 30)
label.textAlignment = .Center
label.textColor = UIColor.whiteColor()
vibrancyView.contentView.addSubview(label)

其效果如下图所示:

image

vibrancy特效是取决于颜色值的。所有添加到contentView的子视图都必须实现tintColorDidChange方法并更新自己。需要注意的是,我们使用UIVibrancyEffect(forBlurEffect:)方法创建UIVibrancyEffect时,参数blurEffect必须是我们想加效果的那个blurEffect,否则可能不是我们想要的效果。

另外,UIVibrancyEffect还提供了一个类方法notificationCenterVibrancyEffect,其声明如下:

1
class func notificationCenterVibrancyEffect() -> UIVibrancyEffect!

这个方法创建一个用于通知中心的Today扩展的vibrancy特效。

参考

  1. UIVisualEffectView Class Reference
  2. UIVisualEffect Class Reference
  3. UIBlurEffect Class Reference
  4. UIVibrancyEffect Class Reference
  5. UIVisualEffect – Swift Tutorial
  6. iOS 8: UIVisualEffect

Pointer is missing a nullability type specifier (nonnull or nullable)问题的处理 – Nullability Annotations

最近在用Xcode 6.3写代码,一些涉及到对象的代码会报如下编译器警告:

1
Pointer is missing a nullability type specifier (_nonnull or _nullable)

于是google了一下,发现这是Xcode 6.3的一个新特性,即nullability annotations。

Nullability Annotations

我们都知道在swift中,可以使用!和?来表示一个对象是optional的还是non-optional,如view?和view!。而在Objective-C中则没有这一区分,view即可表示这个对象是optional,也可表示是non-optional。这样就会造成一个问题:在Swift与Objective-C混编时,Swift编译器并不知道一个Objective-C对象到底是optional还是non-optional,因此这种情况下编译器会隐式地将Objective-C的对象当成是non-optional。

为了解决这个问题,苹果在Xcode 6.3引入了一个Objective-C的新特性:nullability annotations。这一新特性的核心是两个新的类型注释:__nullable和__nonnull。从字面上我们可以猜到,__nullable表示对象可以是NULL或nil,而__nonnull表示对象不应该为空。当我们不遵循这一规则时,编译器就会给出警告。

我们来看看以下的实例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(NSString * __nonnull)name;
@end
@implementation TestNullabilityClass
...
- (void)testNullability {
[self itemWithName:nil]; // 编译器警告:Null passed to a callee that requires a non-null argument
}
- (id)itemWithName:(NSString * __nonnull)name {
return nil;
}
@end

不过这只是一个警告,程序还是能编译通过并运行。

事实上,在任何可以使用const关键字的地方都可以使用__nullable和__nonnull,不过这两个关键字仅限于使用在指针类型上。而在方法的声明中,我们还可以使用不带下划线的nullable和nonnull,如下所示:

1
- (nullable id)itemWithName:(NSString * nonnull)name

在属性声明中,也增加了两个相应的特性,因此上例中的items属性可以如下声明:

1
@property (nonatomic, copy, nonnull) NSArray * items;

当然也可以用以下这种方式:

1
@property (nonatomic, copy) NSArray * __nonnull items;

推荐使用nonnull这种方式,这样可以让属性声明看起来更清晰。

Nonnull区域设置(Audited Regions)

如果需要每个属性或每个方法都去指定nonnull和nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针对象都被假定为nonnull,因此我们只需要去指定那些nullable的指针。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
NS_ASSUME_NONNULL_BEGIN
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(nullable NSString *)name;
@end
NS_ASSUME_NONNULL_END

在上面的代码中,items属性默认是non null的,itemWithName:方法的返回值也是non null,而参数是指定为nullable的。

不过,为了安全起见,苹果还制定了几条规则:

  1. typedef定义的类型的nullability特性通常依赖于上下文,即使是在Audited Regions中,也不能假定它为nonnull。
  2. 复杂的指针类型(如id )必须显示去指定是nonnull还是nullable。例如,指定一个指向nullable对象的nonnull指针,可以使用`”__nullable id __nonnull”`。
  3. 我们经常使用的NSError **通常是被假定为一个指向nullable NSError对象的nullable指针。

兼容性

因为Nullability Annotations是Xcode 6.3新加入的,所以我们需要考虑之前的老代码。实际上,苹果已以帮我们处理好了这种兼容问题,我们可以安全地使用它们:

  1. 老代码仍然能正常工作,即使对nonnull对象使用了nil也没有问题。
  2. 老代码在需要和swift混编时,在新的swift编译器下会给出一个警告。
  3. nonnull不会影响性能。事实上,我们仍然可以在运行时去判断我们的对象是否为nil。

事实上,我们可以将nonnull/nullable与我们的断言和异常一起看待,其需要处理的问题都是同一个:违反约定是一个程序员的错误。特别是,返回值是我们可控的东西,如果返回值是nonnull的,则我们不应该返回nil,除非是为了向后兼容。

参考

  1. Nullability and Objective-C

weak的生命周期

我们都知道weak表示的是一个弱引用,这个引用不会增加对象的引用计数,并且在所指向的对象被释放之后,weak指针会被设置的为nil。weak引用通常是用于处理循环引用的问题,如代理及block的使用中,相对会较多的使用到weak。

之前对weak的实现略有了解,知道它的一个基本的生命周期,但具体是怎么实现的,了解得不是太清晰。今天又翻了翻《Objective-C高级编程》关于__weak的讲解,在此做个笔记。

我们以下面这行代码为例:

1
2
3
{
id __weak obj1 = obj;
}

当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在Clang中的声明如下:

1
id objc_initWeak(id *object, id value);

其具体实现如下:

1
2
3
4
5
id objc_initWeak(id *object, id value)
{
*object = 0;
return objc_storeWeak(object, value);
}

示例代码轮换成编译器的模拟代码如下:

1
2
id obj1;
objc_initWeak(&obj1, obj);

因此,这里所做的事是先将obj1初始化为0(nil),然后将obj1的地址及obj作为参数传递给objc_storeWeak函数。

objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,或者指向一个有效的对象。

如果value是一个空指针或者其指向的对象已经被释放了,则object是zero-initialized的。否则,object将被注册为一个指向value的__weak对象。而这事应该是objc_storeWeak函数干的。objc_storeWeak的函数声明如下:

1
id objc_storeWeak(id *location, id value);

其具体实现如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
id objc_storeWeak(id *location, id newObj)
{
id oldObj;
SideTable *oldTable;
SideTable *newTable;
......
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
oldObj = *location;
oldTable = SideTable::tableForPointer(oldObj);
newTable = SideTable::tableForPointer(newObj);
......
if (*location != oldObj) {
OSSpinLockUnlock(lock1);
#if SIDE_TABLE_STRIPE > 1
if (lock1 != lock2) OSSpinLockUnlock(lock2);
#endif
goto retry;
}
if (oldObj) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
if (newObj) {
newObj = weak_register_no_lock(&newTable->weak_table, newObj,location);
// weak_register_no_lock returns NULL if weak store should be rejected
}
// Do not set *location anywhere else. That would introduce a race.
*location = newObj;
......
return newObj;
}

我们撇开源码中各种锁操作,来看看这段代码都做了些什么。在此之前,我们先来了解下weak表和SideTable。

weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了某个对象相关的的所有的弱引用信息。其定义如下(具体定义在objc-weak.h中):

1
2
3
4
5
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
......
};

其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
......
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};

其中referent是被引用的对象,即示例代码中的obj对象。下面的union即存储了所有指向该对象的弱引用。由注释可以看到,当out_of_line等于0时,hash表被一个数组所代替。另外,所有的弱引用对象的地址都是存储在weak_referrer_t指针的地址中。其定义如下:

1
typedef objc_object ** weak_referrer_t;

SideTable是一个用C++实现的类,它的具体定义在NSObject.mm中,我们来看看它的一些成员变量的定义:

1
2
3
4
5
6
7
8
9
10
class SideTable {
private:
static uint8_t table_buf[SIDE_TABLE_STRIPE * SIDE_TABLE_SIZE];
public:
RefcountMap refcnts;
weak_table_t weak_table;
......
}

RefcountMap refcnts,大家应该能猜到这个做什么用的吧?看着像是引用计数什么的。哈哈,貌似就是啊,这东东存储了一个对象的引用计数的信息。当然,我们在这里不去探究它,我们关注的是weak_table。这个成员变量指向的就是一个对象的weak表。

了解了weak表和SideTable,让我们再回过头来看看objc_storeWeak。首先是根据weak指针找到其指向的老的对象:

1
oldObj = *location;

然后获取到与新旧对象相关的SideTable对象:

1
2
oldTable = SideTable::tableForPointer(oldObj);
newTable = SideTable::tableForPointer(newObj);

下面要做的就是在老对象的weak表中移除指向信息,而在新对象的weak表中建立关联信息:

1
2
3
4
5
6
7
if (oldObj) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
if (newObj) {
newObj = weak_register_no_lock(&newTable->weak_table, newObj,location);
// weak_register_no_lock returns NULL if weak store should be rejected
}

接下来让弱引用指针指向新的对象:

1
*location = newObj;

最后会返回这个新对象:

1
return newObj;

objc_storeWeak的基本实现就是这样。当然,在objc_initWeak中调用objc_storeWeak时,老对象是空的,所有不会执行weak_unregister_no_lock操作。

而当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

  1. 调用objc_release
  2. 因为对象的引用计数为0,所以执行dealloc
  3. 在dealloc中,调用了_objc_rootDealloc函数
  4. 在_objc_rootDealloc中,调用了object_dispose函数
  5. 调用objc_destructInstance
  6. 最后调用objc_clear_deallocating

我们重点关注一下最后一步,objc_clear_deallocating的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void objc_clear_deallocating(id obj)
{
......
SideTable *table = SideTable::tableForPointer(obj);
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
OSSpinLockLock(&table->slock);
if (seen_weak_refs) {
arr_clear_deallocating(&table->weak_table, obj);
}
......
}

我们可以看到,在这个函数中,首先取出对象对应的SideTable实例,如果这个对象有关联的弱引用,则调用arr_clear_deallocating来清除对象的弱引用信息。我们来看看arr_clear_deallocating具体实现:

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
PRIVATE_EXTERN void arr_clear_deallocating(weak_table_t *weak_table, id referent) {
{
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == NULL) {
......
return;
}
// zero out references
for (int i = 0; i < entry->referrers.num_allocated; ++i) {
id *referrer = entry->referrers.refs[i].referrer;
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
objc_inform("_weak variable @ %p holds %p instead of %p\n", referrer, *referrer, referent);
}
}
}
weak_entry_remove_no_lock(weak_table, entry);
weak_table->num_weak_refs--;
}
}

这个函数首先是找出对象对应的weak_entry_t链表,然后挨个将弱引用置为nil。最后清理对象的记录。

通过上面的描述,我们基本能了解一个weak引用从生到死的过程。从这个流程可以看出,一个weak引用的处理涉及各种查表、添加与删除操作,还是有一定消耗的。所以如果大量使用__weak变量的话,会对性能造成一定的影响。那么,我们应该在什么时候去使用weak呢?《Objective-C高级编程》给我们的建议是只在避免循环引用的时候使用__weak修饰符。

另外,在clang中,还提供了不少关于weak引用的处理函数。如objc_loadWeak,objc_destroyWeak, objc_moveWeak等,我们可以在苹果的开源代码中找到相关的实现。等有时间,我再好好研究研究。

参考

  1. 《Objective-C高级编程》1.4: __weak修饰符
  2. Clang 3.7 documentation - Objective-C Automatic Reference Counting (ARC)
  3. apple opensource - NSObject.mm

零碎

CAGradientLayer

CAGradientLayer类是用于在其背景色上绘制一个颜色渐变,以填充层的整个形状,包括圆角。这个类继承自CALayer类,使用起来还是很方便的。

与Quartz 2D中的渐变处理类似,一个渐变有一个起始位置(startPoint)和一个结束位置(endPoint),在这两个位置之间,我们可以指定一组颜色值(colors,元素是CGColorRef对象),可以是两个,也可以是多个,每个颜色值会对应一个位置(locations)。另外,渐变还分为轴向渐变和径向渐变。

我们写个实例来看看CAGradientLayer的具体使用:

1
2
3
4
5
6
7
8
CAGradientLayer *layer = [CAGradientLayer layer];
layer.startPoint = (CGPoint){0.5f, 0.0f};
layer.endPoint = (CGPoint){0.5f, 1.0f};
layer.colors = [NSArray arrayWithObjects:(id)[UIColor blueColor].CGColor, (id)[UIColor redColor].CGColor, (id)[UIColor greenColor].CGColor, nil];
layer.locations = @[@0.0f, @0.6f, @1.0f];
layer.frame = self.view.layer.bounds;
[self.view.layer insertSublayer:layer atIndex:0];

参考

  1. CAGradientLayer Class Reference

Xcode中Ineligible Devices的处理

换了台新电脑,装了个Xcode 6.3,整了个新证书和profile,然后打开Xcode,连上手机。额,然后发现设备居然被标识为Ineligible Devices,没认出来。情况类似于下图:

image

电脑是受信任的,证书和profile也都是OK的。试了几次重启Xcode和重新连接手机,无效。设备就是选不了。最后是在Product->Destination里面才选中这个设备的。不过在工具栏还是不能选择,郁闷,求解。

iOS 7后隐藏UITextField的光标

新项目只支持iOS 7后,很多事情变得简单多了,就像隐藏UITextField的光标一样,就简单的一句话:

1
textFiled.tintColor = [UIColor clearColor];

通常我们用UIPickerView作为我们的UITextField的inputView时,我们是需要隐藏光标的。当然,如果想换个光标颜色,也是这么处理。

这么处理的有个遗留问题是:通常我们使用UIPickerView作为UITextField的inputView时, 并不希望去执行各种菜单操作(全选、复制、粘帖),但只是去设置UITextField的tintColor时,我们仍然可以执行这边操作,所以需要加额外的处理。这个问题,我们可以这样处理:在textFieldShouldBeginEditing:中,我们把UITextField的userInteractionEnabled设置为NO,然后在textFieldShouldEndEditing:,将将这个值设置回来。如下:

1
2
3
4
5
6
7
8
9
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
textField.userInteractionEnabled = NO;
return YES;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
textField.userInteractionEnabled = YES;
return YES;
}

这样就OK了。当然这只是我们当前使用的一种处理方式,还有其它的方法,直接google或者stackoverflow吧。

iOS 7后UIAlertView中文字左对齐问题

在iOS 7之前,如果我们想要让UIAlertView中的文字居左显示的话,可以使用以下这段代码来处理:

1
2
3
4
5
for (UIView *view in alert.subviews) {
if([[view class] isSubclassOfClass:[UILabel class]]) {
((UILabel*)view).textAlignment = NSTextAlignmentLeft;
}
}

但很遗憾的是,在iOS 7之后,苹果不让我们这么干了。我们去取UIAlertView的subviews时,获得的只是一个空数组,我们没有办法获取到我们想要的label。怎么办?三条路:告诉产品经理和UED说这个实现不了(当然,这个是会被鄙视的,人家会说你能力差);自己写;找第三方开源代码。嘿嘿,不过由于最近时间紧,所以我决定跟他们说实现不了,哈哈。不过在github上找了一个开源的,Custom iOS AlertView,star的数量也不少,看来不错,回头好好研究研究。

123…9
南峰子

南峰子

86 日志
7 分类
© 2017 南峰子
由 Hexo 强力驱动
主题 - NexT.Pisces