【iOS】Swift系列二十九 - 从OC到Swift(一)

现在大部分项目还是纯OC,即使迁移到Swift也只能是一点点模块过度,那么OC和Swift有什么样的区别呢?两者之间怎样相互调用?

一、注释

  • // MARK:类似于OC中的#pragma mark
  • // MARK: -类似于OC中的#pragma mark-
  • // TODO:用于标记未完成的任务
  • // FIXME: -用于标记待修复的问题
  • #warning("msg")用来做全局提示

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Person {
// MARK: - 属性
var age = 0
var weight = 0
var height = 0

// MARK: - 私有方法
// MARK: 跑步
private func run1() {
// TODO: 未完成
}
private func run2() {
// FIXME: 待修复
age += 20
}
// MARK: 走路
private func walk1() { }
private func walk2() { }

// MARK: - 公共方法
public func eat1() { }
public func eat2() { }
}

效果呈现:

使用MARK: -时,代码区对应位置也会显示一条分割线(在标记位上方,颜色很淡)。

warning效果:

注意:只能大写,不能小写,否则没有效果(AndroidStudio和IDEA做的比Xcode好太多)。

二、条件编译

Swift支持条件编译的内容是不多的,大概就是下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
// 操作系统:macOS\iOS\tvOS\watchOS\Linux\Android\Windows\FreeBSD
#if os(macOS) || os(iOS)
// CPU架构:i386\x86_64\arm\arm64
#elseif arch(x86_64) || arch(arm64)
// swift版本
#elseif swift(<5) && swift(>=3)
// 模拟器
#elseif targetEnvironment(simulator)
// 是否可以导入某模块
#elseif canImport(Foundation)
#else
#endif

自定义编译标记:

Xcode默认有一个DEBUG标记,我们也可以自己添加一个新的标记。Active Compilation ConditionsOther Swift Flags没有多大区别,只是在Other Swift Flags区域增加标记时需要在最前面加上-D

1
2
3
4
5
6
7
8
9
10
11
12
13
#if DEBUG
// debug模式
#else
// release模式
#endif

#if TEST
print("test")
#endif

#if OTHER
print("other")
#endif

在OC中是可以通过不同编译条件定义不同的宏,来控制不同环境下NSLog是否有效。但是在Swift中只能通过定义一个新的函数,通过不同环境的编译标记让其运行:

1
2
3
4
5
func log(_ msg: String) {
#if DEBUG
print(msg)
#endif
}

我们不需要考虑在Release环境下是否有多余log函数占内存。因为编译器会自动做内联优化。

自定义精准打印:

1
2
3
4
5
6
7
8
9
10
11
func log(_ msg: String, file: NSString = #file, line: Int = #line, fn: String = #function) {
#if DEBUG
let prefix = "from:\(file.lastPathComponent)_line:\(line)_fn:\(fn):"
print(prefix, msg)
#endif
}

func test() {
log("测试信息")
}
test() // 输出:from:main.swift_line:20_fn:test(): 测试信息

如果在log函数内部直接使用print(#file, #line, #function, msg),每次打印都是同样的文件、同一行,同一个log函数。因为#file, #line, #function捕捉的是当前函数的环境。

为什么要使用OC的NSString,因为NSStringlastPathComponent属性用起来更加便捷。

注意:在Swift中是没有宏的。

三、版本检测

3.1. 系统版本检测

示例代码:

1
2
3
4
5
if #available(iOS 10, macOC 10.12, *) {
// 对于iOS平台,只在iOS10及以上版本执行
// 对于macOS平台,只在macOS 10.12及以上版本执行
// 最后的*表示在其他所有平台都执行
}

3.2. API可用性说明

可以对一些废弃的API进行标记说明。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Person只在iOS10及以上、macOS 10.12及以上才可以使用
@available(iOS 10, macOS 10.12, *)
class Person { }

struct Student {
// study_已经被修改为study
@available(*, unavailable, renamed: "study")
func study_() { }
func study() { }

// run函数已经在iOS11被废弃
@available(iOS, deprecated: 11)
// run函数已经在macOS 10.11被废弃
@available(macOS, deprecated: 10.11)
func run() { }
}

