asken テックブログ

askenエンジニアが日々どんなことに取り組み、どんな「学び」を得ているか、よもやま話も織り交ぜつつ綴っていきます。 皆さまにも一緒に学びを楽しんでいただけたら幸いです!

Objective-CからSwiftへ書き換えをスムーズに行なった話

はじめに

こんにちは。モバイルエンジニアの大澤です。

普段はあすけんのAndroid/iOSアプリを開発しています。

あすけんのサービス運営は2023年で15周年を迎え、iOSアプリの運用も10年目を迎えました。開発当初のiOSのメイン言語はObjective-Cだったため、今でもObjective-Cのコードが一部残っており、それによって様々な問題が発生していました。

2007年にiPhoneが発表されてから15年以上が経ち、同じようにObjective-CからSwiftへの移行を進めたいと思っている会社も少なくないと思います。 本記事ではSwiftへのスムーズな移行を実現するための実践的なアプローチを紹介します。

なぜ書き換えるのか?

  • Objective-CとSwiftのブリッジで不具合が発生しやすい。
    • e.g. Objective-Cのクラスで、ある変数をnullableとして宣言していなかった。Swiftでその変数を呼び出した際に、値がnilであったがnilチェックがなくアプリがクラッシュしてしまった。
  • モダンな書き方や機能が使えない。
    • e.g. structやSwiftのenumが使えない。

上記の目的を達成するため移行作業を進めました。

プロジェクトの分析

プロジェクトでどれくらいのObjective-Cのコードがあるか把握するために自作のスクリプトを作成しました。

CIでスクリプトを実行し、結果をSlackに通知し、定期的に進捗をチェックするようにしました。

書き換え戦略

ツールの活用

書き換える作業を手動で行うと、書きミスが発生する可能性が高いため、ツールを使って変換しました。

変換後にうまく動かない部分や意図しない変換になった場合は手動で書き換えて対応しました。

LuizZak/SwiftRewriter

環境
Xcode 14.3.1 & Swift 5.8.1

SwiftRewriterはコマンドを実行することでObjective-CからSwiftへ変換されたファイルが生成されます。

$ swift run -c=release SwiftRewriter path /path/to/project/

変換の例

Objective-Cのコード

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
- (void)introduce;

@end

@implementation Person

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
    self = [super init];
    if (self) {
        _name = name;
        _age = age;
    }
    return self;
}

- (void)introduce {
    NSLog(@"My name is %@ and I am %ld years old.", self.name, (long)self.age);
}

@end

ツールでSwiftに変換したコード

import Foundation

class Person: NSObject {
    private var _name: String!
    private var _age: Int = 0
    var name: String! {
        get {
            return self._name
        }
        set {
            self._name = newValue
        }
    }
    var age: Int {
        get {
            return self._age
        }
        set {
            self._age = newValue
        }
    }

    init(name: String!, age: Int) {
        _name = name
        _age = age
        super.init()
    }

    func introduce() {
        NSLog("My name is %@ and I am %ld years old.", self.name, CLong(self.age))
    }
}

変換後に手直ししたコード

class Person: NSObject {
    private var name: String!
    private var age: Int = 0

    init(name: String, age: Int) {
        self.name = name
        self.age = age
        super.init()
    }

    func introduce() {
        NSLog("My name is %@ and I am %ld years old.", self.name, CLong(self.age))
    }
}

注意点

if (@available(iOS 15, *)) {
    
}

上記のようなコードが混ざっているとObjective-CからSwiftへの変換に失敗する現象が発生しました。

その部分はコメントアウトし、変換後に手動で書き換えました。

🎉 成果と今後の流れ 🎉

成果として、半年でObjective-Cの割合を約25% → 約5%まで削減することができました。

これによりObjective-CとSwiftのブリッジを意識することはぼぼなくなりました。

今後はSwiftらしい書き方にリファクタリングを進める予定です。

積極採用中です!

askenではエンジニアを募集しています。 よろしくお願いします。

www.asken.inc