Пора валить! Опыт миграции с Objective-C на Swift

, iOS Superjob, c Objective-C Swift.



RIT2017.







Superjob 3 iOS:











3 , — 250 .



2012 . Objective-C. 5 Swift, . ?



  1. . 20 , ( ). , . -, . Objective-C , 100500 — . Swift .



  2. objc-. UI- Swift. . : Reactive Cocoa. , .



  3. : , . 70% Superjob Swift.




Nullability



— 5%. . Swift , Objective-C , run-time . Swift, .





:





: Nullability 60%. , .



: 3 , Swift . Swift.



objc-



CocoaPods . , . :





ReactiveCocoa.





決定基準を定義しました:



•使いやすくするため。

•厳密なタイピングがあったこと。

•SwiftのようなAPI。



その結果、RxSwiftを選択しました。



RxSwfitを使用してReactiveCocoaを友達にした方法



ソリューションとして、型なしのReactiveCocoa信号を型付きのObservable RxSwfitに変換するRACSignalのカテゴリを作成しました。 最初のステップ:Observableを作成し、新しいRACSignal値をサブスクライブします。 RACSignalで新しいデータを受信するとき、convertBlockを使用してジェネリックで指定したタイプにキャストしようとします(これについては後で説明します)。 判明した場合は、新しい入力値をObservableサブスクライバーにさらに転送します。 そうでない場合は、目的のタイプへの変換エラーを報告します。



 extension RACSignal { private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> { return Observable.create() { observer in self.subscribeNext( { anyValue in if let converted = convertBlock(anyValue) { observer.onNext(converted) } else { observer.onError(RxCastError.cannotConvertTypes) } }, ... ... ... return Disposables.create() { } } }
      
      





さらに、内部でObservable関数を呼び出し、RACSignalからクロージャー値を渡すパブリックメソッドが既にあります。これは、ジェネリックで指定された必要な型にキャストされます。

 extension RACSignal { public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> { return rxMapBody() { anyValue in if let value: T = rx_cast(anyValue) { return value } else { return nil } } } }
      
      





このようなソリューションは標準タイプとコレクションに適しています。たとえば、NSArrayはswift配列に簡単に適合し、NSNumberはswiftに簡単に適合しますが、ReactiveCocoaはRACTupleなどのデータ構造を持っています。 カルタゴに保存するだけではうまくいかないため、問題がありました。そのため、RACTupleの各値をアンパックしてカルタゴを収集するRACTuple専用のメソッドを別途作成する必要がありました。



 public func rxMapTuple<Q, W>(_ type: (Q, W).Type = (Q, W).self) -> Observable<(Q, W)> { return rxMapBody() { anyValue in if let convertible = anyValue as? RACTuple, let value: (Q, W) = convertible.rx_convertTuple() { return value } else { return nil } } }
      
      





また、カーネル自体と同様に、型なしの値を型付きの値にキャストする関数が作成されています。



 internal func rx_cast<T>(_ value: Any?) -> T? { if let v = value as? T { return v } else if let E = T.self as? ExpressibleByNilLiteral.Type { return E.init(nilLiteral: ()) as? T } return nil }
      
      





RACSignalから取得した値、および変換する必要のある必要な型を関数の入力に渡します。 すぐに値を型にキャストすることが判明した場合、値自体が返されます。そうでない場合、2番目のステップは、キャストしようとしている型がオプションかどうかをチェックすることです。 その場合は、このオプションの型の変数を空の値で作成します。 オプションの変数を作成せず、単にnilを返す場合、コンパイラは必要な型Tにnilをキャストできないと言うため、最後の操作が必要です。



これで、RACSignalのrxMap関数を呼び出し、必要なタイプを渡すことができます。これは、サブスクライブブロックで期待されます。これからは、常にonNextでユーザーモデルを取得します。



 profileFacade .authorize(withLogin: login, password: pass) .rxMap(SJAProfileModel.self) .subscribe(onNext: { (user) in }) .addDisposableTo(disposeBag)
      
      





さらに便利にし、ファサード自体に拡張機能を書く必要があります



 extension SJAProfileFacade { func authorize(login: String, passwrod: String) -> Observable<SJAProfileModel> { return self.authorize(withLogin: login, password: passwrod).rxMap() } }
      
      





Observableを返すことをすぐに示しますが、内部では単純にrxMap()を呼び出します。この場合、キャストする必要のある型を指定する必要はありません。 型自体は戻り値から取得されます。

結果として、毎回型をキャストする必要がなくなり、1回だけキャストします。



 profileFacade .authorize(login: login, password: pass) .subscribe(onNext: { (user) in }) .addDisposableTo(disposeBag)
      
      





客観的ではありません



既存のアプリケーションの大量のコードをすぐに置き換えることはできません。 これは問題につながります。Objective-CですべてのSwift機能が利用できるわけではありません。



使用する必要があるものから正確に入手できないもの:





ソリューションはSourceryです。



このソリューションは、コードを自動生成できます。



これを例を使用して理解する方が簡単です。Hashable、Equatableプロトコルを満たす必要があるResume構造があります。 ただし、自分で実装する場合は、新しいプロパティを考慮することを忘れないでください。 これをすべて信頼してSourceryを実行できます。 これを行うには、Resume構造体がAutoHashableとAutoEquatableの2つのプロトコルを満たしていることを示します。



 struct Resume: AutoHashable, AutoEquatable { var key: Int? let name: String? var firstName: String? var lastName: String? var middleName: String? var birthDate: Date? }
      
      





これらのプロトコル自体は種類がありません。



 protocol AuthoHashable {} protocol AutoEqutable { }
      
      





Sourceryが特定の構造に使用するテンプレートを理解するために必要です。



これでSourceryを実行できます。 ResumeのHashableおよびEquatableプロトコルの実装が自動的に生成されるファイルを取得します。 ビルド段階でソースを埋め込む場合、履歴書に新しいプロパティを追加することで、それらを考慮することを忘れる心配はありません。



 extension Resume: Hashable { internal var hashValue: Int { return combineHashes([key?.hashValue ?? 0, name?.hashValue ?? 0, firstName?.hashValue ?? 0, lastName?.hashValue ?? 0, middleName?.hashValue ?? 0, birthDate?.hashValue ?? 0, 0]) } } extension Resume: Equatable {} internal func == (lhs: Resume, rhs: Resume) -> Bool { guard compareOptionals(lhs: lhs.key, rhs: rhs.key, compare: ==) else { return false } guard compareOptionals(lhs: lhs.name, rhs: rhs.name, compare: ==) else { return false } guard compareOptionals(lhs: lhs.firstName, rhs: rhs.firstName, compare: ==) else { return false } ... ... return true }
      
      





HashbaleとEqutableの自動生成テンプレートはすぐに使用できますが、ニーズに合わせてテンプレートを独自に作成できるため、これに制限はありません。 たとえば、このような列挙型があります。



 enum Conf { case Apps case Backend case WebScale case Awesome }
      
      





enum内のenumの数に何らかのロジックを構築したいため、テンプレートを作成してSourceryに渡すことができます。



 {% for enum in types.enums %} extension {{ enum.name }} { static var numberOfCases:Int = {{ enum.cases.count}} } {% endfor %}
      
      





このテンプレートでは、見つかったすべてのタイプをスキャンします。 列挙型の場合は、その拡張機能を作成します。この拡張機能では、量を持つ静的変数を宣言します。



 extension Conf { static var numberOfCases:Int = 4 }
      
      





そのため、この機会に構造体をObjective-cに移植する独自のテンプレートを作成しました。 Swiftでまだ書き換えられていない場所で履歴書を操作できるように、このようなトリックを使用する必要がありました。 その結果、構造からResumeObjcクラスを自動的に生成します。これは、古いObjective-cコードで使用できます。







テスト用のモックの例について



Objective-cでテストを記述するとき、モックのスウィズリングをよく使用しました。 しかし、Swiftではこれは不可能なので、ある種の「FakeProtocolClass」を作成し、必要なすべてのメソッドを実装し、メソッドが呼び出されたかどうかを示す特別な変数を追加する必要がありました。 Sourceryは、このようなmokaを自動的に生成します。







アップグレードチーム



過去6か月間、Superjobのインタビューの4人の候補者のうち3人は、特にSwiftで仕事をしたいという願望を語っていました。



Swiftに切り替えるときは、コードスタイルやリソースの操作など、チームの組織的な問題を考慮することが重要でした。 チームは長年、Objective-Cに取り組んできました。Swiftについては、誰もが独自のビジョンを持っていました。 そのため、チームを正しい方向に導くのに役立つツールが必要でした。 Swiftの最も有名なコードスタイルツールの1つはSwiftLintです。



SwiftLintでは、独自のルールを入力できます。 これにより、チーム固有のミスを登録し、すぐにそれらを取り除くことができました。 たとえば、SwfitでReactiveCocoaの使用を禁止するルールを作成しました。







また、プロジェクトが何度か再設計されても生き残ったため、作業をグラフィックスと統合したいと考えました。 SwiftGenはこれを支援しました。アイコンを削除するとき、アイコンが使用された場所を通知します。



All Articles