更多用法参考: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html

小技巧:有返回值的函数体暂时不写内部逻辑时,可以用fatalError()代替。

四、iOS程序的入口

AppDelegate上面默认有个@UIApplicationMain标记,这表示编译器自动生成入口代码(main函数代码),自动设置AppDelegateApp的代理。

也可以删掉@UIApplicationMain,自定义入口代码:新建一个main.swift文件,然后手动实现UIApplicationMain函数(和OC的main.m基本一致)。

1
2
3
4
5
6
7
8
// main.swift
import UIKit

// 自定义Application
class DBApplication: UIApplication {}

// 程序入口
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, NSStringFromClass(DBApplication.self), NSStringFromClass(AppDelegate.self))

注意:自定义入口代码的文件一定要是main.swift

五、Swift调用OC

很多第三方代码/库都是用OC写的,而我们的项目使用Swift作为开发主语言。这时候就需要Swift调用OC的技术了。

5.1. 建立桥接头文件

方式一(手动创建):

  1. 新建1个桥接头文件,文件名格式默认为:{targetName}-Bridging-Header.h(文件名称是固定写法)。

  2. Build Settings中设置头文件的位置。

方式二(自动创建):

如果源项目是Swift,在新建OC文件时,Xcode会提示是否创建桥接头文件,选择创建即可。

头文件的作用:

OC需要暴露给Swift的一些内容放到头文件中。

5.2. 调用OC代码

  1. 在桥接头文件中导入Swift需要用到的相关OC头文件。
1
#import "DBPerson.h"

DBPerson.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sum(int a, int b);
@interface DBPerson : NSObject

@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *name;

- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;
+ (instancetype)personWithAge:(NSInteger)age name:(NSString *)name;

- (void)run;
+ (void)run;

- (void)eat:(NSString *)food other:(NSString *)other;
+ (void)eat:(NSString *)food other:(NSString *)other;

@end

DBPerson.m

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
#import "DBPerson.h"

int sum(int a, int b) {
return a + b;
}

@implementation DBPerson

- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name {
NSLog(@"-init");
if (self = [super init]) {
self.age = age;
self.name = name;
}
return self;
}

+ (instancetype)personWithAge:(NSInteger)age name:(NSString *)name {
NSLog(@"+init");
return [[self alloc] initWithAge:age name:name];
}

- (void)run {
NSLog(@"%zd %@ -run", _age, _name);
}

+ (void)run {
NSLog(@"Person +run");
}

- (void)eat:(NSString *)food other:(NSString *)other {
NSLog(@"%zd %@ -eat %@ %@", _age, _name, food, other);
}

+ (void)eat:(NSString *)food other:(NSString *)other {
NSLog(@"Person +eat %@ %@", food, other);
}

@end
  1. 在Swift文件中使用导入的OC类。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var p = DBPerson(age: 10, name: "Jack") // 输出:-init
    p.age = 18
    p.name = "Rose"
    p.run() // 输出:18 Rose -run
    p.eat("Apple", other: "Water") // 输出:18 Rose -eat Apple Water

    DBPerson.run() // 输出:Person +run
    DBPerson.eat("Pizza", other: "Banana") // 输出:Person +eat Pizza Banana

    print(sum(10, 20)) // 输出:30

5.3. 修改C函数名

如果C语言暴露给Swift的函数名和Swift中的其他函数名冲突了,可以在Swift中使用@_silgen_name修改C函数名。

示例代码:

1
2
3
4
5
6
7
8
9
10
// C
int sum(int a, int b) {
return a + b;
}

// Swift
@_silgen_name("sum")
func swift_sum(_ v1: Int32, _ v2: Int32) -> Int32
print(swift_sum(10, 20)) // 输出:30
print(sum(10, 20)) // 输出:30

注意Swift的函数参数类型一定要和C中原方法参数类型一致(C中的int对应Swift中的Int32)。

