OC转Swift指南_头文件

运行环境:Xcode 11.1 Swift5.0

最近参与的一个项目需要从Objective-C(以下简称OC)到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。

OCSwift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide。

换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。

自动化工具

有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。

Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。OC转Swift指南_头文件_02

手动Swift化

桥接文件

如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个 ​.h​​的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个 ​.h​​文件,然后在 ​BuildSettings>SwiftCompiler-General>Objective-CBridgingHeader​​中填入该 ​.h​文件的路径。

OC转Swift指南_c代码_03

这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。

  1. #import "Utility.h"
  2. #import <Masonry/Masonry.h>

在 ​BridgingHeader​​的下面还有一个配置项是 ​Objective-CGeneratedInterfaceHeaderName​​,对应的值是 ​ProjectName-Swift.h​​。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分成OC代码,OC部分的文件会类似 ​pch​​一样全局引用这个头文件。因为是Build过程中生成的,所以只有 ​.m​​文件中可以直接引用,对于在 ​.h​文件中的引用下文有介绍。

Appdelegate(程序入口)

Swift中没有 ​main.m​​文件,取而代之的是 ​@UIApplicationMain​​命令,该命令等效于原有的执行 ​main.m​​。所以我们可以把 ​main.m​文件进行移除。

系统API

对于 ​UIKit​框架中的大部分代码换可以直接查看系统API文档进行转换,这里就不过多介绍。

property(属性)

Swift没有 ​property​​,也没有 ​copy​​, ​nonatomic​​等属性修饰词,只有表示属性是否可变的 ​let​​和 ​var​。

注意点一OC中一个类分 ​.h​​和 ​.m​​两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将 ​.m​​中的property标为 ​private​​,即外接无法直接访问,对于 ​.h​​中的property不做处理,取默认的 ​internal​,即同模块可访问。

对于函数的迁移也是相同的。

注意点二有一种特殊情况是在OC项目中,某些属性在内部( ​.m​​)可变,外部( ​.h​)只读。这种情况可以这么处理:

  1. private(set) var value: String

就是只对 ​value​​的 ​set​​方法就行 ​private​标记。

注意点三Swift中针对空类型有个专门的符号 ​?​​,对应OC中的 ​nil​​。OC中没有这个符号,但是可以通过在 ​nullable​​和 ​nonnull​表示该种属性,方法参数或者返回值是否可以空。

如果OC中没有声明一个属性是否可以为空,那就去默认值 ​nonnull​。

如果我们想让一个类的所有属性,函数返回值都是 ​nonnull​,除了手动一个个添加之外还有一个宏命令。

  1. NS_ASSUME_NONNULL_BEGIN
  2. /* code */
  3. NS_ASSUME_NONNULL_END

enum(枚举)

OC代码:

  1. typedef NS_ENUM(NSInteger, PlayerState) {

  2. PlayerStateNone = 0,

  3. PlayerStatePlaying,

  4. PlayerStatePause,

  5. PlayerStateBuffer,

  6. PlayerStateFailed,

  7. };

  8. typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) {

  9. XXViewAnimationOptionNone = 1 << 0,

  10. XXViewAnimationOptionSelcted1 = 1 << 1,

  11. XXViewAnimationOptionSelcted2 = 1 << 2,

  12. }

Swift代码

  1. enum PlayerState: Int {
  2. case none = 0
  3. case playing
  4. case pause
  5. case buffer
  6. case failed
  7. }
  8. struct ViewAnimationOptions: OptionSet {
  9. let rawValue: UInt
  10. static let None = ViewAnimationOptions(rawValue: 1<<0)
  11. static let Selected1 = ViewAnimationOptions(rawValue: 1<<0)
  12. static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2)
  13. //...
  14. }

Swift没有 ​NS_OPTIONS​​的概念,取而代之的是为了满足 ​OptionSet​​协议的 ​struct​类型。

懒加载

OC代码:

  1. - (MTObject *)object {
  2. if (!_object) {
  3. _object = [MTObject new];
  4. }
  5. return _object;
  6. }

Swift代码:

  1. lazy var object: MTObject = {
  2. let object = MTObject()
  3. return imagobjecteView
  4. }()

闭包

OC代码:

  1. typedef void (^DownloadStateBlock)(BOOL isComplete);

Swift代码:

  1. typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void)

单例

OC代码:

  1. + (XXManager *)shareInstance {
  2. static dispatch_once_t onceToken;
  3. dispatch_once(&onceToken, ^{
  4. instance = [[self alloc] init];
  5. });
  6. return instance;
  7. }

Swift对单例的实现比较简单,有两种方式:

第一种

