カスタムexpo-pluginによるinjection処理を実装してみた

カスタムexpo-pluginによるinjection処理を実装してみたの記事のサムネイル

はじめに

モバイルアプリ開発において、「SDKの統合にいつも手こずっている」という方も多いのではないでしょうか。特にExpoのような環境では、ネイティブモジュールの導入がなかなかスムーズにいかないことがあると思います。

先日、Expoで開発されているアプリに、LINE連携機能を実現するために、LINE SDKを導入する必要があり、その対応にちょっと苦戦したので解決方法をこの記事にまとめておこうと思います。

LINE SDK自体、Expoに公式対応していませんが、Expo Modules APIを利用してネイティブモジュールを追加することができます。iOSにおいては、LINEモジュールの初期セットアップ処理をAppDelegateメソッド内に呼び出す必要があるため、その実現方法について紹介していきます。

Expoカスタムモジュールを追加する

まずは、以下のコマンドを実行し、ガイダンスに従い、line-linkingにてExpo Modulesを追加します。

npx create-expo-module@latest –local

参考:Expo Modules API: Get started

プロジェクトのルートディレクトリに、modules/line-linking が追加されていることを確認します。

.gitignoreにてmodules以下のandroid、iosディレクトリ(buildディレクトリ以下は除く)は許可するように変更しておきます。

LINE SDK for Swift をカスタムモジュールに連携する

  • modules/line-linking/ios/LineLinking.podspec に以下1行を追加します。
    • s.dependency ‘LineSDKSwift’, ‘~> 5.0’
  • app.config.ts LSApplicationQueriesSchemes キーを追加します。
ios: {
      /// 省略
      infoPlist: {
        NSAppTransportSecurity: {
          NSAllowsArbitraryLoads: true,
          LSApplicationQueriesSchemes: ['lineauth2'], // LINE連携用設定
        },
        CFBundleDevelopmentRegion: 'jp',
      },
    /// 省略
}
  • modules/line-linking/ios/LineLinkingModule.swift を編集し、React Native側から呼び出すFunctionを実装します。
import ExpoModulesCore
import LineSDK // 追加

public class LineLinkingModule: Module {

    public func definition() -> ModuleDefinition {
       
        Name("LineLinking")
               
        Events("onLoginSuccess")
       
        AsyncFunction("lineLoginAsync") { (channelId: String) in
            LoginManager.shared.login(permissions: [.profile, .openID]) { result in
                switch result {
                case .success(let loginResult):
                    let token = loginResult.accessToken.value
                    self.sendEvent("onLoginSuccess", [
                        "accessToken": token
                    ])
                case .failure(let error):
                    print("Failed to login with LINE. Error occurred ->\(error)")
                }
            }
        }
    }
}

// LoginManagerモジュール初期セットアップ処理
// アプリ起動後に一度だけ呼び出す必要がある
@objc public class LineLinking: NSObject {
    @objc public static func LineLinkingSetup(_ channelId: String) {
        print("LineLinkingSetup called: channelID: \(channelId)")
        LoginManager.shared.setup(channelID: channelId, universalLinkURL: nil)
    }
   
// LINEから戻る時の起動制御処理
// AppDelegateメソッドのhandleOpenUrlFunction内に実施する必要がある
@objc public static func handleOpenUrl(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        return LoginManager.shared.application(app, open: url)
    }
}

カスタムexpo-pluginを実装する

loginManagerモジュールの初期セットアップ処理とLINEから戻る時の起動制御処理は原則AppDelegate内で実施する必要があり、これらの処理を追加するためにInjection用のカスタムexpo-pluginを実装します。

  • カスタムpluginを実装します。今回は modules/line-linking/ios 配下に、withModAppDelegate.ts として実装します。
const { withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');

// Injection対象のAppDelegate.mmを検索する
const withModAppDelegate = (config) => {
  return withDangerousMod(config, [
    'ios',
    async (configWithMod) => {
      const appDelegatePath = path.join(
        configWithMod.modRequest.platformProjectRoot,
        config.name.replaceAll('-', ''),
        'AppDelegate.mm'
      );

      // ファイル内のコンテンツを取得する
      let appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');

      // Injection内容を準備する
      const importHeaderCode = '#import <LineLinking-Swift.h>';

      const setupFunction = 'LineLinkingSetup';
      const setupCode = `\n  // LineLinking Setup\n  [LineLinking ${setupFunction}:@"${process.env.EXPO_PUBLIC_LINE_CHANNEL_ID}"];`;

      const handleOpenUrlFunction =
        '[LineLinking handleOpenUrl:application open:url options:options];';

      // モジュールheader injection
      if (!appDelegateContent.includes(importHeaderCode)) {
        appDelegateContent = appDelegateContent.replace(
          '#import "AppDelegate.h"',
          `#import "AppDelegate.h"\n${importHeaderCode}`
        );

        fs.writeFileSync(appDelegatePath, appDelegateContent);
        console.log(
          'Successfully added LineLinking module header into AppDelegate.mm'
        );
      }
      // LINE LoginManagerセットアップ injection
      if (!appDelegateContent.includes(setupFunction)) {
        appDelegateContent = appDelegateContent.replace(
          'return [super application:application didFinishLaunchingWithOptions:launchOptions];',
          `${setupCode}\n  return [super application:application didFinishLaunchingWithOptions:launchOptions];`
        );

        fs.writeFileSync(appDelegatePath, appDelegateContent);
        console.log(
          'Successfully added LineLinking setup code into AppDelegate.mm'
        );
      } else {
        console.log('setup code already exists in AppDelegate.m');
      }

      // LINEから戻った場合の起動制御 injection
      if (!appDelegateContent.includes(handleOpenUrlFunction)) {
        appDelegateContent = appDelegateContent.replace(
          'return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];',
          `return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options] || ${handleOpenUrlFunction}`
        );

  // Injection完了後のコンテンツをAppDelegateに適用する
        fs.writeFileSync(appDelegatePath, appDelegateContent);

        console.log(
          'Successfully add LineLinking handleOpenUrlFunction into AppDelegate.mm'
        );
      } else {
        console.log(
          'handleOpenUrlFunction code already exists in AppDelegate.mm'
        );
      }

      return configWithMod;
    },
  ]);
};

module.exports = withModAppDelegate;
  • app.config.ts にカスタムpluginファイルを追加します。
plugins: [
    'expo-router',
    '@react-native-firebase/app',
    '@react-native-firebase/crashlytics',
  ・・・省略
    './modules/line-linking/ios/withModAppDelegate.ts', // iOS LINE連携用Plugin
  ]

まとめ

今回、ExpoアプリにおいてLINE SDKの導入を実現するために、カスタムexpo-pluginを利用したAppDelegateへのInjection処理を実装しました。

このアプローチにより、LINEモジュールの初期セットアップやhandleOpenUrlの処理がExpoビルド時に自動的に追加されます。

主なポイントは以下の通りです:

  • Expo Modules APIを利用してカスタムモジュールを作成し、LINE SDKを導入。
  • カスタムexpo-pluginを利用して、iOSネイティブコードへの動的Injectionを実現。

今回の内容は、他のSDK連携やカスタム機能の開発にも応用可能であり、Expo環境でネイティブモジュール対応の際にご参考いただければ幸いです。

アプリ開発でお困りの方はぜひご相談ください!