Swift与OC混编
为什么要选择Swift
从2014年苹果推出Swift1.0到今年9月份的Swift5.1已经过去了5年,平均每年一个大版本,半年一个小版本的迭代速度已经让Swift语言日臻完善。从TIOBE指数来看,今年Swift站在了排行榜的第10位,11月份占有率为1.49%;OC已经从当年的双连冠跌落到了今年的第13位。国外Swift的使用早已在流行,不论是WWDC的技术分享还是Raywenderlich的在线课程都已经把语言的使用转为Swift。而国内apple开发者主要使用的语言仍是OC,这主要是因为现有的成熟应用当年是由OC构建并且有很深厚的技术沉淀,拥抱新变化往往会付出沉重的代价。当然也有一些年轻的团队使用Swift并且取得了很大的成功[例如头条,美团等]。
苹果官方是这么宣传Swift的
用Swift不论在手机、桌面、服务器还是在任何跑代码的地方编写软件都是很奇妙的。它是安全的、快速的、交互式的语言,它结合了最好的现代语言智慧,这智慧来自于广泛的苹果开发者文化和开源社区的各种贡献。编译器为性能而优化,语言为开发者优化,这两者全然不打折扣。
这描述的有点广告了。Swift的优势具体来说可以列出以下几点(相对于OC)
- Swift更接近自然语言可读性好,方法名称更短,语言更现代化,代码行数更少
- Swift不需要区分头文件和实现文件,这使项目中创建的文件显著减少便于管理
- Swift在处理集合、字符串等基本类型使更加灵活便捷
- Swift运行速度更快
- Swift支持动态库
- Swift Playground可以像Python的Notebook一样成为优秀的模拟实验平台,SwiftUI像Flutter、HTML一样用DSL来写页面。
- 开源让更多的开发者拥抱Swift,也从社区吸收了更多的营养
- 变量总是在使用前就初始化
- Optional确保nil可以被直接处理
- Swift还拥有强大的类型推导和模式匹配能力
- 强大的Enum、Struct、Protocol、Generics让语言更加灵活,业务实现更加容易。
- 在硬件层面上也对Swift编译和优化做了更好的支持,
早在2014年年末Mattt Thompson就在NSHipster上写了一篇文章"The Death of Cocoa",他说“就像Objective-C一样,Cocoa将会渐渐消亡。现在的问题不是它是否会消亡,而是消亡的时间”。Swift真的能重塑苹果的标准库吗,这只能让时间来检验了。
对于我们当下的开发者而言完全放弃OC显然是不明智的,而不去使用新的技术和语言也是低效缺乏竞争力的。苹果为开发者提供了一个好的解决方案:两种语言并存。使Swift能够以简单有效的方法和Objective-C进行互操作是很有战略意义的决定——同时在一定程度上也是必须的。这样做将允许团队中愿意冒险的工程师,以一种低风险的方式将Swift加入到已经在运行的代码中去,这对语言的普及有极大意义。工程师可以一边探索语言的特性,一边完善业务的开发。接下来我们将逐一探索Swift与OC混编的各种场景。
Swift与OC的混编场景
根据实践经验我们可以总结出以下几种混编场景
- OC&Swift Mixed In The Same Target
- Project中OC 调用 Swift
- Pod中OC 调用 Swift
- Project中Swift Project 调用 OC
- Pod中OC 调用 Swift
- OC&Swift Mixed In The Different Target
- OC Project 调用 Swift Pod
- Swift Project 调用 OC Pod
- OC Pod 调用 Swift Pod
- Swift Pod 调用 OC Pod
这里把Pod替换为Framework也是一样的,有时也把Project Target说成APP Target。1是同一target内混编,2是target间混编。
主工程里OC&Swift相互调用
在OC工程里创建一个Swift类会跳出如下弹框
点击
Create Bridging Header
按钮生成桥接文件MainProject-Bridging-Header.h
和MainProject-Swift
, MainProject
是工程名称。同时会在Build Settings里添加Swift Complier配置:如果没有点击Create Bridging Header
按钮,而是点击了Don't create
,那么将不会创建桥接文件,但是Build Settings里添加Swift Complier配置项,只是该配置项里没有Bridging Header,这时需要开发者自行创建桥接文件,并配置好路径。
OC调用Swift接口
- Swift类里的接口应该加上
@objc
修饰
class Dog: NSObject {
@objc let legNumber = 4
var temper = "temper-good"
@objc var friend = Cat()
@objc func eat() {
print("The dog is eating")
}
}
- 在OC类中首先要引用
MainProject-Swift.h
#import "MainProject-Swift.h"
- 最后就可以在OC类里引用Swift类
#import "MainProject-Swift.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Dog *dog = [[Dog alloc] init];
[dog eat];
}
@end
- 结论
我们再看看MainProject-Swift.h
到底是什么。MainProject-Swift.h
是一个自动生成的头文件,工程里没有对应的实体文件,可以Command+LeftClick
点击MainProject-Swift.h
进去看到其中有一段
@class Cat;
SWIFT_CLASS("_TtC11MainProject3Dog")
@interface Dog : NSObject
@property (nonatomic, readonly) NSInteger legNumber; // @objc let legNumber = 4
@property (nonatomic, strong, getter=friend, setter=setFriend:) Cat * _Nonnull friend_; //@objc var friend = Cat()
- (void)eat; //@objc func eat()
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
在Cat
这个Swift类中加了@objc
修饰的属性和方法都在MainProject-Swift.h
中自动生成了对应的OC属性和方法,所以通过引用MainProject-Swift.h
就可以去调用这些接口了。
Swift调用OC接口
Swift调用OC类略有不同
- 在
MainProject-Bridging-Header.h
桥接文件里引入需要被Swift调用的类
#import "Cat.h"
- Swift可以直接使用
MainProject-Bridging-Header.h
里引入的类
class Dog: NSObject {
@objc var friend = Cat()
func friendEatAction() {
friend.eat();
}
}
这个桥接过程比较简单。
如果主工是Swift编写的
这种情况下OC&Swift的相互调用与上述并无二致。在创建OC类的时候需要同时生成一个桥接文件和一个Swift转OC的接口文件,其他过程就不再赘述了。
OC主工程里调用Swift Pod
-
首先要确保OC主工程处于混编模式,否则再引入swfit pod,执行
pod install
时会报错(这个问题在问题2中有说明),此时最好是新建一个swift文件,自动创建桥接文件。 -
创建Swift类和接口。由于Swift接口是OC类调用的,所以Swift接口要有
@objc
修饰符; 另外接口调用是跨Target的,所以要有public
或open
关键字。如下代码,class
前面和接口前面有@objc public
修饰的就可以在主工程里使用。
@objc public class Dog: NSObject {
@objc let legNumber = 4
var temper = "temper-good"
@objc public func eat() {
print("The dog is eating")
}
}
-
OC调用Swift接口, Swift Pod工程的build setting里也有一个Interface Header文件,首先要在主工程里引入这个文件,让后就可以在OC里面使用Swift相关接口了。(除此之外还有@import导入方式,这是module机制)
#import "ViewController.h"
#import <SwiftPodFirst/SwiftPodFirst-Swift.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Dog *dog = [[Dog alloc] init];
[dog eat];
}
如果调用没有public的接口就会产生如下报错,OC可以访问的接口可以进入接口头文件SwiftPodFirst-Swift.h
查看。
No visible @interface for 'Dog' declares the selector 'temper'
Swift主工程里调用OC Pod
- 首先要确保Swift主工程处于混编模式,具体方法不再赘述。
- 在Swift主工程桥接文件
SwiftMainProject-Bridging-Header.h
中添加#import
引用
#import <OCPodSecond/Cat.h> // 这里OCPodSecond是OC pod,Cat是OC类
- Swift主工程中使用OC类
import UIKit
class ViewController: UIViewController {
let cat = Cat() // Cat是OC类
override func viewDidLoad() {
super.viewDidLoad()
cat.eat() // 是OC对象的方法
}
}
一个由OC语言创建的Pod里有Swift代码,再由一个OC主工程使用这个Pod(这是项目里最常用到的场景,也是最复杂的一个场景)
- 在之前的OCPodSecond里添加一个Swift类,使OC Pod变成混编模式。
- 在第1步之后会自动创建
OCPodSecond-Swift.h
接口头文件,但并不会有创建OC-Bridging-Header.h
的提示,那只好自己创建一个桥接文件并在build setting里关联路径 - 遵循之前的原则为Swift类添加
@objc public
接口
// 这个类是OCPodSecond里的Swift类
@objc public class Pig: NSObject {
@objc public func eat() {
print("The pig is eating!!")
}
}
- 最后在主工程里调用
#import "ViewController.h"
#import <SwiftPodFirst/SwiftPodFirst-Swift.h>
#import <OCPodSecond/Cat.h>
#import <OCPodSecond/OCPodSecond-Swift.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Dog *dog = [[Dog alloc] init]; // <SwiftPodFirst/SwiftPodFirst-Swift.h>
[dog eat];
Cat *cat = [[Cat alloc] init]; // <OCPodSecond/Cat.h>
[cat eat];
Pig *pig = [[Pig alloc] init]; // <OCPodSecond/OCPodSecond-Swift.h> 在OC Pod里添加的Swift类 Pig
[pig eat];
}
运行一下试试,结果出现报错提示,framework targets不支持bridging header(引文7),
<unknown>:0: error: using bridging headers with framework targets is unsupported
Command CompileSwiftSources failed with a nonzero exit code
在Support Files
里有个OCPodSecond-umbrella.h
头文件,这个头文件替代了bridging header的作用,文件中会自动导入OC类。因此在Pod里混编更加简单了,桥接文件和相关接口会自动生成。
另外,每当新增一个接口时,都有可能出现接口不可用的情况,这或许是因为语言之间的转换没有这么快,也可能是存在一些接口缓存文件没有及时更新。遇到这种情况,最好是重新build一下或者把pod重新装一遍再或者clean重启一下工程。
OC Pod引用Swift Pod
这种情形与主工程调用Swift Pod没有区别,只需引入相关的接口文件就可以xxx-Swift.h
,此处不再多述。需要注意的是,在podspec文件里要添加dependency,否则会无法引用相关pod。
Swift Pod 引用 OC Pod
这种情况有点特殊,虽然umbrella header有Bridging header的作用,但是umbrella header里的#import
指令是自动生成的,并且只有Pod内部混编时才会自动引用Pod内的OC header。所以这样只能支持Pod内部混编。
假设一个Swift Pod需要引用一个OC Pod,此时在Swift Pod的umbrella header里添加#import "<xxPod/xx.h>"
,虽然最后可以成功使用<xxPod/xx.h>
接口并且通过编译。但只要重新执行pod install
命令umbrella header就会被更新,里面自定义的import
指令会被全部删除。这意味着其他项目无法成功引用该Swift Pod。
这时我们需要modular的编译方式,在podfile里把use_frameworks!
改为use_modular_headers!
,这样就可以使用import
方式引用pod,import
方式同时兼容OC和Swift。
总结
最后混编路径汇总成下图,如果完全使用modular方式,可以把该图做一些简化。
问题1
[!] Unable to determine Swift version for the following pods:
- `SwiftPodFirst` does not specify a Swift version and none of the targets (`MainProject`) integrating
it have the `SWIFT_VERSION` attribute set. Please contact the author or set the `SWIFT_VERSION` attribute
in at least one of the targets that integrate this pod.
需要指定pod的swift版本,以便提供合适的编译器。修改方式是在集成的Target里指定SWIFT_VERSION
。例如我们这里产生的原因是OC主工程不是混编模式,所以没有swift相关的编译项,这里修改的方式是在OC主工程里新建一个Swift类,让主工程变为混编模式,会自动在build setting生成相关的swift编译项。
问题2
Module 'SwiftPodFirst' not found
找不到某个Module,这是我们再开发中最常遇到的问题了,导致这个报错的原因很多:
- https://docs.swift.org/swift-book/RevisionHistory/RevisionHistory.html
- https://www.tiobe.com/tiobe-index/
- https://juejin.im/post/5bd3172e518825292d6afc11
- https://nshipster.com/the-death-of-cocoa/
- Importing Swift into Objective-C
- Importing Objective-C into Swift
- https://blog.csdn.net/cooldragon/article/details/50172649