RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

Swift 3 からSwift 4.2 にリプレイスとリファクタリングした話しを紹介します。

開発本部APP・UXチームの玉置です。主にiOSAndroidの運用を担当しています。
先月の4月からレアジョブに入社しました。
チーム内で前回の記事を投稿したチームリーダーの羽田の事を社内で浸透していない「ジャンボさん」と呼んでいる数少ないメンバーです。

rarejob-tech-dept.hatenablog.com

それまではフリーランスとしてモバイルアプリのエンジニアをしていました。
レアジョブ(RareJob)アプリに入社してすぐに取り組んだことがiOSのレガシーとなっている技術負債の返済です。
その名もSwiftとXcodeのバージョンを引き上げるというリファクタリング作業です。
いきなり大きなプロジェクトを任されましたが、約2週間の作業で事を終えることができ今ではSwift 4.2で開発しています。
今回はそのSwiftのバージョンを上げる事を通して得られた知見について紹介したいと思います。

レアジョブアプリ自体は2016年、つまりちょうど3年前にリリースされました。
当時のSwiftの最新バージョンがSwift 3.2 でした。

逆に同一バージョンで3年間も正常に動くSwiftに感動しています。

と前置きはこれぐらいにして本題に移りたいと思います。

RareJobには次の行動規範があります。

  1. やりたいことをやろう (High Alignment High Autonomy)
  2. ストーリーを語ろう (Share the Whole Story)
  3. 変化を生み出そう (Make a Difference)

入社してから上記3つの行動規範を元にレアジョブアプリのリプレイス作業に取り組みました。

やりたいことをやろう、に基づいて3年間運用していたSwift 3 を最新バージョンに近いSwift 4.X系にリプレイスすることを決意しました。

本当は今のSwiftの最新バージョンである Swift 5 に一気にリプレイスする方がユーザーのストレスを 最小限に抑えられていいのですが諸処の事情により一つ前のSwift 4.2 にバージョンアップという決断に踏み込みました。

Swift 4.2 にして得られたメリットについて

結果的にはSwift 4.2 にバージョンアップさせる事で下記のメリットを得られました。

  1. Xcode のバージョンを引き上げることができるのでアプリの寿命を引き伸ばせた
  2. SwiftのAPIを最新のものにリプレイスできた
  3. Xcode のビルド速度の向上

最初の課題について

Swift のバージョンを上げる事自体はそれほど難しくありません。
XcodeBuild SettingsSwift Language Versionを3.2 から 4.2 に変更するだけで完了します。

注意はSwift 3.2 の頃は Xcode 9.4 でビルドしていましたがXcode 9.4 ではSwift 4.1 までしかサポートしていないのでXcode 10.1 に変更することです。

f:id:qed805:20190426201559p:plain
Swift 4.2

ただし、そこからが大変でした。
Swift 4.2 に移行すると以下の変更点が発生してしまいます。

  1. Xcodeのnew Build System への対応
  2. Swift APIの修正
  3. 使用しているライブラリのSwiftバージョンのリプレイス
  4. Warning対応

それぞれ解説していきます。

Xcodeのnew Build System への対応

Swift 4.2にバージョンを上げた時に最初に起きた問題は
Xcodeの既存のBuild SystemだとMultiple commands produceエラーによってビルドが失敗するという現象でした。

f:id:qed805:20190426202442p:plain
Multiple commands produce

解決方法は2つあります。

  1. Xcodeの設定でSwift 3のビルドシステムを維持する(レガシービルドの採用)
  2. 新しいビルドシステムを採用してライブラリやキャッシュなどを一度削除して再インストールする

1, 2 の選択についてはXcodeFile < Workspace Settings上で設定することができます。

f:id:qed805:20190426202802p:plain
Buidl System

古いビルドシステムのまま開発を進めることができますがそれだとSwiftのバージョンを上げるメリットを享受できません。
さらに新しいビルドシステムにした方が開発が快適になるはずです。
そのため弊社は新方式のビルドシステムを採用する方針でリプレイスを進めました。

デフォルトでは新方式のビルドシステムなので設定で古いビルドシステムにするのではなく、
既存のライブラリなどを再度インストールすればいいだけです。

レアジョブアプリではcocoapodsとcathegeを使ってライブラリ管理を管理しています。
これらのlockファイルを一旦削除して入れ直せば動くようになります。
また、同時にXcodeのキャッシュを削除してclean buildをします。

これだけで新方式でのビルドシステムでアプリが起動するようになります。

Swift APIの修正

f:id:qed805:20190510020754j:plain
Swift 4.2

ビルド方式を新方式にしてアプリが認識するようになってからがリプレイス作業の本番でした。
次に襲ってきた問題はAppleが提供しているSwiftのAPIが変わったことによるビルドエラーです。

例えば、次のコードはSwift 4.2 ではエラーになってしまいます。

self.view.bringSubview(toFront: self.button) 

上記コードをSwift 4.2 で動かすには次のように修正します。

self.view.bringSubviewToFront(self.button)

このような文法チェックを逐一修正していきました。

他にも

return UITableViewAutomaticDimension

というiOSエンジニアであれば懐かしい文法を拝見することになりました。

こちらは

return  UITableView.automaticDimension

このように変更しなければいけません。

主な変更点を挙げてみますと

  • UIViewのaddSubviewメソッド
  • UITableView のデリゲートメソッド
  • RxSwift のVariableの初期宣言
  • NSNotificationNameの書き方の変更
  • NSAttributedString のKeyの指定の変更

などが挙げられます。
これらを一つ一つ修正していきました。
といっても総計150個ほどのエラーなので集中的に行えば2,3 時間でリプレイスできるボリュームの作業になります。

ここまで正しく修正を行うとXcode10.1でのビルドが成功するようになりました。