可以使用@_silgen_name调用底层私有API(谨慎使用)。

六、OC调用Swift

Xcode已经默认生成一个用于OC调用Swift的头文件,文件名格式是:{targetName-Swift.h}(固定格式)。

Swift暴露给OC的类最终继承自NSObject

  • 使用@objc修饰需要暴露给OC的成员
  • 使用@objcMembers修饰类
    • 代表默认所有成员都会暴露给OC(包括扩展中定义的成员)
    • 最终是否成功暴露,还需要考虑成员自身的访问级别

6.1. 调用Swift

Car.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Foundation

@objcMembers class Car: NSObject {
var price: Double
var band: String
init(price: Double, band: String) {
self.price = price
self.band = band
}
func run() {
print(price, band, "run")
}
static func run() {
print("Car run")
}
}

extension Car {
func test() {
print(price, band, "test")
}
}

OC调用Swift

1
2
3
4
5
6
7
8
#import "SwiftDemo-Swift.h"

void testSwift() {
Car *car = [[Car alloc] initWithPrice:2.0 band:@"BMW"];
[car test]; // 输出:2.0 BMW test
[car run]; // 输出:2.0 BMW run
[Car run]; // 输出:Car run
}

SwiftDemo-Swift.h

Xcode会根据Swift代码自动生成对应的OC声明,写入{targetName-Swift.h}文件。

提示:如果Swift代码写完之后发现在OC中无法提示或找不到,需要编译一下项目。不要修改{targetName-Swift.h}文件,因为这个文件内容是编译后自动生成的。

6.2. 重命名

可以通过@objc重命名Swift暴露给OC的符号名(类名、属性名、函数名等)。

Swift代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@objc(DBCar)
@objcMembers class Car: NSObject {
var price: Double
@objc(name)
var band: String
init(price: Double, band: String) {
self.price = price
self.band = band
}
@objc(drive)
func run() {
print(price, band, "run")
}
static func run() {
print("Car run")
}
}

extension Car {
@objc(exec:v2:)
func test(a: Int, b: Int) {
print(price, band, "test")
}
}

OC使用:

1
2
3
4
5
6
7
DBCar *car = [[DBCar alloc] initWithPrice:2.0 band:@"BMW"];
car.name = @"Benz";
car.price = 98.0;

[car drive]; // 输出:98.0 Benz run
[car exec:10 v2:20]; // 输出:98.0 Benz test
[DBCar run]; // 输出:Car run

SwiftDemo-Swift.h

6.3. 选择器

Swift中依然可以使用选择器,使用#selector(name)定义一个选择器。必须是被@objcMembers@objc修饰的方法才可以定义选择器。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@objcMembers class Person: NSObject {
func test1(v1: Int) {
print("test1")
}
func test2(v1: Int, v2: Int) {
print("test2(v1:v2:)")
}
func test2(_ v1: Double, _ v2: Double) {
print("test2(_:_:)")
}
func run() {
perform(#selector(test1))
perform(#selector(test1(v1:)))
perform(#selector(test2(v1:v2:)))
perform(#selector(test2(_:_:)))
perform(#selector(test2 as (Double, Double) -> Void))
}
}

如果没有函数重载,选择器的函数名称后面不需要写参数列表。

Swift是没有runtime概念的,所以只能是暴露给OC的成员才可以使用选择器。

6.4. 思考

  1. 为什么Swift暴露给OC的类最终要继承自NSObject

    • 因为这个类最终是要给OC使用的,OC的所有类最终都继承自NSObject
  2. p.run()底层是怎么调用的(走OC的runtime还是Swift的虚表)?反过来,OC调用Swift底层又是如何调用的?

    • OC调用Swift,Swift代码由于生成了OC代码,所以还是走runtime流程的,也就意味着必然有isa指针,而isa来自NSObject
    • Swift调用OC,最终还是走runtime。就算被@objcMembers修饰,Swift代码之间的调用还是虚表。
    • 如果Swift中的类成员(函数)必须使用OC的runtime实现时,可以将@objc替换为dynamic