忽悠一个月又过去了,今年的9月还是挺精彩的。苹果发布iPhone 6s,iPad Pro,iOS 9,让我们又有很多活可做了;然后是XCodeGoast搅得圈内沸沸扬扬的,居然还惊动了CCAV;再然后苏宁的小伙伴们给大家送上了一份中秋礼物,虽然是老代码,但这是要搞哪样啊?不过,正是这些事情为我等屌丝的生活平添了许多的乐趣,还是挺酸爽的嘛。
嗯,回到正题上来。这期的知识小集主要是Swift开发的一些内容,主要的内容有三点:
Swift中随机数的使用Swift中String与CChar数组的转换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。如下所示:
|
|
如果我们想生成一个指定范围内的整型随机数,则可以使用arc4random() % upper_bound的方式,其中upper_bound指定的是上边界,如下处理:
|
|
不过使用这种方法,在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),如下所示:
|
|
而如果想指定区间的最小值(如随机数区间在[5, 100)),则可以如下处理:
|
|
当然,在Swift中也可以使用传统的C函数rand与random。不过这两个函数有如下几个缺点:
- 这两个函数都需要初始种子,通常是以当前时间来确定。
- 这两个函数的上限在
RAND_MAX=0X7fffffff(2147483647),是arc4random的一半。 rand函数以有规律的低位循环方式实现,更容易预测
我们以rand为例,看看其使用:
|
|
64位整型随机数
在大部分应用中,上面讲到的几个函数已经足够满足我们获取整型随机数的需求了。不过我们看看它们的函数声明,可以发现这些函数主要是针对32位整型来操作的。如果我们需要生成一个64位的整型随机数呢?毕竟现在的新机器都是支持64位的了。
目前貌似没有现成的函数来生成64位的随机数,不过jstn在stackoverflow上为我们分享了他的方法。我们一起来看看。
他首先定义了一个泛型函数,如下所示:
|
|
这个函数中使用了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位的随机数生成方法便可以如下实现:
|
|
我们来试用一下:
|
|
当然jstn还提供了Int64,UInt32,Int32的实现,大家可以脑补一下。
浮点型随机数
如果需要一个浮点值的随机数,则可以使用drand48函数,这个函数产生一个[0.0, 1.0]区间中的浮点数。这个函数的返回值是Double类型。其使用如下所示:
|
|
记住这个函数是需要先调用srand48生成一个种子的初始值。
一个小示例
最近写了一个随机键盘,需要对0-9这几个数字做个随机排序,正好用上了上面的arc4random函数,如下所示:
|
|
在闭包中,随机生成两个数,比较它们之间的大小,来确定数组的排序规则。还是挺简单的。
小结
其实如果翻看一下Swift中关于C函数的API,发现还有许多跟随机数相关的函数,如arc4random_addrandom,erand48等。上面的只是我们经常用到的一些函数,这几个函数基本上够用了。当然,不同场景有不同的需求,我们需要根据实际的需求来选择合适的函数。
以上的代码已上传到github,地址是Random.playground有需要的可以参考一下。
参考
- rand(3) / random(3) / arc4random(3) / et al.
- Random Swift
- 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类型,如下所示:
|
|
如果我们想将一个String转换成一个CChar数组,则可以使用String的cStringUsingEncoding方法,它是String扩展中的一个方法,其声明如下:
|
|
参数指定的是编码格式,我们一般指定为NSUTF8StringEncoding,因此下面这段代码:
|
|
其输出结果是:
|
|
可以看到"个"字由三个字节表示,这是因为Swift的字符串是Unicode编码格式,一个字符可能由1个或多个字节组成。另外需要注意的是CChar数组的最后一个元素是0,它表示的是一个字符串结束标志符\n。
我们知道,在C语言中,一个数组还可以使用指针来表示,所以字符串也可以用char *来表示。在Swift中,指针是使用UnsafePointer或UnsafeMutablePointer来包装的,因此,char指针可以表示为UnsafePointer<CChar>,不过它与[CChar]是两个不同的类型,所以以下代码会报编译器错误:
|
|
不过有意思的是我们可以直接将String字符串传递给带有UnsafePointer<CChar>参数的函数或方法,如以下代码所示:
|
|
而String字符串却不能传递给带有[CChar]参数的函数或方法,如以下代码会报错误:
|
|
实际上,在C语言中,我们在使用数组参数时,很少以数组的形式来定义参数,则大多是通过指针方式来定义数组参数。
如果想从[CChar]数组中获取一上String字符串,则可以使用String的fromCString方法,其声明如下:
|
|
从注释可以看到,它会将UTF-8数据拷贝以新字符串中。如下示例:
|
|
这里需要注意的一个问题是,CChar数组必须以0结束,否则会有不可预料的结果。在我的Playground示例代码中,如果没有0,报了以下错误:
|
|
还有可能出现的情况是CChar数组的存储区域正好覆盖了之前某一对象的区域,这一对象有一个可以表示字符串结尾的标识位,则这时候,str2输出的可能是"cde1一"。
小结
在Swift中,String是由独立编码的Unicode字符组成的,即Character。一个Character可能包括一个或多个字节。所以将String字符串转换成C语言的char *时,数组元素的个数与String字符的个数不一定相同(即在Swift中,与str.characters.count计算出来的值不一定相等)。这一点需要注意。另外还需要注意的就是将CChar数组转换为String时,数组最后一个元素应当为字符串结束标志符,即0。
参考
Swift中Selector方法的访问权限控制问题
今天用Swift写了个视图,在视图上加个手势,如下所示:
|
|
运行了下程序,然后崩溃了。崩溃日志如下:
|
|
而我已经在SwipeCardView类中定义了beginDragged:方法,如下所示:
|
|
由于我并不想将beginDragged:方法暴露出去,所以将其定义为一个private方法。方法的定义一切正常,手势的Selector方法也设置正常,却报了未找到方法的异常。那问题可能就应该在访问权限问题上了。
我们知道Selector是Objective-C的产物,它用于在运行时作为一个键值去找到对应方法的实现。一个Objective-C的方法是由objc_method结束体定义的,其声明如下:
|
|
这就要求selector引用的方法必须对ObjC运行时是可见的。而Swift是静态语言,虽然继承自NSObject的类默认对ObjC运行时是可见的,但如果方法是由private关键字修饰的,则方法默认情况下对ObjC运行时并不是可见的,所以就导致了以上的异常:运行时并没找到SwipeCardView类的beginDragged:方法。
所以,我们必须将private修饰的方法暴露给运行时。正确的做法是在 private 前面加上 @objc 关键字,这样就OK了。
|
|
另外需要注意的是,如果我们的类是纯Swift类,而不是继承自NSObject,则不管方法是private还是internal或public,如果要用在Selector中,都需要加上@objc修饰符。
参考
零碎
Swift中枚举项设置相同的值
在Objective-C及C语言中,在枚举中我们可以设置两个枚举项的值相等,如下所示:
|
|
在上例中,我们让枚举项TypeDefault的值等于TypeIn。
而在Swift中,要求枚举项的rawValue是唯一的,如果像下面这样写,则编译器会报错:
|
|
那如果我们希望上面枚举中Default的值与In的值一样,那应该怎么做呢?这时候就需要充分利用Swift中enum的特性了。我们知道,Swift中的enum与结构体、类一样,可以为其定义属性和方法,所以我们可以如下处理:
|
|
我们将Default定义为Type的一个静态只读属性,这个属性与枚举的其它枚举项的调用方式是一样的,可以如下调用:
|
|
参考
Swift中如何实现IBOutletCollection
在使用IB做界面开发时,我们经常需要将界面上的元素连接到我们的代码中。IBOutlet和IBAction就是专门用来做这事的两个关键字。另外在Objective-C还提供了一个伪关键字IBOutletCollection,它的实际作用是将界面上的一组相同的控件连接到一个数组中。具体可以参考iOS知识小集 第一期(2015.05.10)中的IBOutletCollection一节。
在Swift中,同样提供了@IBOutlet和@IBAction实现Objective-C中对应的功能,不过却没提供@IBOutletCollection来将一组相同控件连接到一个数组中。那如果我们想实现类似的功能,需要怎么处理呢?
实际上,我们在IB中选中一组相同的控件,然后将其连到到代码中时,会生成一个IBOutlet修饰的控件数组,类似于如下代码:
|
|
这就是Swift中类IBOutletCollection的处理。如果需要往数组中添加新建的对应的控件,则只需要在代码前面的小圆点与UI上的控件做个连线就OK了。而如果要想将控件从数组中移除,则只需要将对应的连接关系移除就可以了。