swiftletshared=XXManager()// 声明在全局命名区(global namespace)ClassXXManager{}

你可能会疑惑,为什么没有 ​dispatch_once​​,如何保证多线程下创建的唯一性?其实是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,之后所有的调用都会使用该实例。而且全局变量的初始化是默认使用 ​dispatch_once​​的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了 ​shard​的原子性

第二种

  1. Class XXManager {
  2. static let shared = XXManager()
  3. private override init() {
  4. // do something
  5. }
  6. }

Swift 2 开始增加了 ​static​​关键字,用于限定变量的作用域。如果不使用 ​static​​,那么每一个 ​shared​​都会对应一个实例。而使用 ​static​​之后, ​shared​​成为全局变量,就成了跟上面第一种方式原理一致。可以注意到,由于构造器使用了 ​private​ 关键字,所以也保证了单例的原子性。

初始化方法和析构函数

对于初始化方法OC先调用父类的初始化方法,然后初始自己的成员变量。Swift先初始化自己的成员变量,然后在调用父类的初始化方法。

OC代码:

  1. // 初始化方法

  2. @interface MainView : UIView

  3. @property (nonatomic, strong) NSString *title;

  4. - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER;

  5. @end

  6. @implementation MainView

  7. - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title {

  8. if (self = [super initWithFrame:frame]) {

  9. self.title = title;

  10. }

  11. return self;

  12. }

  13. @end

  14. // 析构函数

  15. - (void)dealloc {

  16. //dealloc

  17. }

Swift代码:

  1. class MainViewSwift: UIView {
  2. let title: String
  3. init(frame: CGRect, title: String) {
  4. self.title = title
  5. super.init(frame: frame)
  6. }
  7. required init?(coder: NSCoder) {
  8. fatalError("init(coder:) has not been implemented")
  9. }
  10. deinit {
  11. //deinit
  12. }
  13. }

函数调用

OC代码:

  1. // 实例函数(共有方法)
  2. - (void)configModelWith:(XXModel *)model {}
  3. // 实例函数(私有方法)
  4. - (void)calculateProgress {}
  5. // 类函数
  6. + (void)configModelWith:(XXModel *)model {}

Swift代码

  1. // 实例函数(共有方法)
  2. func configModel(with model: XXModel) {}
  3. // 实例函数(私有方法)
  4. private func calculateProgress() {}
  5. // 类函数(不可以被子类重写)
  6. static func configModel(with model: XXModel) {}
  7. // 类函数(可以被子类重写)
  8. class func configModel(with model: XXModel) {}
  9. // 类函数(不可以被子类重写)
  10. class final func configModel(with model: XXModel) {}

OC可以通过是否将方法声明在 ​.h​​文件表明该方法是否为私有方法。Swift中没有了 ​.h​​文件,对于方法的权限控制是通过权限关键词进行的,各关键词权限大小为: ​private<fileprivate<internal<public<open

其中 ​internal​​为默认权限,可以在同一 ​module​下访问。

NSNotification(通知)

OC代码:

  1. // add observer
  2. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil];
  3. // post
  4. [NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil];

Swift代码:

  1. // add observer
  2. NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil)
  3. // post
  4. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self)

可以注意到,Swift中通知中心 ​NotificationCenter​​不带 ​NS​​前缀,通知名由字符串变成了 ​NSNotification.Name​的结构体。

改成结构体的目的就是为了便于管理字符串,原本的字符串类型变成了指定的 ​NSNotification.Name​类型。上面的Swift代码可以修改为:

  1. extension NSNotification.Name {
  2. static let NotificationName = NSNotification.Name("NotificationName")
  3. }
  4. // add observer
  5. NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil)
  6. // post
  7. NotificationCenter.default.post(name: .NotificationName, object: self)

protocol(协议/代理)

OC代码:

  1. @protocol XXManagerDelegate <NSObject>

  2. - (void)downloadFileFailed:(NSError *)error;

  3. @optional

  4. - (void)downloadFileComplete;

  5. @end

  6. @interface XXManager: NSObject

  7. @property (nonatomic, weak) id<XXManagerDelegate> delegate;

  8. @end

Swift中对 ​protocol​​的使用拓宽了许多,不光是 ​class​​对象, ​struct​​和 ​enum​​也都可以实现协议。需要注意的是 ​struct​​和 ​enum​​为指引用类型,不能使用 ​weak​​修饰。只有指定当前代理只支持类对象,才能使用 ​weak​。将上面的代码成对应的Swift代码,就是:

  1. @objc protocol XXManagerDelegate {
  2. func downloadFailFailed(error: Error)
  3. @objc optional func downloadFileComplete() // 可选协议的实现
  4. }
  5. class XXManager: NSObject {
  6. weak var delegate: XXManagerDelegate?
  7. }

