📌

React Native の Native Module に props を追加する方法(iOS編)

こんにちは!

KANNA の開発のお手伝いをしております、 len_prog です。

React NativeのNative Moduleでカメラ起動させてみた(iOS/Swift)
https://zenn.dev/aldagram/articles/e74512b3747a3b

React NativeのNative Module化について(Android)
https://zenn.dev/aldagram/articles/a89fac17788a3e

上記記事にて Native Module を使用して新規実装を行う方法については解説していただきましたが、実際の業務では既存の Native Module に対して props を追加する必要がある場面もあるかと思います。

今回、実際に私が iOS の Native Module に対して props を追加する機会がありましたので、せっかくなので調べたことを記事として残そうと思います。

サンプルアプリについて

今回の主題は新しい props を アプリから渡して iOS の Native Module 側で使用できる状態にすることなので、説明のノイズにならないようサンプルアプリ自体はかなり簡素なものとしました。
見たとおりではありますが、懐中電灯を再現したアプリケーションです。

ボタンをタップすると端末のライトのオン・オフができる機能のみを持ちます。

なお、端末のライトのオン・オフを行うロジックが Native Module として書かれております。

以下が実際に動作している様子です。

なお、サンプルアプリのソースコードは以下のリポジトリにございますので、ご興味ある方はご覧ください!

https://github.com/h-tachikawa/electric-torch

Native Module に機能を追加する

仮にこのサンプルアプリに対して、明るさをスライダーで調整できるようにしたいという要望が来たとします。

その場合、以下の手順で Native Module に明るさを表す props を追加する必要があります。

  1. アプリから任意の明るさを Native Module に渡せるようにする
  2. Native Module の props として明るさを受け取れるようにする

以下、上記それぞれの手順について詳しく解説していきます。

1. アプリから任意の明るさを Native Module に渡せるようにする

まず、アプリのコード上で明るさの値を表す brightness state を定義し、 Native Module の props に渡せるようにします。

なお、ここについては至って普通の React のコードなので説明を省略いたします(追加: ではじまるコメントが入っている箇所が追加部分です)。

// src/Page.tsx

import React, {useState} from 'react';
import {View, SafeAreaView, NativeModules, StyleSheet} from 'react-native';
import {Header, Button, Slider, Icon, lightColors} from '@rneui/themed';

export const Page = () => {
  const [isActive, setIsActive] = useState(false);
  const [brightness, setBrightness] = useState(0.5); // 追加: 現在の明るさを表す state。

  const toggle = () => {
    NativeModules.ElectricTorchModule.toggle(isActive);
    setIsActive(prev => !prev);
  };

  // 追加: スライダーが動いた際に呼ばれる関数。現在の明るさを ElectricTorchModule#changeBrightness に渡す。
  const changeBrightness = (nextBrightness: number) => {
    if (!isActive) {
      return;
    }
    NativeModules.ElectricTorchModule.changeBrightness(brightness);
    setBrightness(nextBrightness);
  };

  return (
    <SafeAreaView>
      <Header
        containerStyle={styles.container}
        leftComponent={{icon: 'menu', color: '#FFF'}}
        centerComponent={{text: '懐中電灯アプリ(仮)', style: styles.heading}}
      />
      <View style={{alignItems: 'center'}}>
        <Slider
          disabled={!isActive}
          value={brightness}
          onValueChange={changeBrightness} // 追加: Slider の値が変わったときに changeBrightness 関数を呼ぶ。
          minimumValue={0.01}
          maximumValue={1.0}
          step={0.01}
          thumbStyle={{height: 20, width: 20, backgroundColor: 'transparent'}}
          style={{width: '90%'}}
          thumbProps={{
            children: (
              <Icon
                name="brightness-6"
                type="material-community"
                size={20}
                reverse
                containerStyle={{bottom: 20, right: 20}}
                color={isActive ? lightColors.secondary : lightColors.grey4}
              />
            ),
          }}
        />
        <View
          style={{
            flexDirection: 'row',
            justifyContent: 'center',
            marginTop: 20,
          }}>
          <Button
            color="secondary"
            title={isActive ? '消灯する' : '点灯する'}
            onPress={toggle}
            containerStyle={{width: 150}}
          />
        </View>
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#397af8',
    marginBottom: 20,
    width: '100%',
    paddingVertical: 10,
  },
  heading: {
    color: 'white',
    fontSize: 22,
    fontWeight: 'bold',
  },
});

2. Native Module の props として明るさを受け取れるようにする

手順1でアプリから明るさを props として渡せるようにしたので、次にそれを Native Module 側で受け取れるようにする必要があります。

この手順では2つのファイルを編集する必要があります。

1つは module の実態(ios/ElectricTorchModule.swift)、もう1つは module を React Native から認識させるために必要なファイル(ios/ElectricTorchModule.m)です。

// ios/ElectricTorchModule.swift

import Foundation
import AVFoundation

@objc(ElectricTorchModule)
class ElectricTorchModule: NSObject {
  private var currentBrightness: Float = 0.5

  // ...省略
  
  // ① メソッドを定義する
  @objc
  func changeBrightness(_ nextBrightness: Float) {
    currentBrightness = nextBrightness
    
    let avCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video)
    
    if !avCaptureDevice!.hasTorch { // 端末のライトが使用できるか確認する
      return
    }
    
    do {
      try avCaptureDevice!.lockForConfiguration() // avCaptureDevice のメソッドを呼ぶ前に必ずロックする必要がある
      try avCaptureDevice?.setTorchModeOn(level: nextBrightness)
    } catch let error {
      print(error)
    }
    avCaptureDevice!.unlockForConfiguration() // avCaptureDevice のロックを解除する
  }
}
// ios/ElectricTorchModule.m

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(ElectricTorchModule, NSObject)
RCT_EXTERN_METHOD(toggle:(BOOL) isActive)
RCT_EXTERN_METHOD(changeBrightness:(float) nextBrightness) // ② 追加したメソッドのシグネチャを宣言する
@end

ここからは、2つのファイル内にあるコメントの ① メソッドを定義する / ② 追加したメソッドのシグネチャを宣言する の箇所について説明します。

①メソッドを定義する

アプリ側から呼ぶメソッドを定義します。

React Native から呼べるようにするために最初に @objcを書く必要がありますが、残りの箇所には普通にロジックを書けば大丈夫です。

② 追加したメソッドのシグネチャを宣言する

①で実装した Native Module のメソッドを React Native から呼び出せるようにするために、アプリと Native Module の橋渡しを行う Objective-C のファイル内でメソッドのシグネチャを宣言する必要があります。

私もそうですが Objective-C に慣れていない方も多いと思うので、このコードが何をしているのかを説明します。

RCT_EXTERN_METHOD は、 React/RCTBridgeModule.h で定義されているマクロです。このマクロにメソッドのシグネチャの情報を渡すことで React Native からこのメソッドを呼び出せるようになります。

RCT_EXTERN_METHOD(changeBrightness:(float) nextBrightness)

なお、RCT_EXTERN_METHOD マクロに渡しているchangeBrightness:(Float) nextBrightness は以下の構造になっています。

  • changeBrightness…メソッド名
  • (Float)…引数名の型
  • nextBrightness…仮引数名

動作確認

さて、これで実装が終わったので XCode でビルドし、iPhone の実機で動作確認しましょう。

以下が実際に動作している様子です。

お疲れ様でした!

これで、無事 Native Module に新しく props を渡して新機能が実装できました 🎉

アルダグラム Tech Blog

Discussion