运行环境:Xcode 11.1 Swift5.0html
最近参与的一个项目须要从Objective-C(如下简称OC)转到Swift,期间遇到了一些坑,因而有了这篇总结性的文档。
若是你也有将OC项目Swift化的需求,能够做为参考。git
OC转Swift有一个大前提就是你要对Swift有必定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide。github
转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分状况都是须要手动转换的。面试
有一个比较好的自动化工具Swiftify,能够将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感受效果并不理想,由于没有使用过付费版,因此也很差评价它就是很差。swift
Swiftify还有一个Xcode的插件Swiftify for Xcode,能够实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,须要手动再修改。xcode
若是你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h
的桥接文件。若是不当心点了不添加还能够手动导入,就是本身手动生成一个.h
文件,而后在Build Settings > Swift Compiler - General > Objective-C Bridging Header
中填入该.h
文件的路径。多线程
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>闭包
这个桥接文件的做用就是供Swift代码引用OC代码,或者OC的三方库。框架
#import "Utility.h" #import <Masonry/Masonry.h> 复制代码
在Bridging Header
的下面还有一个配置项是Objective-C Generated Interface Header Name
,对应的值是ProjectName-Swift.h
。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会相似pch
同样全局引用这个头文件。由于是Build过程当中生成的,因此只有.m
文件中能够直接引用,对于在.h
文件中的引用下文有介绍。ide
Swift中没有main.m
文件,取而代之的是@UIApplicationMain
命令,该命令等效于原有的执行main.m
。因此咱们能够把main.m
文件进行移除。
对于UIKit
框架中的大部分代码转换能够直接查看系统API文档进行转换,这里就不过多介绍。
Swift没有property
,也没有copy
,nonatomic
等属性修饰词,只有表示属性是否可变的let
和var
。
注意点一 OC中一个类分.h
和.m
两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将.m
中的property标为private
,即外接没法直接访问,对于.h
中的property不作处理,取默认的internal
,即同模块可访问。
对于函数的迁移也是相同的。
注意点二 有一种特殊状况是在OC项目中,某些属性在内部(.m
)可变,外部(.h
)只读。这种状况能够这么处理:
private(set) var value: String 复制代码
就是只对value
的set
方法就行private
标记。
注意点三 Swift中针对空类型有个专门的符号?
,对应OC中的nil
。OC中没有这个符号,可是能够经过在nullable
和nonnull
表示该种属性,方法参数或者返回值是否能够空。
若是OC中没有声明一个属性是否能够为空,那就去默认值nonnull
。
若是咱们想让一个类的全部属性,函数返回值都是nonnull
,除了手动一个个添加以外还有一个宏命令。
NS_ASSUME_NONNULL_BEGIN /* code */ NS_ASSUME_NONNULL_END 复制代码
这是个人iOS开发交流群:519832104无论你是小白仍是大牛欢迎入驻,能够一块儿分享经验,讨论技术,共同窗习成长!
另附上一份各好友收集的大厂面试题,须要iOS开发学习资料、面试真题,进群便可获取!点击此处,当即与iOS大牛交流学习
enum(枚举)
OC代码:
typedef NS_ENUM(NSInteger, PlayerState) { PlayerStateNone = 0, PlayerStatePlaying, PlayerStatePause, PlayerStateBuffer, PlayerStateFailed, }; typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) { XXViewAnimationOptionNone = 1 << 0, XXViewAnimationOptionSelcted1 = 1 << 1, XXViewAnimationOptionSelcted2 = 1 << 2, } 复制代码
Swift代码:
enum PlayerState: Int { case none = 0 case playing case pause case buffer case failed } struct ViewAnimationOptions: OptionSet { let rawValue: UInt static let None = ViewAnimationOptions(rawValue: 1<<0) static let Selected1 = ViewAnimationOptions(rawValue: 1<<0) static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2) //... } 复制代码
Swift没有NS_OPTIONS
的概念,取而代之的是为了知足OptionSet
协议的struct
类型。
OC代码:
- (MTObject *)object { if (!_object) { _object = [MTObject new]; } return _object; } 复制代码
Swift代码:
lazy var object: MTObject = { let object = MTObject() return imagobjecteView }() 复制代码
OC代码:
typedef void (^DownloadStateBlock)(BOOL isComplete); 复制代码
Swift代码:
typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void) 复制代码
OC代码:
+ (XXManager *)shareInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } 复制代码
Swift对单例的实现比较简单,有两种方式:
第一种
let shared = XXManager()// 声明在全局命名区(global namespace) Class XXManager { } 复制代码
你可能会疑惑,为何没有dispatch_once
,如何保证多线程下建立的惟一性?实际上是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,以后全部的调用都会使用该实例。并且全局变量的初始化是默认使用dispatch_once
的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了shard
的原子性。
第二种
Class XXManager { static let shared = XXManager() private override init() { // do something } } 复制代码
Swift 2 开始增长了static
关键字,用于限定变量的做用域。若是不使用static
,那么每个shared
都会对应一个实例。而使用static
以后,shared
成为全局变量,就成了跟上面第一种方式原理一致。能够注意到,因为构造器使用了 private
关键字,因此也保证了单例的原子性。
对于初始化方法OC先调用父类的初始化方法,而后初始本身的成员变量。Swift先初始化本身的成员变量,而后在调用父类的初始化方法。
OC代码:
// 初始化方法 @interface MainView : UIView @property (nonatomic, strong) NSString *title; - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER; @end @implementation MainView - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title { if (self = [super initWithFrame:frame]) { self.title = title; } return self; } @end // 析构函数 - (void)dealloc { //dealloc } 复制代码
上面类在调用时
Swift代码:
class MainViewSwift: UIView { let title: String init(frame: CGRect, title: String) { self.title = title super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { //deinit } } 复制代码
OC代码:
// 实例函数(共有方法) - (void)configModelWith:(XXModel *)model {} // 实例函数(私有方法) - (void)calculateProgress {} // 类函数 + (void)configModelWith:(XXModel *)model {} 复制代码
// 实例函数(共有方法) func configModel(with model: XXModel) {} // 实例函数(私有方法) private func calculateProgress() {} // 类函数(不能够被子类重写) static func configModel(with model: XXModel) {} // 类函数(能够被子类重写) class func configModel(with model: XXModel) {} // 类函数(不能够被子类重写) class final func configModel(with model: XXModel) {} 复制代码
OC能够经过是否将方法声明在.h
文件代表该方法是否为私有方法。Swift中没有了.h
文件,对于方法的权限控制是经过权限关键词进行的,各关键词权限大小为: private < fileprivate < internal < public < open
其中internal
为默认权限,能够在同一module
下访问。
OC代码:
// add observer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil]; // post [NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil]; 复制代码
Swift代码:
// add observer NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil) // post NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self) 复制代码
能够注意到,Swift中通知中心NotificationCenter
不带NS
前缀,通知名由字符串变成了NSNotification.Name
的结构体。
改为结构体的目的就是为了便于管理字符串,本来的字符串类型变成了指定的NSNotification.Name
类型。上面的Swift代码能够修改成:
extension NSNotification.Name { static let NotificationName = NSNotification.Name("NotificationName") } // add observer NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil) // post NotificationCenter.default.post(name: .NotificationName, object: self) 复制代码
OC代码:
@protocol XXManagerDelegate <NSObject> - (void)downloadFileFailed:(NSError *)error; @optional - (void)downloadFileComplete; @end @interface XXManager: NSObject @property (nonatomic, weak) id<XXManagerDelegate> delegate; @end 复制代码
Swift中对protocol
的使用拓宽了许多,不光是class
对象,struct
和enum
也均可以实现协议。须要注意的是struct
和enum
为指引用类型,不能使用weak
修饰。只有指定当前代理只支持类对象,才能使用weak
。将上面的代码转成对应的Swift代码,就是:
@objc protocol XXManagerDelegate { func downloadFailFailed(error: Error) @objc optional func downloadFileComplete() // 可选协议的实现 } class XXManager: NSObject { weak var delegate: XXManagerDelegate? } 复制代码
@objc
是代表当前代码是针对NSObject
对象,也就是class
对象,就能够正常使用weak了。
若是不是针对NSObject对象的delegate,仅仅是普通的class对象能够这样设置代理:
protocol XXManagerDelegate: class { func downloadFailFailed(error: Error) } class XXManager { weak var delegate: XXManagerDelegate? } 复制代码
值得注意的是,仅@objc
标记的protocol
可使用@optional
。
若是你在一个Swift类里定义了一个delegate方法:
@objc protocol MarkButtonDelegate { func clickBtn(title: String) } 复制代码
若是你要在OC中实现这个协议,这时候方法名就变成了:
- (void)clickBtnWithTitle:(NSString *)title { // code } 复制代码
这主要是由于Swift有指定参数标签,OC却没有,因此在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数做为一个句子能够"通顺"。
若是要在OC的头文件里引用Swift类,由于Swift没有头文件,而为了让在头文件可以识别该Swift类,须要经过@class
的方法引入。
@class SwiftClass; @interface XXOCClass: NSObject @property (nonatomic, strong) SwiftClass *object; @end 复制代码
由于Swift对不一样的module都有命名空间,因此Swift类都不须要添加前缀。若是有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,因此苹果添加了一个宏命令NS_SWIFT_NAME
,容许在OC类在Swift环境下的重命名:
NS_SWIFT_NAME(LoginManager) @interface XXLoginManager: NSObject @end 复制代码
这样咱们就将XXLoginManager
在Swift环境下的类名改成了LoginManager
。
struct
和 enum
是值类型,类 class
是引用类型。String
,Array
和 Dictionary
都是结构体,所以赋值直接是拷贝,而NSString
, NSArray
和NSDictionary
则是类,因此是使用引用的方式。struct
比 class
更“轻量级”,struct
分配在栈中,class
分配在堆中。OC中id
类型被Swift调用时会自动转成AnyObject
,他们很类似,但却其实概念并不一致。Swift中还有一个概念是Any
,他们三者的区别是:
id
是一种通用的对象类型,它能够指向属于任何类的对象,在OC中便是能够表明全部继承于NSObject
的对象。AnyObject
能够表明任何class
类型的实例。Any
能够表明任何类型,甚至包括func
类型。从范围大小比较就是:id < AnyObject < Any
。
一、Swift语句中不须要加分号;
。
二、关于Bool类型更加严格,Swift再也不是OC中的非0就是真,真假只对应true
和false
。
三、Swift类内通常不须要写self
,可是闭包内是须要写的。
四、Swift是强类型语言,必需要指定明确的类型。在Swift中Int
和Float
是不能直接作运算的,必需要将他们转成同一类型才能够运算。
五、Swift抛弃了传统的++
,--
运算,抛弃了传统的C语言式的for
循环写法,而改成for-in
。
六、Swift的switch
操做,不须要在每一个case语句结束的时候都添加break
。
七、Swift对enum
的使用作了很大的扩展,能够支持任意类型,而OC枚举仅支持Int
类型,若是要写兼容代码,要选择Int型枚举。
八、Swift代码要想被OC调用,须要在属性和方法名前面加上@objc
。
九、Swift独有的特性,如泛型,struct
,非Int型的enum
等被包含才函数参数中,即便添加@objc
也不会被编译器经过。
十、Swift支持重载,OC不支持。
十一、带默认值的Swift函数再被OC调用时会自动展开。
对于OC转Swift以后的语法变化还有不少细节值得注意,特别是对于初次使用Swift这门语言的同窗,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,能够自动化的检查咱们的代码是否符合Swift规范。
能够经过cocoapods
进行引入,配置好以后,每次Build
的过程,Lint脚本都会执行一遍Swift代码的语法检查操做,Lint还会将代码规范进行分级,严重的代码错误会直接报错,致使程序没法启动,不太严重的会显示代码警告(⚠️)。
若是你感受SwiftLint有点过于严格了,还能够经过修改.swiftlint.yml
文件,自定义属于本身的语法规范。