@objc​​是表明当前代码是针对 ​NSObject​​对象,也就是 ​class​对象,就可以正常使用weak了。

如果不是针对NSObject对象的delegate,仅仅是普通的class对象可以这样设置代理:

  1. protocol XXManagerDelegate: class {
  2. func downloadFailFailed(error: Error)
  3. }
  4. class XXManager {
  5. weak var delegate: XXManagerDelegate?
  6. }

值得注意的是,仅 ​@objc​​标记的 ​protocol​​可以使用 ​@optional​。

Swift和OC混编注意事项

函数名的变化

如果你在一个Swift类里定义了一个delegate方法:

  1. @objc protocol MarkButtonDelegate {
  2. func clickBtn(title: String)
  3. }

如果你要在OC中实现这个协议,这时候方法名就变成了:

  1. - (void)clickBtnWithTitle:(NSString *)title {
  2. // code
  3. }

这主要是因为Swift有指定参数标签,OC却没有,所以在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数作为一个句子可以"通顺"。

在OC的头文件里调用Swift类

如果要在OC的头文件里引用Swift类,因为Swift没有头文件,而为了让在头文件能够识别该Swift类,需要通过 ​@class​的方法引入。

  1. @class SwiftClass;

  2. @interface XXOCClass: NSObject

  3. @property (nonatomic, strong) SwiftClass *object;

  4. @end

对OC类在Swift调用下重命名

因为Swift对不同的module都有命名空间,所以Swift类都不需要添加前缀。如果有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,所以苹果添加了一个宏命令 ​NS_SWIFT_NAME​,允许在OC类在Swift环境下的重命名:

  1. NS_SWIFT_NAME(LoginManager)
  2. @interface XXLoginManager: NSObject
  3. @end

这样我们就将 ​XXLoginManager​​在Swift环境下的类名改为了 ​LoginManager​。

引用类型和值类型

  • struct​​ 和 ​​enum​​ 是值类型,类 ​​class​​ 是引用类型。
  • String​​, ​​Array​​和 ​​Dictionary​​都是结构体,因此赋值直接是拷贝,而 ​​NSString​​, ​​NSArray​​ 和 ​​NSDictionary​​则是类,所以是使用引用的方式。
  • struct​​ 比 ​​class​​ 更“轻量级”, ​​struct​​ 分配在栈中, ​​class​​ 分配在堆中。

id类型和AnyObject

OC中 ​id​​类型被Swift调用时会自动成 ​AnyObject​​,他们很相似,但却其实概念并不一致。Swift中还有一个概念是 ​Any​,他们三者的区别是:

  • id​​ 是一种通用的对象类型,它可以指向属于任何类的对象,在OC中即是可以代表所有继承于 ​​NSObject​​的对象。
  • AnyObject​​可以代表任何 ​​class​​类型的实例。
  • Any​​可以代表任何类型,甚至包括 ​​func​​类型。

从范围大小比较就是: ​id<AnyObject<Any​。

其他语法区别及注意事项(待补充)

1、Swift语句中不需要加分号 ​;​。

2、关于Bool类型更加严格,Swift不再是OC中的非0就是真,真假只对应 ​true​​和 ​false​。

3、Swift类内一般不需要写 ​self​,但是闭包内是需要写的。

4、Swift是强类型语言,必须要指定明确的类型。在Swift中 ​Int​​和 ​Float​是不能直接做运算的,必须要将他们成同一类型才可以运算。

5、Swift抛弃了传统的 ​++​​, ​--​​运算,抛弃了传统的C语言式的 ​for​​循环写法,而改为 ​for-in​。

6、Swift的 ​switch​​操作,不需要在每个case语句结束的时候都添加break​。

7、Swift对 ​enum​​的使用做了很大的扩展,可以支持任意类型,而OC枚举仅支持 ​Int​类型,如果要写兼容代码,要选择Int型枚举。

8、Swift代码要想被OC调用,需要在属性和方法名前面加上 ​@objc​。

9、Swift独有的特性,如泛型, ​struct​​,非Int型的 ​enum​​等被包含才函数参数中,即使添加@objc​也不会被编译器通过。

10、Swift支持重载,OC不支持。

11、带默认值的Swift函数再被OC调用时会自动展开。

语法检查

对于OCSwift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。

可以通过 ​cocoapods​​进行引入,配置好之后,每次 ​Build​的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。

如果你感觉SwiftLint有点过于严格了,还可以通过修改 ​.swiftlint.yml​文件,自定义属于自己的语法规范。