Swift从2014年发布到现在,马上接近三年,经过苹果的不断改进和完善,语法方面已经趋于稳定。如果是新建的项目,严重建议使用Swift,因为Swift必定会取代Objective-C。然后对于用Objective-C写的旧项目,我们有两个选择:1)直接整个项目用Swift重写;2)在旧项目的基础上,新的东西用Swift编写,然后再把之前用Objective-C写的代码慢慢改为Swift。我个人更偏向于在旧项目的基础上逐渐把整个项目转为Swift。下面我将会结合实际工作和苹果的官方文档《Using Swift with Cocoa and Objective-C (Swift 3.1)》来总结下如何将旧的Objective-C项目逐渐转为Swift项目。
学习Swift
你要懂得Swift(这TMD不是讲废话吗 ...)。英文能力不错的建议看官方的文档《The Swift Programming Language (Swift 3.1)》,官方的文档总是最好的。不嫌弃的话,可以看看我写的《Swift文集》,总结了Swift的关键知识点。大家可以看看Swift翻译组翻译的内容。
Objective-C和Swift的互用
在这部分内容里,我将会根据官方的文档,总结下Objective-C和Swift是如何互用的。
初始化
在Objective-C中,类的初始化方法通常是以init
或者initWith
开头的。在Swift中使用Objective-C的类时,Swift会把init
开头的方法作为初始化方法,如果是以initWith
开头的,在Swift中调用时,会把With
去掉,例如:
在Objective-C中:
- (instancetype)init;
在Swift中调用上面的接口,就会是下面这种形式:
init() { /... */ }init(frame: CGRect, style: UITableViewStyle) { /... */ }
类方法和便利初始化器
在Objective-C的类方法,在Swift中会被作为便利初始化器:
在Objective-C中:
UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];
在Swift中,就会是下面这种形式:
let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)
访问属性
Objective-C中的属性将会按照下面这个规则来导入Swift:
- 被
nonnull
,nullable
和null_resettable
标记的属性导入Swift会变成optional和nonoptional类型 - 被
readonly
标记的属性导入Swift变成计算属性 ({ get }
)。 - 被
weak
标记的属性导入Swift同样是被weak
标记 (weak var
)。 - 被
assign
,copy
,strong
或者unsafe_unretained
标记的,将会以适当的存储导入Swift。 - 被
class
标记的属性导入Swift变成类型属性。 - 原子性属性(
atomic
和nonatomic
)在对应的Swift属性中没有反应出来,但是在Swift中被访问的时候,Objective-C原子性的实现仍然会保留。 getter=
和setter=
在Swift中被省略。
在Swift中,直接用点语法来访问Objective-C的属性。
方法
同样地,在Swift中也是使用点语法来访问方法。
当Objective-C的方法被导入Swift后,Objective-C的Selector的第一部分会被作为Swift的方法名。例如:
在Objective-C中:
[myTableView insertSubview:mySubview atIndex:2];
导入Swift后:
myTableView.insertSubview(mySubview, at: 2)
id兼容性
Objective-C的id
类型,导入Swift成为Swift的Any
类型。
Swift还有一个类型AnyObject
,可以代表所有的class类型,它可以动态的搜索任何@objc
方法,而无需向下转型。例如:
var myObject: AnyObject = UITableViewCell()myObject = NSDate()let futureDate = myObject.addingTimeInterval(10)let timeSinceNow = myObject.timeIntervalSinceNow
但是我们在运行代码之前,AnyObject
的具体类型是不确定的,所以上面这种写法非常危险。,例如下面这个例子,在运行的时候会crash:
myObject.character(at: 5)// crash, myObject doesn't respond to that method
我们可以使用可选链或者if let
来解决这个问题:
// 可选链let myChar = myObject.character?(at: 5)
空属性和可选
我们都知道在Objective-C中,可以使用一些注释来标记属性、参数或者返回值是否可以为空,例如_nullable
、_Nonull
等等。他们会按照下面的规则来导入Swift:
- 被
_Nonnull
标记的,在导入Swift会被作为非可选类型 - 被
_Nullable
标记的,在导入Swift会被作为可选类型 - 没有被任何注释标记的,在导入Swift会被作为隐式解包可选类型
例如,在Objective-C中:
@property (nullable) id nullableProperty;@property (nonnull) id nonNullProperty;@property id unannotatedProperty;NS_ASSUME_NONNULL_BEGIN- (id)returnsNonNullValue;
导入Swift之后:
var nullableProperty: Any?var nonNullProperty: Anyvar unannotatedProperty: Any!func returnsNonNullValue() -> Anyfunc takesNonNullParameter(value: Any)func returnsNullableValue() -> Any?func takesNullableParameter(value: Any?)func returnsUnannotatedValue() -> Any!func takesUnannotatedParameter(value: Any!)
轻量级泛型
在Swift中:
@property NSArray<NSDate *> *dates;@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;
导入Swift之后:
var dates: [Date]var cachedData: NSCache<AnyObject, NSDiscardableContent>var supportedLocales: [String: [Locale]]
扩展
Swift的扩展其实类似于Objective-C的分类。Swift的扩展可以对现有的类、结构和枚举添加新的成员,即使是在Objective-C中定义的类、结构和枚举,都可以进行扩展。
例如下面这个例子,为UIBezierPath
添加一个便利初始化器,可用来画一个等边三角形:
extension UIBezierPath {
闭包
Objective-C的block,导入Swift之后变为Closure。例如在Objective-C中有一个block:
void (^completionBlock)(NSData *) = ^(NSData *data) { // ...}
在Swift中是这样的:
let completionBlock: (Data) -> Void = { data in
Objective-C的block和Swift的Closure基本上可以说是等价的,但是有一点不同的是:外部的变量在Swift的Closure中是可变的,我们可以直接在Closure内部更新变量的值;而在Objective-C中,需要用__block
标记变量。
解决Block中的循环引用问题
在Objective-C中:
__weak typeof(self) weakSelf = self;self.block = ^{
在Swift中是这样解决的,[unowned self]
被称为捕获列表(Capture List):
self.closure = { [unowned self] in
对象之间的比较
在Swift中,比较两个对象是否相等有两种方法:1) ==
:比较两个对象的内容是否相等;2) ===
:比较两个常量或者变量是否引用着同一个对象实例。
Swift为继承自NSObject
的子类提供了默认的==
和===
实现,并实现了Equatable
协议。默认的==
实现调用了isEqual:
方法,默认的===
实现检查指针是否相等。我们不能重写从Objective-C导入的类的这两个操作符。
Swift类型的兼容性
下面这些Swift特有的类型,是不兼容Objective-C的:
- 泛型
- 元组
- Swift中定义的没有
Int
类型原始值的枚举 - Swift中定义的结构
- Swift中定义的高阶函数
- Swift中定义的全局变量
- Swift中定义的类型别名
- Swift风格的variadics
- 嵌套类型
- Curried functions
Swift转换为Objective-C:
- 可选类型,被
__nullable
标记 - 非可选类型,被
__nonnull
标记 - 常量和计算属性,变成只读属性
- 类型属性在Objective-C中被
class
标记 - 类型方法在Objective-C是类方法
- 初始化器和实例方法变成Objective-C的实例方法
- 会抛出错误的方法,在Objective-C中会多了一个
NSerror **
参数。如果Swift的方法没有返回值,在Objective-C中会返回一个BOOL
。
例如,在Swift中:
class Jukebox: NSObject { var library: Set<String> var nowPlaying: String? var isCurrentlyPlaying: Bool { return nowPlaying != nil
转换成Objective-C后:
@interface Jukebox : NSObject@property (nonatomic, strong, nonnull) NSSet<NSString *> *library;@property (nonatomic, copy, nullable) NSString *nowPlaying;@property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying;@property (nonatomic, class, readonly, nonnull) NSArray<NSString *> favoritesPlaylist;
自定义Swift在Objective-C的接口
我们可以使用@objc(
name)
自定义Swift的类、属性、方法、枚举类型或者枚举case在Objective-C中使用时的名字。
例如,在Swift中:
@objc(Color)
Swift还提供了一个属性@nonobjc
,被这个属性标记的成员将不能在Objective-C中使用。
需要动态调度
当Swift的API被Objective-C runtime使用时,不能保证能动态调度属性、方法、下标或者初始化器。Swift的编译器仍然会反虚拟化或者内联成员访问来优化代码的属性,并绕过Objective-C runtime。
我们可以使用dynamic
在使用Objective-C runtime时动态的访问成员。需要动态调度的情况是非常少的。但是,在Objective-C runtime中使用key-value observing
或者method_exchangeImplementations
时,我们就需要动态调度,在运行的时候来动态地替换一个方法的实现。
注意:使用了dynamic
标记的声明,不能再使用@nonobjc
。因为使用了@nonobjc
,就意味着不能在Objective-C中使用,而dynamic
就是为了给Objective-C使用,这两个属性是完全冲突的。
Selector
在Objective-C中,我们使用@selector
来构造一个Selector;而在Swift中,我们要使用#selector
Key和Key Path
在Swift中,可以使用#keyPath
来生成编译器检查(也就是说编译的时候就能知道key和keyPath是否有误,而不必等到运行时才能确定)的key和keyPath,然后就可以给这些方法使用:value(forKey:)
、value(forKeyPath:)
、addObserver(_:forKeyPath:options:context:)
。#keyPath
支持链式方法或者属性,如#keyPath(Person.bestFriend.name)
。
例如:
class Person: NSObject { var name: String var friends: [Person] = [] var bestFriend: Person? = nil init(name: String) {
Cocoa Frameworks
Swift能自动地将一些类型在Swift和Objective-C之间互相转换。例如我们可以传一个String
值给NSString
参数。
Foundation
桥接类型
Swift Foundation提供了下列桥接值类型:
Objective-C引用类型 | Swift值类型 |
---|---|
NSAffineTransform | AffineTransform |
NSArray | Array |
NSCalendar | Calendar |
NSCharacterSet | CharacterSet |
NSData | Data |
NSDateComponents | DateComponents |
NSDateInterval | DateInterval |
NSDate | Date |
NSDecimalNumber | Decimal |
NSDictionary | Dictionary |
NSIndexPath | IndexPath |
NSIndexSet | IndexSet |
NSMeasurement | Measurement |
NSNotification | Notification |
NSNumber | Swift的数字类型(Int 和Float 等等) |
NSPersonNameComponents | PersonNameComponents |
NSSet | Set |
NSString | String |
NSTimeZone | TimeZone |
NSURL | URL |
NSURLComponents | URLComponents |
NSURLQueryItem | URLQueryItem |
NSURLRequest | URLRequest |
NSUUID | UUID |
我们可以看到,就是直接把Objective-C的前缀NS
去掉,就是Swift的值类型(但是有些情况例外)。这些Swift的值类型拥有Objective-C引用类型的所有方法。任何使用Objective-C引用类型的地方,都可以使用对应的Swift值类型。
统一的Logging
统一的logging系统提供了一些平台通用的API来打印一些信息,但是这个API只在 iOS 10.0, macOS 10.12, tvOS 10.0和watchOS 3.0以后的版本才可用。
下面是使用的例子:
import os.log
Cocoa的结构
当Swift的结构被桥接成Objective-C时,下面这些结构会变成NSValue
。
- CATransform3D
- CLLocationCoordinate2D
- CGAffineTransform
- CGPoint
- CGRect
- CGSize
- CGVector
- CMTimeMapping
- CMTimeRange
- CMTime
- MKCoordinateSpan
- NSRange
- SCNMatrix4
- SCNVector3
- SCNVector4
- UIEdgeInsets
- UIOffset
Cocoa设计模式
代理
代理设计模式,是我们经常用到的。在Objective-C中,在调用代理方法之前,我们首先要检查代理是否有实现这个代理方法。而在Swift中,我们可以使用可选链来调用代理方法。例如:
class MyDelegate: NSObject, NSWindowDelegate { func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize { return proposedSize
Lazy初始化
一个lazy属性只会在第一次被访问的时候才会初始化,相当于在Objective-C的懒加载(重写getter方法)。当需要进行比较复杂或者耗时的计算才能初始化一个属性时,我们应该尽量使用lazy属性。
在Objective-C中:
@property NSXMLDocument *XML;
而在Swift,我们使用lazy属性:
lazy var XML: XMLDocument = try! XMLDocument(contentsOf: Bundle.main.url(forResource: "document", withExtension: "xml")!, options: 0)
对于其他需要更复杂的初始化的属性,可以写成:
lazy var currencyFormatter: NumberFormatter = {
单例
单例模式使我们在开发中经常用到的。
在Objective-C中,我们通常用GCD来实现:
+ (instancetype)sharedInstance { static id _sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
在Swift中,直接使用static类型属性即可,可以保证只初始化一次,即使时在多线程中被同时访问。
class Singleton { static let sharedInstance = Singleton()
如果我们需要其他设置,可以写成:
class Singleton { static let sharedInstance: Singleton = {
API可用性
有些类和方法并不是在所有平台或者版本都可用的,所有有时我们需要进行API可用性检查。
例如,CLLocationManager
的requestWhenInUseAuthorization
方法只能在iOS 8.0和macOS 10.10以后的版本才能使用:
let locationManager = CLLocationManager()if #available(iOS 8.0, macOS 10.10, *) {
*
是为了处理未来的平台。
平台名称:
- iOS
- iOSApplicationExtension
- macOS
- macOSApplicationExtension
- watchOS
- watchOSApplicationExtension
- tvOS
- tvOSApplicationExtension
同样地,我们在写自己的API时,也可以指定那些平台可以使用:
@available(iOS 8.0, macOS 10.10, *)func useShinyNewFeature() { // ...}
Swift和Objective-C混编
把Objective-C代码导入Swift
为了把Objective-C代码导入Swift中,我们需要用到Objective-C bridging header。当你把Objective-C文件拖入Swift项目中时,Xcode会提示你是否新建一个bridging header,如下图:
Create Bridging Header
点击Create Bridging Header,项目的文件路径下就会创建一个名为项目名称-Bridging-Header.h
的文件(如果项目名称不是英文,将会以_
代替;如果第一个是字母,也会以_
代替)。
我们也可以手动创建:File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Header File。
当-Bridging-Header.h
文件创建好我们还需要进行以下操作:
- 把Swift中要用到的Objective-C类的头文件,以下面这种形式添加到
Bridging-Header.h
文件
#import "XYZCustomCell.h"#import "XYZCustomView.h"#import "XYZCustomViewController.h"
- 在Build Settings > Swift Compiler - General > Objective-C Bridging Header添加
-Bridging-Header.h
的路径,路径的格式:项目名/项目名称-Bridging-Header.h
如图
Objective-C Bridging Header
这样我们就配置完成了,可以在Swift中调用Objective-C的代码:
let myCell = XYZCustomCell()
Swift代码导入Objective-C
当需要在Objective-C中使用Swift的代码时,我们依赖于Xcode自动生成的头文件,这个头文件的名称是项目名-Swift.h
(如果项目名称不是英文,将会以_
代替;如果第一个是字母,也会以_
代替)。
默认情况下,这个自动生成的头文件包含了在Swift中被public
或者open
标记的声明,如果这个项目中有Objective-C bridging header,internal
标记的声明也包含在内。被private
和fileprivate
标记的不包含在内。私有的声明不会暴露给Objective-C,除非他们被@IBAction
、@IBOutlet
或者@objc
标记。
当需要在Objective-C中使用Swift的代码时,直接导入头文件项目名-Swift.h
,然后我们就可以在Objective-C中调用Swift的接口,用法与Objective-C的语法相同:
// 初始化实例,并调用方法MySwiftClass *swiftObject = [[MySwiftClass alloc] init];
注意:如果是刚刚写的Swift代码,马上就想在Objective-C调用,我们需要先编译一下,然后Objective-C中才能访问到Swift的接口。
声明可以被Objective-C使用的Swift协议
为了声明一个可以被Objective-C使用的Swift协议,我们要用@objc
标记,如果协议的方法是optional
,也需要用@objc
。
@objc public protocol MySwiftProtocol { func requiredMethod()
把Objective-C代码转为Swift
前面讲了一大堆基础知识,就是为了更好地将Objective-C代码转为Swift。
迁移过程
- 创建一个对应Objective-C
.m
和.h
的Swift类,创建方法:File > New > File > (iOS, watchOS, tvOS, or macOS) > Source > Swift File。类的名称可以相同,也可以不同。 - 导入相关的系统框架
- 如果要需要用到Objective-C的代码,需要在bridging header中导入相关的头文件
- 为了让这个Swift类可以在Objective-C中使用,需要让这个类继承自Objective-C的类。如果要自定义在Objective-C中调用的Swift接口的名称,使用
@objc(
name)
。 - 我们可以通过继承Objective-C的类,实现Objective-C协议等来集成Objective-C已有的成员。
- 在迁移过程中,我们要知道:1)Objective-C的语言特性转换成Swift后,是变成怎样;2)Cocoa框架中Objective-C的类型,在Swift中是什么类型;3)常用的设计模式;4)Objective-C的属性如何迁移到Swift。这些大部分内容我上面都有提到。
- Objective-C的(
-
)和(+
)方法,对应到Swift就是func
和class func
。 - Objective-C的简单的宏定义改为全局常量,复杂的宏定义改为方法
- 迁移完成后,在有导入Objective-C类的地方,用
#import "项目名称-Swift.h"
替换。 - 把之前的
.m
文件的target membership这个勾去掉。先别着急把之前的.m
和.h
文件删掉,因为我们刚刚写完的Swift类可能不太完善,我们还需要用之前的文件来解决问题。
target membership
- 如果Swift的类名和之前的Objective-C的类名不一样,在用到Objective-C的类的地方,更新为新的类名。