運行環境:Xcode 11.1 Swift5.0 最近參與的一個項目需要從Objective-C(以下簡稱OC)轉到Swift,期間遇到了一些坑,於是有了這篇總結性的文檔。 如果你也有將OC項目Swift化的需求,可以作為參考。 OC轉Swift有一個大前提就是你要對Swift有一定的瞭解,熟悉Sw ...
運行環境:Xcode 11.1 Swift5.0
最近參與的一個項目需要從Objective-C(以下簡稱OC)轉到Swift,期間遇到了一些坑,於是有了這篇總結性的文檔。
如果你也有將OC項目Swift化的需求,可以作為參考。
OC轉Swift有一個大前提就是你要對Swift有一定的瞭解,熟悉Swift語法,最好是完整看過一遍官方的Language Guide。
轉換的過程分自動化和手動轉譯,鑒於自動化工具的識別率不能讓人滿意,大部分情況都是需要手動轉換的。
自動化工具
有一個比較好的自動化工具Swiftify,可以將OC文件甚至OC工程整個轉成Swift,號稱準確率能達到90%。我試用了一些免費版中的功能,但感覺效果並不理想,因為沒有使用過付費版,所以也不好評價它就是不好。
Swiftify還有一個Xcode的插件Swiftify for Xcode,可以實現對選中代碼和單文件的轉化。這個插件還挺不錯,對純系統代碼轉化還算精確,但部分代碼還存在一些識別問題,需要手動再修改。
手動Swift化
橋接文件
如果你是在項目中首次使用Swift代碼,在添加Swift文件時,Xcode會提示你添加一個.h
的橋接文件。如果不小心點了不添加還可以手動導入,就是自己手動生成一個.h
文件,然後在Build Settings > Swift Compiler - General > Objective-C Bridging Header
中填入該.h
文件的路徑。
這個橋接文件的作用就是供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
文件中的引用下文有介紹。
Appdelegate(程式入口)
Swift中沒有main.m
文件,取而代之的是@UIApplicationMain
命令,該命令等效於原有的執行main.m
。所以我們可以把main.m
文件進行移除。
系統API
對於UIKit
框架中的大部分代碼轉換可以直接查看系統API文檔進行轉換,這裡就不過多介紹。
property(屬性)
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
下訪問。
NSNotification(通知)
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)
複製代碼
protocol(協議/代理)
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和OC混編註意事項
函數名的變化
如果你在一個Swift類里定義了一個delegate方法:
@objc protocol MarkButtonDelegate {
func clickBtn(title: String)
}
複製代碼
如果你要在OC中實現這個協議,這時候方法名就變成了:
- (void)clickBtnWithTitle:(NSString *)title {
// code
}
複製代碼
這主要是因為Swift有指定參數標簽,OC卻沒有,所以在由Swift方法名生成OC方法名時編譯器會自動加一些修飾詞,已使函數作為一個句子可以"通順"。
在OC的頭文件里調用Swift類
如果要在OC的頭文件里引用Swift類,因為Swift沒有頭文件,而為了讓在頭文件能夠識別該Swift類,需要通過@class
的方法引入。
@class SwiftClass;
@interface XXOCClass: NSObject
@property (nonatomic, strong) SwiftClass *object;
@end
複製代碼
對OC類在Swift調用下重命名
因為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
分配在堆中。
id類型和AnyObject
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
文件,自定義屬於自己的語法規範。