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;
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;
在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)
// if let
if let fifthCharacter = myObject.character?(at: 5) {
print("Found \(fifthCharacter) at index 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;
- (void)takesNonNullParameter:(id)value;
NS_ASSUME_NONNULL_END
- (nullable id)returnsNullableValue;
- (void)takesNullableParameter:(nullable id)value;
- (id)returnsUnannotatedValue;
- (void)takesUnannotatedParameter:(id)value;
導入Swift之后:
var nullableProperty: Any?
var nonNullProperty: Any
var unannotatedProperty: Any!
func returnsNonNullValue() -> Any
func 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 {
convenience init(triangleSideLength: CGFloat, origin: CGPoint) {
self.init()
let squareRoot = CGFloat(sqrt(3.0))
let altitude = (squareRoot * triangleSideLength) / 2
move(to: origin)
addLine(to: CGPoint(x: origin.x + triangleSideLength, y: origin.y))
addLine(to: CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude))
close()
}
}
閉包
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 = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
};
在Swift中是這樣解決的,[unowned self]
被稱為捕獲列表(Capture List):
self.closure = { [unowned self] in
self.doSomething()
}
對象之間的比較
在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
}
class var favoritesPlaylist: [String] {
// return an array of song names
}
init(songs: String...) {
self.library = Set<String>(songs)
}
func playSong(named name: String) throws {
// play song or throw an error if unavailable
}
}
轉換成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;
- (nonnull instancetype)initWithSongs:(NSArray<NSString *> * __nonnull)songs OBJC_DESIGNATED_INITIALIZER;
- (BOOL)playSong:(NSString * __nonnull)name error:(NSError * __nullable * __null_unspecified)error;
@end
自定義Swift在Objective-C的接口
我們可以使用@objc(
name)
自定義Swift的類、屬性、方法、枚舉類型或者枚舉case在Objective-C中使用時的名字。
例如,在Swift中:
@objc(Color)
enum Цвет: Int {
@objc(Red)
case Красный
@objc(Black)
case Черный
}
@objc(Squirrel)
class Белка: NSObject {
@objc(color)
var цвет: Цвет = .Красный
@objc(initWithName:)
init (имя: String) {
// ...
}
@objc(hideNuts:inTree:)
func прячьОрехи(количество: Int, вДереве дерево: Дерево) {
// ...
}
}
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) {
self.name = name
}
}
let gabrielle = Person(name: "Gabrielle")
let jim = Person(name: "Jim")
let yuanyuan = Person(name: "Yuanyuan")
gabrielle.friends = [jim, yuanyuan]
gabrielle.bestFriend = yuanyuan
#keyPath(Person.name)
// "name"
gabrielle.value(forKey: #keyPath(Person.name))
// "Gabrielle"
#keyPath(Person.bestFriend.name)
// "bestFriend.name"
gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name))
// "Yuanyuan"
#keyPath(Person.friends.name)
// "friends.name"
gabrielle.value(forKeyPath: #keyPath(Person.friends.name))
// ["Yuanyuan", "Jim"]
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
// 直接打印一個String
os_log("This is a log message.")
// 拼接一個或多個參數
let fileSize = 1234567890
os_log("Finished downloading file. Size: %{iec-bytes}d", fileSize)
// 定義打印等級,例如info、debug、error、fault
os_log("This is additional info that may be helpful for troubleshooting.", type: .info)
// 打印信息到特定的子系統
let customLog = OSLog("com.your_company.your_subsystem_name.plist", "your_category_name")
os_log("This is info that may be helpful during development or debugging.", log: customLog, type: .debug)
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
}
}
myWindow.delegate = MyDelegate()
if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {
print(NSStringFromSize(fullScreenSize))
}
Lazy初始化
一個lazy屬性只會在第一次被訪問的時候才會初始化,相當于在Objective-C的懶加載(重寫getter方法)。當需要進行比較復雜或者耗時的計算才能初始化一個屬性時,我們應該盡量使用lazy屬性。
在Objective-C中:
@property NSXMLDocument *XML;
- (NSXMLDocument *)XML {
if (_XML == nil) {
_XML = [[NSXMLDocument alloc] initWithContentsOfURL:[[Bundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil];
}
return _XML;
}
而在Swift,我們使用lazy屬性:
lazy var XML: XMLDocument = try! XMLDocument(contentsOf: Bundle.main.url(forResource: "document", withExtension: "xml")!, options: 0)
對于其他需要更復雜的初始化的屬性,可以寫成:
lazy var currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "¤"
return formatter
}()
單例
單例模式使我們在開發中經常用到的。
在Objective-C中,我們通常用GCD來實現:
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
在Swift中,直接使用static類型屬性即可,可以保證只初始化一次,即使時在多線程中被同時訪問。
class Singleton {
static let sharedInstance = Singleton()
}
如果我們需要其他設置,可以寫成:
class Singleton {
static let sharedInstance: Singleton = {
let instance = Singleton()
// setup code
return instance
}()
}
API可用性
有些類和方法并不是在所有平臺或者版本都可用的,所有有時我們需要進行API可用性檢查。
例如,CLLocationManager
的requestWhenInUseAuthorization
方法只能在iOS 8.0和macOS 10.10以后的版本才能使用:
let locationManager = CLLocationManager()
if #available(iOS 8.0, macOS 10.10, *) {
locationManager.requestWhenInUseAuthorization()
}
*
是為了處理未來的平臺。
平臺名稱:
- 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,項目的文件路徑下就會創建一個名為項目名稱-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
如圖
這樣我們就配置完成了,可以在Swift中調用Objective-C的代碼:
let myCell = XYZCustomCell()
myCell.subtitle = "A custom cell"
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];
[swiftObject swiftMethod];
// 引用類或者協議
// MyObjcClass.h
@class MySwiftClass;
@protocol MySwiftProtocol;
@interface MyObjcClass : NSObject
- (MySwiftClass *)returnSwiftClassInstance;
- (id <MySwiftProtocol>)returnInstanceAdoptingSwiftProtocol;
// ...
@end
// 實現Swift協議
// MyObjcClass.m
#import "ProductModuleName-Swift.h"
@interface MyObjcClass () <MySwiftProtocol>
// ...
@end
@implementation MyObjcClass
// ...
@end
注意:如果是剛剛寫的Swift代碼,馬上就想在Objective-C調用,我們需要先編譯一下,然后Objective-C中才能訪問到Swift的接口。
聲明可以被Objective-C使用的Swift協議
為了聲明一個可以被Objective-C使用的Swift協議,我們要用@objc
標記,如果協議的方法是optional
,也需要用@objc
。
@objc public protocol MySwiftProtocol {
func requiredMethod()
@objc optional func optionalMethod()
}
把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類可能不太完善,我們還需要用之前的文件來解決問題。
- 如果Swift的類名和之前的Objective-C的類名不一樣,在用到Objective-C的類的地方,更新為新的類名。
完
有任何問題,歡迎大家留言!
歡迎加入我管理的Swift開發群:536353151
,本群只討論Swift相關內容。
原創文章,轉載請注明出處。謝謝!