现在大部分项目还是纯OC,即使迁移到Swift也只能是一点点模块过度,那么OC和Swift有什么样的区别呢?两者之间怎样相互调用?
一、注释
// MARK:
类似于OC中的#pragma mark
// MARK: -
类似于OC中的#pragma mark-
// TODO:
用于标记未完成的任务// FIXME: -
用于标记待修复的问题#warning("msg")
用来做全局提示
示例代码:
1 | public class Person { |
效果呈现:
使用MARK: -
时,代码区对应位置也会显示一条分割线(在标记位上方,颜色很淡)。
warning
效果:
注意:只能大写,不能小写,否则没有效果(AndroidStudio和IDEA做的比Xcode好太多)。
二、条件编译
Swift支持条件编译的内容是不多的,大概就是下面这些:
1 | // 操作系统:macOS\iOS\tvOS\watchOS\Linux\Android\Windows\FreeBSD |
自定义编译标记:
Xcode
默认有一个DEBUG
标记,我们也可以自己添加一个新的标记。Active Compilation Conditions
和Other Swift Flags
没有多大区别,只是在Other Swift Flags
区域增加标记时需要在最前面加上-D
。
1 | #if DEBUG |
在OC中是可以通过不同编译条件定义不同的宏,来控制不同环境下NSLog
是否有效。但是在Swift中只能通过定义一个新的函数,通过不同环境的编译标记让其运行:
1 | func log(_ msg: String) { |
我们不需要考虑在Release
环境下是否有多余log
函数占内存。因为编译器会自动做内联优化。
自定义精准打印:
1 | func log(_ msg: String, file: NSString = #file, line: Int = #line, fn: String = #function) { |
如果在log
函数内部直接使用print(#file, #line, #function, msg)
,每次打印都是同样的文件、同一行,同一个log
函数。因为#file, #line, #function
捕捉的是当前函数的环境。
为什么要使用OC的NSString
,因为NSString
的lastPathComponent
属性用起来更加便捷。
注意:在Swift中是没有宏的。
三、版本检测
3.1. 系统版本检测
示例代码:
1 | if #available(iOS 10, macOC 10.12, *) { |
3.2. API可用性说明
可以对一些废弃的API进行标记说明。
示例代码:
1 | // Person只在iOS10及以上、macOS 10.12及以上才可以使用 |
更多用法参考: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html
小技巧:有返回值的函数体暂时不写内部逻辑时,可以用
fatalError()
代替。
四、iOS程序的入口
在AppDelegate
上面默认有个@UIApplicationMain
标记,这表示编译器自动生成入口代码(main
函数代码),自动设置AppDelegate
为App
的代理。
也可以删掉@UIApplicationMain
,自定义入口代码:新建一个main.swift
文件,然后手动实现UIApplicationMain
函数(和OC的main.m
基本一致)。
1 | // main.swift |
注意:自定义入口代码的文件一定要是
main.swift
。
五、Swift调用OC
很多第三方代码/库都是用OC写的,而我们的项目使用Swift作为开发主语言。这时候就需要Swift调用OC的技术了。
5.1. 建立桥接头文件
方式一(手动创建):
新建1个桥接头文件,文件名格式默认为:
{targetName}-Bridging-Header.h
(文件名称是固定写法)。在
Build Settings
中设置头文件的位置。
方式二(自动创建):
如果源项目是Swift,在新建OC文件时,Xcode会提示是否创建桥接头文件,选择创建即可。
头文件的作用:
OC需要暴露给Swift的一些内容放到头文件中。
5.2. 调用OC代码
- 在桥接头文件中导入Swift需要用到的相关OC头文件。
1 | #import "DBPerson.h" |
DBPerson.h
1 | int sum(int a, int b); |
DBPerson.m
1 | #import "DBPerson.h" |
- 在Swift文件中使用导入的OC类。
1
2
3
4
5
6
7
8
9
10var 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 | // C |
注意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 | import Foundation |
OC调用Swift
1 | #import "SwiftDemo-Swift.h" |
SwiftDemo-Swift.h
Xcode会根据Swift代码自动生成对应的OC声明,写入{targetName-Swift.h}
文件。
提示:如果Swift代码写完之后发现在OC中无法提示或找不到,需要编译一下项目。不要修改
{targetName-Swift.h}
文件,因为这个文件内容是编译后自动生成的。
6.2. 重命名
可以通过@objc
重命名Swift暴露给OC的符号名(类名、属性名、函数名等)。
Swift代码:
1 | @objc(DBCar) |
OC使用:
1 | DBCar *car = [[DBCar alloc] initWithPrice:2.0 band:@"BMW"]; |
SwiftDemo-Swift.h
6.3. 选择器
Swift中依然可以使用选择器,使用#selector(name)
定义一个选择器。必须是被@objcMembers
或@objc
修饰的方法才可以定义选择器。
示例代码:
1 | @objcMembers class Person: NSObject { |
如果没有函数重载,选择器的函数名称后面不需要写参数列表。
Swift是没有runtime概念的,所以只能是暴露给OC的成员才可以使用选择器。
6.4. 思考
为什么Swift暴露给OC的类最终要继承自
NSObject
?- 因为这个类最终是要给OC使用的,OC的所有类最终都继承自
NSObject
,
- 因为这个类最终是要给OC使用的,OC的所有类最终都继承自
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
- OC调用Swift,Swift代码由于生成了OC代码,所以还是走runtime流程的,也就意味着必然有