😮

FlutterでのiOSアプリ開発 - 学びと気づき

に公開

はじめに

本記事では、Flutterを使用したiOSアプリケーション開発を通じて得られた知見を共有します。

開発環境情報

執筆時点での環境情報は以下の通りです。

[✓] Flutter (Channel stable, 3.22.3, on macOS 15.3 darwin-arm64)
    • Flutter version 3.22.3 on channel stable
    • Framework revision b0850beeb2 (2024-07-16)
    • Engine revision 235db911ba
    • Dart version 3.4.4
    • DevTools version 2.34.3

[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
    • Build 16B40
    • CocoaPods version 1.16.2

[✓] VS Code (version 1.96.4)
    • Flutter extension version 3.104.0

課題と解決策

1. CupertinoActivityIndicator の色設定の制限

課題:

  • 指定した色よりも若干薄く表示されます。
  • CupertinoActivityIndicator クラスでアルファ値が固定されているため、完全に一致させることが困難です。

コード詳細:

// activity_indicator.dart 内のアルファ値設定
const List<int> _kAlphaValues = <int>[
  47, 47, 47, 47, 72, 97, 122, 147,
];

// 部分的に表示される場合のアルファ値
const int _partiallyRevealedAlpha = 147;

対応策:

  • カスタムの ActivityIndicator を実装する選択肢もありますが、デザインチームと協議の上、今回は色の差異を許容することとしました。

2. アプリ非起動時のPush通知ハンドリング

課題:

  • firebase_messaging パッケージを使用していますが、アプリ非起動時のPush通知ハンドリングに制限があります。
  • バックグラウンドメッセージハンドラのコードを確認すると、Android以外のプラットフォームでは処理が早期リターンしていました。

Future<void> registerBackgroundMessageHandler(
    BackgroundMessageHandler handler) async {
  if (defaultTargetPlatform != TargetPlatform.android) {
    return;  // Android以外は早期リターン
  }
  // 以下Android向け処理
  // ...
}

解決策:

  • iOS側の AppDelegate.swift に処理を追加し、Flutter側とのブリッジを構築しました。
import Flutter
import UIKit
import FirebaseMessaging

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    // メソッドチャネル名の定義
    let methodChannelName = "com.example.app/notification"

    override func application(_ application: UIApplication,
                          didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Flutterプラグインの登録
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func application(_ application: UIApplication,
                          didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                          fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

        if let controller = window?.rootViewController as? FlutterViewController {
            // Flutterメソッドチャネルの初期化
            let methodChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: controller.binaryMessenger)

            // Dart側に通知データを渡す
            methodChannel.invokeMethod("firebaseMessagingBackgroundHandler", arguments: userInfo) { _ in
                completionHandler(UIBackgroundFetchResult.newData)
            }
        } else {
            completionHandler(UIBackgroundFetchResult.noData)
        }
    }
}

Flutter側で完結できたシーン

1. 画面レイアウト作成

基本的にMaterialデザインのコンポートを使用しました。

ProgressIndicatorやアラートなど一部のUIは、ユーザが慣れ親しんだものを利用する方針としたので、iOSとAndroidで出し分けることとしました。

2. 通信周り

httpパッケージやhttp_proxyパッケージを使用することで、ネットワーク通信のロジックを共通化できました。ネットワーク接続状態の監視もconnectivity_plusパッケージにより実現しています。

3. 権限管理

permission_handler パッケージを使用することで、デバイス権限を管理しています。

カメラ利用やデータ収集許可などの説明文は Info.plist に追加する必要があります。

4. データ保存

以下のパッケージを利用し、データ保存機能を実装しました。

  • キーバリューストア: shared_preferences パッケージ
  • SQLiteデータベース: drift パッケージ

5. ビルド・デプロイ

Fastlaneを活用し、以下のプロセスを自動化しました。

  • 証明書・プロビジョニングの管理
  • ビルドの自動化
  • テスト配布処理

6. 環境設定の管理

Flutter標準の--dart-define-from-fileオプションと環境設定JSONファイルを活用し、開発・ステージング・本番環境の設定を管理しています。

  • 環境変数定義ファイル(.json)を用意
{
    "apiKey": "example_key",
    "baseUrl": "https://api.example.com",
    "bundleId": "com.example.app",
    "flavor": "dev"
}

  • ビルド時に -dart-define-from-file オプションを使用
  • 生成された設定は Generated.xcconfig ファイルに反映されます。

まとめ

すべての機能をFlutter側で完結させることはできませんでしたが、ロジックや画面作成など大半は共通化できたことで、全体的には効率的な開発ができたと考えています。

株式会社ガラパゴス(有志)

Discussion