Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

FlutterのAdd-to-appを導入しているiOSアプリ保守で起きたライブラリ競合と対処法

プロダクト部クライアントグループの明渡です。 昨年9月から今年4月半ばまで産育休をとり、約1年ぶりの当番ブログです。

産んだ子どもは1歳になりました。食欲魔人という文言がしっくりくる食べっぷりで、成長曲線の上辺すれすれで推移しております。

今回は、同チームに所属している隅山がFlutterKaigiのプロポーザルへ提出したものの、残念ながら不採択だった内容を一部供養します。

fortee.jp

内容としては、Add-to-appを導入済みのStudyplus iOSアプリにてFlutterのバージョンを3.7系から3.10系へアップデート作業中に直面した事象と対処方法です。 なお、Flutterのバージョンアップデート作業自体は滞りなく完了し、発生した事象はAdd-to-app内の参照先モジュールにおける機能追加もまとめて取り込んだことが起因でした。

環境

動作環境

  • Xcode 14.3.1 (Swift 5.8.1)
  • Flutter 3.10.7

ライブラリの管理方法

  • ネイティブ側
    • Swift Package Manager
  • Add-to-appモジュール側
    • Swift Package Manager
      • Option Bを採用しており、ネイティブ側と中身が同じライブラリのバージョン指定や除外をAdd-to-appモジュール配下のPackage.swiftで対応
    • Cocoa Pods
      • pubspec.yamlに記載の自動インポート分

発生した事象

ネイティブ側CropViewControllerとAdd-to-appモジュール側image_cropperで競合してビルドエラー

ネイティブ側で画像のトリミング機能をサポートするため、CropViewControllerを導入済みでした。

GitHub - TimOliver/TOCropViewController: A view controller for iOS that allows users to crop portions of UIImage objects

そして、Flutterで開発した機能内にて新たに同じ機能をサポートするため、image_cropperを導入しました。

pub.dev

このimage_cropperですが、各プラットフォーム毎に選定されたライブラリをラッピングして呼び分ける形式のライブラリです。 iOSでは、弊社アプリケーションのネイティブ側で導入済みであるCropViewControllerのObjective-C版、TOCropViewControllerが採用されています。

そして、ネイティブ側で導入済みのCropViewControllerは依存先としてTOCropViewControllerが指定されています。

TOCropViewController/Package.swift at main · TimOliver/TOCropViewController · GitHub

ネイティブ側とAdd-to-appモジュール側でTOCropViewControllerを二重に参照してしまう形となり、CropViewControllerの利用箇所でビルドエラーが発生しました。

対処方法

結論としては、Swift対応版であるCropViewControllerの利用をやめてObjective-C版のTOCropViewControllerへ統一しました。 ネイティブ側とAdd-to-appモジュール側で同じライブラリを利用すれば、競合してエラーが発生することはなくなります。

まず、ネイティブ側のSPMからCropViewControllerを削除します。 そして、Add-to-appモジュール側のSPMへ親に当たるimage_cropperだけでなくTOCropViewControllerも併せて明記します。

let items = [
    // ... 省略 ...
    "image_cropper",
    "TOCropViewController"
]

let package = Package(
    name: "module_add-to-app",
     // ... 省略 ...
    products: [
        .library(
            name: "module_add-to-app",
            targets: items
        ),
    ],
    dependencies: [
        // ... 省略 ...
    ],
    targets: items.map({ name in
        Target.binaryTarget(
            name: name,
            path: "build/Release/\(name).xcframework"
        )
    })
)

こうすることで、ネイティブ側からもTOCropViewControllerを参照できるようになりました。

あとはネイティブ側でもともとCropViewControllerを利用している箇所で、クラスおよび同じ役割のプロパティやメソッドへ差し替えれば完了です。

SwiftのCropViewControllerからTOCropViewController移行により発生したアプリ起動後実行時エラー

上記のビルドエラーを解消し終えてアプリを起動してみたところ、起動直後にクラッシュが発生してしまいました。

実行時エラーはObjective-Cのコードを触っていた頃はわりとあるあるだったことを今回の記事を書き始めて思い出しましたが、遭遇するのが久々で当時対応に悩みました。

