条件编译

代码

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() 的参数需要说明的是 armarm64 两项分别对应 32 位 CPU 和 64 位 CPU 的真机情况,而对于模拟器,相应地 32 位设备的模拟器和 64 位设备的模拟器所对应的分别是 i386x86_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 的其他像是 structenum 类型进行判断。使用起来是这个样子的:

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))

这样每次发送事件 (比如点击按钮) 时,我们都可以监听到这个事件了。

results matching ""

    No results matching ""