条件编译
Swift 中没有宏定义的概念,因此我们不能使用 #ifdef
的方法来检查某个符号是否经过宏定义。但是为了控制编译流程和内容,Swift 还是为我们提供了几种简单的机制来根据需求定制编译内容的。
首先是 #if
这一套编译标记还是存在的,使用的语法和原来也没有区别:
#if <condition>
#elseif <condition>
#else
#endif
当然, #elseif
和 #else
是可选的。但是这几个表达式里的 condition 并不是任意的。Swift 内建了几种平台和架构的组合,来帮助我们为不同的平台编译不同的代码,具体地:
方法 | 可选参数 |
---|---|
os() | macOS,iOS,tvOS,watchOS,Linux |
arch() | x86_64,arm,arm64,i386 |
swift() | >= 某个版本 |
注意这些方法和参数都是大小写敏感的。举个例子,如果我们统一我们在 iOS 平台和 Mac 平台的关于颜色的 API 的话,一种可能的方法就是配合 typealias 进行条件编译:
#if os(macOS)
typealias Color = NSColor
#else
typealias Color = UIColor
#endif
虽然 Swift 现在只能在上面列表中列出的平台上运行,但是os()
的可选用参数还包括 "FreeBSD","Windows" 和 "Android"。也许我们在不久的将来就能够在这些平台上看到 Swift 的身影。
另外对于 arch()
的参数需要说明的是 arm
和 arm64
两项分别对应 32 位 CPU 和 64 位 CPU 的真机情况,而对于模拟器,相应地 32 位设备的模拟器和 64 位设备的模拟器所对应的分别是 i386
和 x86_64
,它们也是需要分开对待的。
另一种方式是对自定义的符号进行条件编译,比如我们需要使用同一个 target 完成同一个 app 的收费版和免费版两个版本,并且希望在点击某个按钮时收费版本执行功能,而免费版本弹出提示的话,可以使用类似下面的方法:
@IBAction func someButtonPressed(sender: AnyObject!) {
#if FREE_VERSION
//弹出购买提示,导航至商店等
#else
// 实际功能
#endif
}
在这里我们用 FREE_VERSION
这个编译符号来代表免费版本。为了使之有效,我们需要在项目的编译选项中进行设置,在项目的 Build Settings
中,找到 Swift Compiler
- Custom Flags
,并在其中的 Other Swift Flags
加上 -D FREE_VERSION
就可以了。
自省
向一个对象发出询问,以确定它是不是属于某个类,这种操作就称为自省。在Objective-C中因为 id
这样的可以指向任意对象的指针的存在(其实严格来说 Objective-C 的指针的类型都是可以任意指向和转换的,它们只不过是帮助编译器理解你的代码而已),我们经常需要向一个对象询问它是不是属于某个类。常用的方法有下面两类:
[obj1 isKindOfClass:[ClassA class]];
[obj2 isMemberOfClass:[classB class]];
isKindOfClass:
判断 obj1
是否是 classA 或者其子类的实例对象; 而 isMemberOfClass:
则对 obj2
做出判断, 当且仅当 obj2
的类型为 ClassB
时返回为真。
这两个方法是 NSObject
的方法,所以我们在 Swift 中如果写的是 NSObject
的子类的话,直接使用这两个方法是没有任何问题的:
class ClassA: NSObject {}
class ClassB: ClassA {}
let obj1: NSObject = ClassB()
let obj2: NSObject = ClassB()
obj1.isKind(of: ClassA.self) //true
obj2.isMember(of: ClassA.self) //false
对于那些不是 NSObject
的类,我们应该怎么确定其类型呢?
首先需要明确的一点是,我们为什么需要在运行时去确定类型。因为 Swift 有泛型支持,Swift 对类型的推断和记录是完备的。因此在绝大多数情况下,我们使用的 Swift 类型都应该是在编译期间就确定的。如果在你写的代码中经常需要检查和确定 AnyObject
到底是什么类的话,几乎就意味着你的代码设计出了问题(或者你正在写一些充满各种"小技巧"的代码)。虽然没有太多的意义,但是我们还是可以做这件事情:
class ClassA {}
class ClassB: ClassA {}
let obj1: AnyObject = ClassB()
let obj2: AnyObject = ClassB()
obj1.isKind(of: ClassA.self) //true
obj2.isMember(of: ClassA.self) //false
在 Swift 中对于使用 AnyObject
最多的地方应该就是原来那些返回 id
的 Cocoa API 了。
为了快速确定类型,Swift 提供了一个简洁的写法: 对于一个不确定的类型,我们现在可以使用 is
来进行判断。在功能上相当于原来的 isKindOfClass
,可以检查一个对象是否属于某类型或其子类型。is
和原来的区别主要在于亮点,首先它不仅可以用于 class
类型上,也可以对 Swift 的其他像是 struct
或 enum
类型进行判断。使用起来是这个样子的:
class ClassA {}
class ClassB: ClassA {}
let obj: AnyObject = ClassB()
if (obj is ClassA) {
print("属于 ClassA")
}
if (obj is ClassB) {
print("属于 ClassB")
}
另外,编译器将对这种检查进行必要性的判断: 如果编译器能够唯一确定类型,那么 is
的判断就没有必要,编译器将会抛出一个警告,来提示你并没有转换的必要。
let string = "String"
if string is String {
// Do something
}
// 'is' test is always true
@UIApplicationMain
在 C 系语言中,程序的入口都是 main
函数。对于一个 Objective-C 的 iOS app 项目,在新建项目时, Xcode 将帮我们准备好一个main.m
文件,其中就有这个 main
函数:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([LLAppDelegate class]));
}
}
在这里我们调用了 UIKit 的 UIApplicationMain
方法。这个方法将根据第三个参数初始化一个 UIApplication
或其子类的对象并开始接收事件 (在这个例子中传入 nil
,意味使用默认的 UIApplication
)。最后一个参数指定了 LLAppDelegate
类作为应用的委托,它被用来接收类似 didFinishLaunching
或者 didEnterBackground
这样的与应用生命周期相关的委托方法。另外,虽然这个方法标明为返回一个 int
,但是其实它并不会真正返回。它会一直存在于内存中,直到用户或者系统将其强制终止。
了解了这些后,我们就可以来看看 Swift 的项目中对应的情况了。新建一个 Swift 的 iOS app 项目后,我们会发现所有文件中都没有一个像 Objective-C 时那样的 main.m
文件,也不存在 main
函 数。唯一和 main
有关系的是在默认的 AppDelegate
类的声明上方有一个 @UIApplicationMain
的标签。
不说可能您也已经猜到,这个标签做的事情就是将被标注的类作为委托,去创建一个 UIApplication
并启动整个程序。在编译的时候,编译器将寻找这个标记的类,并自动插入像 main
函数这样的模板代码。我们可以试试看把 @UIApplicationMain
去掉会怎么样:
Undefined symbols _main
说明找不到 `main` 函数了。
在一般情况下,我们并不需要对这个标签做任何修改,但是当我们如果想要使用 UIApplication
的子类而不是它本身的话,我们就需要对这部分内容 “做点手脚” 了。
刚才说到,其实 Swift 的 app 也是需要 mian
函数的,只不过默认情况下是 @UIApplicationMain
帮助我们自动生成了而已。和 C 系语言的 main.c
或者 main.m
文件一样,Swift 项目也可以有一个名为 main.swift
的特殊文件。在这个文件中,我们不需要定义作用域,而可以直接书写代码。这个文件中的代码将作为 main
函数来执行。比如我们在删除 @UIApplicationMain
后,在项目中添加 一个 main.swift
文件,然后加上这样的代码:
UIApplicationMain(CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to: UnsafeMutablePointer<Int8>.self,
capacity: Int(CommandLine.argc)),
nil,
NSStringFromClass(AppDelegate.self))
现在编译运行,就不会再出现错误了。当然,我们还可以通过将第三个参数替换成自己的 UIApplication
子类,这样我们就可以轻易地做一些控制整个应用行为的事情了。比如将 main.swift
的内容换成:
class MyApplication: UIApplication {
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
print("event sent: \(event)");
}
}
UIApplicationMain(CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to: UnsafeMutablePointer<Int8>.self,
capacity: Int(CommandLine.argc)),
NSStringFromClass(MyApplication.self),
NSStringFromClass(AppDelegate.self))
这样每次发送事件 (比如点击按钮) 时,我们都可以监听到这个事件了。