運行環境:Xcode 11.1 Swift5.0html
最近參與的一個項目須要從Objective-C(如下簡稱OC)轉到Swift,期間遇到了一些坑,因而有了這篇總結性的文檔。
若是你也有將OC項目Swift化的需求,能夠做爲參考。git
OC轉Swift有一個大前提就是你要對Swift有必定的瞭解,熟悉Swift語法,最好是完整看過一遍官方的Language Guide。github
轉換的過程分自動化和手動轉譯,鑑於自動化工具的識別率不能讓人滿意,大部分狀況都是須要手動轉換的。面試
有一個比較好的自動化工具Swiftify,能夠將OC文件甚至OC工程整個轉成Swift,號稱準確率能達到90%。我試用了一些免費版中的功能,但感受效果並不理想,由於沒有使用過付費版,因此也很差評價它就是很差。swift
Swiftify還有一個Xcode的插件Swiftify for Xcode,能夠實現對選中代碼和單文件的轉化。這個插件還挺不錯,對純系統代碼轉化還算精確,但部分代碼還存在一些識別問題,須要手動再修改。xcode
若是你是在項目中首次使用Swift代碼,在添加Swift文件時,Xcode會提示你添加一個.h
的橋接文件。若是不當心點了不添加還能夠手動導入,就是本身手動生成一個.h
文件,而後在Build Settings > Swift Compiler - General > Objective-C Bridging Header
中填入該.h
文件的路徑。多線程
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>閉包
這個橋接文件的做用就是供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
文件中的引用下文有介紹。ide
Swift中沒有main.m
文件,取而代之的是@UIApplicationMain
命令,該命令等效於原有的執行main.m
。因此咱們能夠把main.m
文件進行移除。
對於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_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
下訪問。
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; @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類裏定義了一個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
。
struct
和 enum
是值類型,類 class
是引用類型。String
,Array
和 Dictionary
都是結構體,所以賦值直接是拷貝,而NSString
, NSArray
和NSDictionary
則是類,因此是使用引用的方式。struct
比 class
更「輕量級」,struct
分配在棧中,class
分配在堆中。OC中id
類型被Swift調用時會自動轉成AnyObject
,他們很類似,但卻其實概念並不一致。Swift中還有一個概念是Any
,他們三者的區別是:
id
是一種通用的對象類型,它能夠指向屬於任何類的對象,在OC中便是能夠表明全部繼承於NSObject
的對象。AnyObject
能夠表明任何class
類型的實例。Any
能夠表明任何類型,甚至包括func
類型。從範圍大小比較就是:id < AnyObject < Any
。
一、Swift語句中不須要加分號;
。
二、關於Bool類型更加嚴格,Swift再也不是OC中的非0就是真,真假只對應true
和false
。
三、Swift類內通常不須要寫self
,可是閉包內是須要寫的。
四、Swift是強類型語言,必需要指定明確的類型。在Swift中Int
和Float
是不能直接作運算的,必需要將他們轉成同一類型才能夠運算。
五、Swift拋棄了傳統的++
,--
運算,拋棄了傳統的C語言式的for
循環寫法,而改成for-in
。
六、Swift的switch
操做,不須要在每一個case語句結束的時候都添加break
。
七、Swift對enum
的使用作了很大的擴展,能夠支持任意類型,而OC枚舉僅支持Int
類型,若是要寫兼容代碼,要選擇Int型枚舉。
八、Swift代碼要想被OC調用,須要在屬性和方法名前面加上@objc
。
九、Swift獨有的特性,如泛型,struct
,非Int型的enum
等被包含才函數參數中,即便添加@objc
也不會被編譯器經過。
十、Swift支持重載,OC不支持。
十一、帶默認值的Swift函數再被OC調用時會自動展開。
對於OC轉Swift以後的語法變化還有不少細節值得注意,特別是對於初次使用Swift這門語言的同窗,很容易遺漏或者待着OC的思想去寫代碼。這裏推薦一個語法檢查的框架SwiftLint,能夠自動化的檢查咱們的代碼是否符合Swift規範。
能夠經過cocoapods
進行引入,配置好以後,每次Build
的過程,Lint腳本都會執行一遍Swift代碼的語法檢查操做,Lint還會將代碼規範進行分級,嚴重的代碼錯誤會直接報錯,致使程序沒法啓動,不太嚴重的會顯示代碼警告(⚠️)。
若是你感受SwiftLint有點過於嚴格了,還能夠經過修改.swiftlint.yml
文件,自定義屬於本身的語法規範。