🐨

React NativeからSwiftのコードを呼び出す方法

2024/06/29に公開

やりたいこと

ネイティブのSwift側で実装している関数をReact Nativeから呼び出したいです。

一からプロジェクトを立ち上げるのが面倒なので、以下の記事で作成したチャットアプリをベースにします。

https://zenn.dev/headwaters/articles/cc110e5b6866e8

Swift側の実装

Xcodeを立ち上げ

ReactNativeはVScode上でコーディングしますが、SwiftはXcode上でコーディングします。
理由はVScode上でコーディングしてるとバグがあった時に指摘してくれないからです。(もしかしたら拡張機能入れたらしてくれるかも)
まあSwiftの標準エディターはXcodeなので今回はXcodeを使います。


対象のプロジェクトのiosフォルダ直下にある<PJ名>.xcworkspaceをダブルクリックして立ち上げます。

もしくはターミナル上からOpenコマンドでも立ち上がります。

open <xcworkspaceのpath>

以下のように立ち上がればOK


SwiftファイルとBridgeファイルの作成

Moduleファイルを作成します。
右クリックして「New File」を選択


「Swift File」を選択


適当にファイル名を決めて、「Create」をクリック
おそらく「Briding-Headerファイルを作成しますか?」と出てくるので、作成してください
(私はすでに作成済みのため出てきませんでした。)


作成されたBridging-Headerファイルに以下を追加

<PJ名>-Bridging-Header.h
#import "AppDelegate.h"
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"


次に作成したSwiftファイルにクラスと関数を追加します。
今回は単純に文字列配列の変数をReactNative側に返すだけの関数です。
また、イベントを検知するためにイベント名を定義しておきます。今回は「streamingResponse」としています。

StreamGPTModule.swift
import Foundation
import React

@objc(StreamGPTModule)
//
//  StreamGPTModule.swift
//  multi_modal_ai_app
//
//  Created by ikeuchi.ryuto on 2024/06/29.
//

import Foundation
import React

@objc(StreamGPTModule)
class StreamGPTModule: RCTEventEmitter {
  
  override func supportedEvents() -> [String]! {
    return ["streamingResponse"]
  }
  
  @objc
  func startStreaming() {
    let streamingResponse = ["data1", "data2", "data3"]
    sendEvent(withName: "streamingResponse", body: streamingResponse)
  }
}


Objective-Cファイルの作成

SwiftファイルだけでなくObjective-Cのファイルも必要ですので作成します。
Swiftファイル同様に右クリックから「New File」を選択して、メニューの中から「Objective-C File」を選択


ファイル名はSwiftファイルと全く同じにしてください。
今回自分は「StreamGPTModule」としたので、同じ名前にしています。


中身のソースコードは以下です。
モジュール名と使う関数をRCT_EXTERN_METHODの引数に入れます。

StreamGPTModule.m
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

  
@interface RCT_EXTERN_MODULE(StreamGPTModule, RCTEventEmitter)
  
RCT_EXTERN_METHOD(startStreaming)
  
@end


Pod Install

最後にPod Installすることを忘れずに

cd ios && pod install


ReactNative側の実装

ReactNative側では以下のようにします。

  1. マウント時にSwift側で作成したModuleを取得
  2. useEffect内で定義したEventをサブスクライブ
  3. アンマウント時にEventをアンサブスクライブ
  4. ボタンをクリックしたら定義した関数を実行
App.tsx
import React, {useEffect, useState} from 'react';
import {
  KeyboardAvoidingView,
  SafeAreaView,
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  useColorScheme,
  View,
  NativeEventEmitter,
  NativeModules,
  NativeModulesStatic,
} from 'react-native';

...

  const [streamGPTModule, setStreamGPTModule] = useState<
    NativeModulesStatic[string] | null
  >();

...

  const handleStreamWithNativeModule = () => {
    streamGPTModule.startStreaming();
  };

  useEffect(() => {
    if (streamGPTModule == null) {
      setStreamGPTModule(NativeModules.StreamGPTModule);
      return;
    }

    const eventEmitter = new NativeEventEmitter(streamGPTModule);
    const eventListener = eventEmitter.addListener(
      'streamingResponse',
      (data: string) => {
        console.log('streamingResponse', data);
      },
    );
    return () => {
      if (eventListener) {
        eventListener.remove();
      }
    };
  }, [streamGPTModule]);

...

検証

ボタンを押してみたところEventListenerから取得できていることがLogから分かりました。


ちなみにModule名を「StreamGPTModule」としているのは、ReactNative側でいい感じにStreamで受け取れるメソッドがないためネイティブのSwiftに任せようと思っているためです。
ただSwift側でも中々一筋縄でいかなかなくてまだ成功してないのでまた別の機会で。

ヘッドウォータース

Discussion