原因としてはAdd-to-app要素は特に関係なく、ネイティブ側で完結する要素でした。 利用しているライブラリを今回のような諸事情でSwift対応版からObjective-C版に移行すると発生するケースがありそうです。

Studyplus iOSアプリではAppDelegateからSceneDelegateへの移行が完了しています。 移行タイミングでAppDelegateにてwindowプロパティの保持をやめており、Objective-C版ライブラリから存在しないwindowプロパティを参照しようとして発生したエラーでした。

対処方法

AppDelegateでのwindowプロパティの保持を復帰せざるおえませんでした。 SceneDelegateにて、アプリ起動時の処理タイミングでAppDelegateのwindowプロパティへも併せてインスタンスを保持するようにすることで解決しました。

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let scene = (scene as? UIWindowScene) else {
        return
    }
    let window = UIWindow(windowScene: scene)
    self.window = window

    // Flutter Add-to-app都合でObjective-C版を参照しているTOCropViewControllerにて、
    // SceneDelegateのwindow未対応のためAppDelegateでやむなくwindowを保持
    let appDelegate = UIApplication.shared.delegate as? AppDelegate
    appDelegate?.window = window

    // ... 省略 ...
}

さいごに

Add-to-appを利用しているアプリを継続的に保守していないとなかなか遭遇しないであろう事象と、実際に行なった対処方法の書き起こしでした。

FlutterのAdd-to-appを利用すると、複数プラットフォームにわたり共通のコードで同じ機能を提供できてとても便利です。 しかしながら、事例が少なめでトラブルに遭遇すると都度解決に手間取りがちなので、同じような問題に直面した方の一助になれば幸いです。

本編は以上です。

おまけとして、バージョンの切られ方が特殊なため、特にCI上での指定に少し手間取ったFlutterバージョン3.10.7の参照方法をしたためておきます。

おまけ: Flutterバージョン3.10.7の参照方法

バージョン3.10.7とは

Flutter SDK Archiveに存在しない、hotfixバージョンです。

Flutterバージョン3.10.6以下で構築されたアプリケーションをiOS 16.6以上の環境で動作させた際に、深刻なパフォーマンス低下が発生する問題に対応されています。

上記への対応はバージョン3.13以上にも含まれるため、事情がなければバージョン3.13以上に上げてしまうのが最もおすすめです。

ローカル環境

flutterリポジトリのWikiに記載通りのコマンドで参照できます。

Upgrading from 3.10.6 to 3.10.7 · flutter/flutter Wiki · GitHub

正式バージョンとしてアーカイブはしないけどtagは切ったので、どうしても利用したい場合はそちらを参照してね、という方針らしいです。

GitHub Actions

弊社ではGitHub ActionsにおけるCI環境でFlutterを利用するため、flutter-actionを導入しています。

こちらのアクションで利用を想定しているFlutterバージョンは、あくまで正式バージョンとしてアーカイブされたものです。 この想定は当たり前で、バージョン3.10.7の扱いが例外的だったため、flutter-actionのREADME.mdへ記載済みの指定方法では参照できませんでした。

flutter-actionリポジトリのIssueで同じことに悩んでる方が質問しており、回答を参考にして無事参照できました。

How do I pull 3.10.7? · Issue #242 · subosito/flutter-action · GitHub

- uses: subosito/flutter-action@v2
  with:
    channel: 'stable'
    flutter-version: '3.10.x'
    architecture: 'x64'
- name: Checkout Flutter 3.10.7
  shell: bash
    run: |
      cd $FLUTTER_ROOT;
      git fetch --tags;
      git checkout 3.10.7;
      flutter --version;

ローカル環境で該当のtagを指定する手順をそのままCI環境で実行する、という内容です。

注意点として、指定したバージョンから半ば無理矢理想定外のバージョンを参照し直すこの方法は、cacheオプションを有効にしてもバージョン3.10.7を参照し続ける保証がありません。

cacheオプションが無効な場合はCIを走らせる都度Flutterを丸ごとインストールすることになってしまい、CIの実行回数×インストール時間の分だけ発生する料金がかさみます。

こういった事情も鑑みて、cacheオプションも問題なく効かせられるバージョン3.13以上へ可能な限り早めに対応する方が良いです。