现在大部分项目还是纯OC,即使迁移到Swift也只能是一点点模块过度,那么OC和Swift有什么样的区别呢?两者之间怎样相互调用?
一、注释
// MARK:
类似于OC中的#pragma mark
// MARK: -
类似于OC中的#pragma mark-
// TODO:
用于标记未完成的任务// FIXME: -
用于标记待修复的问题#warning("msg")
用来做全局提示
示例代码:
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支持条件编译的内容是不多的,大概就是下面这些:
// 操作系统: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 Conditions
和Other Swift Flags
没有多大区别,只是在Other Swift Flags
区域增加标记时需要在最前面加上-D
。
#if DEBUG
// debug模式
#else
// release模式
#endif
#if TEST
print("test")
#endif
#if OTHER
print("other")
#endif
在OC中是可以通过不同编译条件定义不同的宏,来控制不同环境下NSLog
是否有效。但是在Swift中只能通过定义一个新的函数,通过不同环境的编译标记让其运行:
func log(_ msg: String) {
#if DEBUG
print(msg)
#endif
}
我们不需要考虑在Release
环境下是否有多余log
函数占内存。因为编译器会自动做内联优化。
自定义精准打印:
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
,因为NSString
的lastPathComponent
属性用起来更加便捷。
注意:在Swift中是没有宏的。
三、版本检测
3.1. 系统版本检测
示例代码:
if #available(iOS 10, macOC 10.12, *) {
// 对于iOS平台,只在iOS10及以上版本执行
// 对于macOS平台,只在macOS 10.12及以上版本执行
// 最后的*表示在其他所有平台都执行
}
3.2. API可用性说明
可以对一些废弃的API进行标记说明。
示例代码:
// 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
函数代码),自动设置AppDelegate
为App
的代理。
也可以删掉@UIApplicationMain
,自定义入口代码:新建一个main.swift
文件,然后手动实现UIApplicationMain
函数(和OC的main.m
基本一致)。
// 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个桥接头文件,文件名格式默认为:
{targetName}-Bridging-Header.h
(文件名称是固定写法)。 -
在
Build Settings
中设置头文件的位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vdfrcsph-1620351458555)(http://blog.idbeny.com/nwy0h.png@normal)]
方式二(自动创建):
如果源项目是Swift,在新建OC文件时,Xcode会提示是否创建桥接头文件,选择创建即可。
头文件的作用:
OC需要暴露给Swift的一些内容放到头文件中。
5.2. 调用OC代码
- 在桥接头文件中导入Swift需要用到的相关OC头文件。
#import "DBPerson.h"
DBPerson.h
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
#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
- 在Swift文件中使用导入的OC类。
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函数名。
示例代码:
// 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
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
#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代码:
@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使用:
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
修饰的方法才可以定义选择器。
示例代码:
@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. 思考
-
为什么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流程的,也就意味着必然有