使用しているライブラリのSwiftバージョンのリプレイス

レアジョブのアプリは調べられば分かるのですが様々なライブラリに依存しているプロジェクトです。
そして設計はMVVMアーキテクチャを採用しています。当然RxSwiftも利用しています。
そのため次のタスクはこれらのライブラリのSwift バージョンを上げる作業になります。(念のためSwiftのバージョンは3.2です。)
全てのライブラリをSwift 4.2 にバージョンアップさせたいのですが確認すると50個ほどのライブラリを使っていました。

これらのライブラリを全てバージョンアップさせるのは非現実的だと判断したので部分的に重要かつ変更が簡易なライブラリのみを Swift 4.2 にバージョンアップさせることにしました。

cocoapodsのバージョン管理はPodfile上で

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = "3.2"
    end
  end
end

という風に本プロジェクトとは別に管理しています。
そこでRubyコードで次のようにすれば部分的にライブラリのバージョンを上げたり維持したりできます。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if ['RxSwift', 'RxCocoa'].include? "#{target}"
        config.build_settings['SWIFT_VERSION'] = "4.2"
      else
        config.build_settings['SWIFT_VERSION'] = "3.2"
      end
    end
  end
end

if else end文に該当するコードを差し込みました。

あとは各ライブラリを個別に上げていき修正可能なエラーであれば対応していく作業をするだけです。
これである程度安全にアプリの品質を担保することができます。

このような作業をしたことによってレアジョブアプリはRxSwiftとRxCocoaなどメジャーなライブラリのSwift 4.2化が完了しました。

Warning対応

細かく書くことはないのですが、
Swift 3 の時のXcodeで表示されていたWarningの数が300数個あり今後の開発効率が下がる可能性がありました。
そのためリファクタリングという意味も込めてこのタイミングでWarning対応をできる限りすることにしました。
結果的には約100個ほどと半分以下まで修正することに成功しました。
半分が本プロジェクトコードの対応で改善できたもの、残り半分はライブラリのアップデートで改善されました。

これである程度目通しの良いコードになったと思います。

リリース前にAVAudioSessionで致命的なバグが発生

今リリースしているアプリはもちろんSwift 4.2 で正常稼働しています。
もちろんSwift 3から一気にSwift 4.2 にバージョンアップしましたのでどんなバグがあるのかは未知でしたので
リリース前に社内テストで全画面・全仕様を洗い出しリリース前のテストを実施しました。

その時にやはりSwift のバージョンアップに伴って2,3 個のデザイン崩れ・数個の既存バグを発見しました。
またチームリーダーの羽田がAppleへの審査前にレッスンルームのテストをしてくれたおかげでBluetoothに関する致命的なバグを発見することができました。

レアジョブアプリはレッスンルームの音声のやり取りでAVFoundationを利用しています。
AVFoundationにはAVAudioSessionというクラスが存在してこのクラスにはsetCategoryというメソッドがあります。

// Swift 3
try session.setCategory(AVAudioSessionCategoryPlayback)

このようなコードがありました。

Swift 4.2 にしてからこのメソッドがさらっとsetCategory:mode:modeをしなければならない仕様に変わっていました。

// Swift 4.2
try session.setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode.default, options: [])

作業中はこの新APIはそこまで問題にならないだろうと思い深追いせずに進めましたが、Bluetoothとの接続に関わるメソッドだったようで Swift4.2 + Xcode10.1のバグの可能性によりそもそも「動いていない」問題が発生しました。つまりBluetoohイヤホンだと音声が聞こえないというバグです。
(Xcode10.2を使えば解決できるのですが社内事情によりXcode10.1 でのリプレイスのため)

細かい話しは下記のリンクが参考になります。

stackoverflow.com

そこで行った対応は最終手段としてObjective-Cのカテゴリを利用してAVAudioSessionクラスを拡張して古いAPIを使うことにしました。
次のようなObjective-Cクラスをimportします。

AVAudioSession+Swift.h

#ifndef AVAudioSession_Swift_h
#define AVAudioSession_Swift_h

@import AVFoundation;

NS_ASSUME_NONNULL_BEGIN

@interface AVAudioSession (Swift)

- (BOOL)swift_setCategory:(AVAudioSessionCategory)category error:(NSError **)outError NS_SWIFT_NAME(setCategory(_:));
- (BOOL)swift_setCategory:(AVAudioSessionCategory)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError NS_SWIFT_NAME(setCategory(_:options:));

@end

NS_ASSUME_NONNULL_END

#endif /* AVAudioSession_Swift_h */

AVAudioSession+Swift.m

#import <Foundation/Foundation.h>
#import "AVAudioSession+Swift.h"

@implementation AVAudioSession (Swift)

- (BOOL)swift_setCategory:(AVAudioSessionCategory)category error:(NSError **)outError {
    return [self setCategory:category error:outError];
}
- (BOOL)swift_setCategory:(AVAudioSessionCategory)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError {
    return [self setCategory:category withOptions:options error:outError];
}

@end

初めてObjective-Cクラスを使いましたのでSwiftクラスで使えるようにBridging-Header.hを導入することになりました。

これでSwift クラスで使用するときにsetCategory:modeではなくsetCategoryのままで利用できるようになりますので解決できました。
ただこの対応は黒魔術のようにも見えますので他にも解決策がありそうでしたら教えて頂けると嬉しいです。

最後に

このように最後までどこでバグが混入してしまうのか分からないのがSwiftのバージョンアップ時のリスクです。
それらをリリース前に防げた点といい、本当に開発しやすい環境で助かりました。
最後のバグの発見と改修はファインプレーに近いです。

ということで大変長くなりましたがSwift 4.2 へのバージョンアップした際のお話しはこれで全部になります。
長々ではありましたが読んで頂きありがとうございます。