iOS知识小集 第5期(2015.09.26)

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

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

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

相对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函数randrandom。不过这两个函数有如下几个缺点:

  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位的随机数,不过jstnstackoverflow上为我们分享了他的方法。我们一起来看看。

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

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还提供了Int64UInt32Int32的实现,大家可以脑补一下。

浮点型随机数

如果需要一个浮点值的随机数,则可以使用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_addrandomerand48等。上面的只是我们经常用到的一些函数,这几个函数基本上够用了。当然,不同场景有不同的需求,我们需要根据实际的需求来选择合适的函数。

以上的代码已上传到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函数。在SwiftC的混编中,经常遇到的一个问题就是需要在两者中互相转换字符串。在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数组,则可以使用StringcStringUsingEncoding方法,它是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中,指针是使用UnsafePointerUnsafeMutablePointer来包装的,因此,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字符串,则可以使用StringfromCString方法,其声明如下:

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方法也设置正常,却报了未找到方法的异常。那问题可能就应该在访问权限问题上了。

我们知道SelectorObjective-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还是internalpublic,如果要用在Selector中,都需要加上@objc修饰符。

参考

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

零碎

Swift中枚举项设置相同的值

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

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的值一样,那应该怎么做呢?这时候就需要充分利用Swiftenum的特性了。我们知道,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做界面开发时,我们经常需要将界面上的元素连接到我们的代码中。IBOutletIBAction就是专门用来做这事的两个关键字。另外在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