はじめに
モバイルアプリ開発において、「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ビルド時に自動的に追加されます。
今回の内容は、他のSDK連携やカスタム機能の開発にも応用可能であり、Expo環境でネイティブモジュール対応の際にご参考いただければ幸いです。
アプリ開発でお困りの方はぜひご相談ください!
主なポイントは以下の通りです: