SwiftとObjective-Cが共存していて辛いこと


TL;DR

  • 仕事でSwiftとObjective-C(以下Objc)と共存するプロジェクトを担当していて辛かった
  • その辛さを説明する機会があったのだが、言葉だけで表現するのが難しかったので記事にまとめた

担当プロジェクトの状況

  • 最初のリリースは2014年10月(フルObjc)
  • Swiftに移行し始めたのは2017年〜
  • 2018年6月現在、SwiftとObjcのコードはファイル単位で1:1くらいの割合
  • Objcで書かれたコードはFatViewControllerのお手本のようなコード
  • SwiftファイルでもFatViewControllerがいくつかある

今回話さないこと

  • FatViewControllerのこと
    • この中にあるロジックのせいでテストが増えたりバグを生んだりしたことはあるが
      ObjcとSwiftが共存しているのが原因ではないため

辛いこと

Objcの学習コストが高い

  • 書き方が特殊すぎて、他の言語で勉強していた人が入りづらい
    →Objcがわかる人を連れてくるか、コストをかけて特殊言語Objcを覚えさせる
// プロパティ宣言
// strongとかnonatomicとか何やねん
// この*なんやねん
@property (strong, nonatomic) String* hoge;
// @つけないと怒られる、こんなのObjcだけだよ…。
hoge = @"ほげ";

// なんじゃこの書き方
-(void) pringHoge;

// メソッドチェーン[]多すぎぃ
Array *array = [[Array alloc] init];

SwiftからObjc、ObjcからSwiftを参照するのが面倒

  • SwiftからObjcを参照
    • ProductName-Bridging-Header.hにSwiftから参照させたいObjcクラスのヘッダーをimportしなければならない
    • 書き忘れて「なんで出てこないんだ?」ってなりがち
    • 初見殺しで学習ハードルがある
  • ObjcからSwiftを参照
    • Objc側にProductName-Swift.hをimportする必要あり
    • Swift側を変更した場合は一度ビルドしないとObjc側からは変更後のメソッドやプロパティなどを参照できない。補完もでない。
    • プロダクトが大きくなっているのでビルド時間も無駄な時間になっていく

Swiftで書いたコードがObjcで使えないこと

  • Swiftできれいに書いたコードが実はObjcからも使わなきゃいけないことが発覚すると、
    Objcで使えない機能がたくさんあるせいで書き直しする必要がある
  • 書き直しをするとSwiftの機能をフルに活用できなくなる
    • 安全性が下がる
    • コードが冗長になる

以下にObjcで使えないSwiftの機能を列挙します

struct

  • みんな大好きstruct
  • 値型なので変数代入時にコピーされる
    • 参照型では同じインスタンスを参照している別変数にも影響がでる危険がある
  • Entityとして使いたい
struct HogeEntity {
    let id: String
    var name: String
}

HogeEntity hogeEntity = [[HogeEntity alloc] init];
// コンパイルエラー!!

↓仕方がないのでObjcでも使えるようにする

// Objcから参照できるようにするため
// classにする & NSObject継承必須
class HogeStruct: NSObjct {
    let id: String
    var name: String

    // structみたいに勝手にinitが生えないので自分で書く必要あり
    init(id: String, name: String) {
        self.id = id
        self.name = name
        super.init()
   }
}

HogeEntity *hogeEntity1 = [[HogeEntity alloc] initWithId:@"01" name:@"hoge"];
HogeEntity *hogeEntity2 = hogeEntity1;

hogeEntity2.name = @"fuga";
NSLog(@"%@", hogeEntity1.name); // fuga
  • ご想像のとおり参照型なので変更が同じインスタンスを参照している別変数にも影響がでる
  • structで書けば安全なのに…

enum

  • みんな大好きenum
  • enumを使うとケースをすべて網羅するのがコンパイラにチェックされる
  • 関連するコンピューテッドプロパティを持てる
  • 連想値あると、柔軟な表現できる
// Objcからは使えない!!
enum HogeType {
    case hoge
    case fuga
    case piyo
    case other(name: String)

    var name: String {
        switch self {
        case .hoge:
            return "ほげ"
        case .fuga:
            return "ふが"
        case .piyo:
            return "ぴよ"
        case .other(let name):
            return name
        }
    }
}
  • Objcから使えない理由
    • @objc enumにする必要がある
    • Intを継承する必要がある
    • 連想値付きのcaseが作れない(NG:case other(name: String))
    • enum内のコンピューテッドプロパティはSwiftからは参照できるがObjcからは参照できない
// これなら行ける。いろいろ機能削ってる。
// `case other(name: String)`の解決方法はない…。
@objc enum HogeType: Int {
    case hoge
    case fuga
    case piyo
//    case other(name: String)

    // コンピューテッドプロパティはObjcからは参照できないけど…
    var name: String {
        switch self {
        case .hoge:
            return "ほげ"
        case .fuga:
            return "ふが"
        case .piyo:
            return "ぴよ"
//        case .other(let name):
//            return name
        }
    }
}

その他

  • SwiftでNonNilの引数を受け取るメソッドを宣言しても、Objc側からは渡せてしまう
    • Warningはでるけど、Build Succeededって出る
    • なぜか落ちないけど。
  • Objcのプロパティやメソッドの戻り値を使うと暗黙的アンラップされる型が返ってくるの怖い
    • ?つけなくても動くのでまるで非オプショナルが返ってきている錯覚に陥る
  • オーバーロードがない
    • Swiftらしいメソッドのラベルをつけられない。
  • デフォルト引数がない
    • 全部書けって言われる…。

まとめ

  • Objcと共存することを考えると夜も眠れない。
  • Swiftだけで楽しくコーディングしたい
  • 作り直す時間返してほしい
  • クソコードを生み出してしまうつくり許せないけど
    許さないとリリースできないの辛すぎ

最後に

  • ここまでObjcとSwiftの共存が辛い話をしてきましたが、決してObjcが悪いというわけではありません。
    • Swiftは今も進化しているので、もしかしたらいつか破壊的変更がくるかもしれません。
    • Objcは安定しているので、今後開発する予定がないのであれば、Objcのまま放置でも大丈夫なのはひとまず安心です。
  • 今後、継続的に開発するのであれば、Swiftに置き換えていったほうが上記の問題に当たることはなくなります。
  • 自分はできるだけ早くSwiftへ完全移行したいと思っています!!

他にも「ここが辛いよ!!」というのがあればコメントください!!