运行环境:Xcode 11.1 Swift5.0
最近项目需要从Objective-C(以下简称OC)转到Swift,期间遇到一些问题,记录下来方便以后查阅。
OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide。
转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。
有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。
Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。
如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个.h文件,然后在Build Settings > Swift Compiler - General > Objective-C Bridging Header中填入该.h文件的路径。
这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。
#import <Masonry/Masonry.h>在Bridging Header的下面还有一个配置项是Objective-C Generated Interface Header Name,对应的值是ProjectName-Swift.h。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会类似pch一样全局引用这个头文件。因为是Build过程中生成的,所以只有.m文件中可以直接引用,对于在.h文件中的引用下文有介绍。
在 C 系语言中,程序的入口都是 main 函数。对于一个 Objective-C 的 iOS app 项目,在新建项目时, Xcode 将帮我们准备好一个 main.m 文件,其中就有这个 main 函数
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }这个方法将根据第三个参数初始化一个 UIApplication 或其子类的对象并开始接收事件 (在这个例子中传入 nil,意味使用默认的 UIApplication)。最后一个参数指定了 AppDelegate 类作为应用的委托,它被用来接收类似 didFinishLaunching 或者 didEnterBackground 这样的与应用生命周期相关的委托方法。另外,虽然这个方法标明为返回一个 int,但是其实它并不会真正返回。它会一直存在于内存中,直到用户或者系统将其强制终止
新建一个 Swift 的 iOS app 项目后,我们会发现所有文件中都没有一个像 Objective-C 时那样的 main文件,也不存在 main 函数。唯一和main有关系的是在默认的 AppDelegate 类的声明上方有一个 @UIApplicationMain的标签。
其实 Swift 的 app 也是需要 main 函数的,只不过默认情况下是 @UIApplicationMain帮助我们自动生成了而已。
如我们在删除 @UIApplicationMain 后,在项目中添加一个 main.swift文件,然后加上这样的代码
UIApplicationMain(Process.argc, Process.unsafeArgv, nil, NSStringFromClass(AppDelegate))现在编译运行,就不会再出现错误了。当然,我们还可以通过将第三个参数替换成自己的 UIApplication 子类,这样我们就可以轻易地做一些控制整个应用行为的事情了。比如将 main.swift 的内容换成
UIApplicationMain( CommandLine.argc, UnsafeMutableRawPointer(CommandLine.unsafeArgv) .bindMemory( to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)), NSStringFromClass(MyApplication.self), NSStringFromClass(AppDelegate.self) ) import UIKit class MyApplication: UIApplication { override func sendEvent(_ event: UIEvent) { super.sendEvent(event) print("Event sent:\(event)") } } let cls = MyClass() cls.mustProtocolMethod() cls.mustProtocolMethod1()这样每次发送事件 (比如点击按钮) 时,我们都可以监听到这个事件了
对于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_ENDOC代码:
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代码:
- (NSObject *)object { if (!_object) { _object = [NSObject new]; } return _object; }Swift代码:
lazy var object: NSObject = { let object = NSObject() 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 {}Swift代码:
// 实例函数(共有方法) 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; @endSwift中对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对象可以这样设置代理:
值得注意的是,仅@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。
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调用时会自动展开。
对于OC转Swift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。
可以通过cocoapods进行引入,配置好之后,每次Build的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。
如果你感觉SwiftLint有点过于严格了,还可以通过修改.swiftlint.yml文件,自定义属于自己的语法规范。