【iOS開發】如何將舊的Objective-C項目逐漸轉為Swift項目

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, nullablenull_resettable標記的屬性導入Swift之后,會變成optional和nonoptional類型
  • readonly標記的屬性導入Swift之后,變成計算屬性 ({ get })。
  • weak標記的屬性導入Swift之后,同樣是被weak標記 (weak var)。
  • assign, copy, strong或者unsafe_unretained標記的,將會以適當的存儲導入Swift。
  • class標記的屬性導入Swift之后,變成類型屬性。
  • 原子性屬性(atomicnonatomic)在對應的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的數字類型(IntFloat等等)
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可用性檢查。

例如,CLLocationManagerrequestWhenInUseAuthorization方法只能在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

點擊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()
myCell.subtitle = "A custom cell"
Swift代碼導入Objective-C

當需要在Objective-C中使用Swift的代碼時,我們依賴于Xcode自動生成的頭文件,這個頭文件的名稱是項目名-Swift.h(如果項目名稱不是英文,將會以_代替;如果第一個是字母,也會以_代替)。

默認情況下,這個自動生成的頭文件包含了在Swift中被public或者open標記的聲明,如果這個項目中有Objective-C bridging header,那么,internal標記的聲明也包含在內。被privatefileprivate標記的不包含在內。私有的聲明不會暴露給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就是funcclass func
  • Objective-C的簡單的宏定義改為全局常量,復雜的宏定義改為方法
  • 遷移完成后,在有導入Objective-C類的地方,用#import "項目名稱-Swift.h"替換。
  • 把之前的.m文件的target membership這個勾去掉。先別著急把之前的.m.h文件刪掉,因為我們剛剛寫完的Swift類可能不太完善,我們還需要用之前的文件來解決問題。
target membership
  • 如果Swift的類名和之前的Objective-C的類名不一樣,在用到Objective-C的類的地方,更新為新的類名。

有任何問題,歡迎大家留言!

歡迎加入我管理的Swift開發群:536353151,本群只討論Swift相關內容。

原創文章,轉載請注明出處。謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容