🎍

React NativeのNative Componentでpropsの変更を検知する(iOS)

2023/01/01に公開

Native Componentを作成した際、JS側でpropsを更新し再レンダリングさせた場合Native Component(iOS)側ではどのようにそれを検知してViewに反映させればいいかMKMapViewを用いて見ていきます。
showsUserLocationというプロパティにBooleanを設定することで自分の青ポチ(?)を表示したり非表示にしたりできます。
このshowsUserLocationをpropsで設定できるようにします。

セットアップ

npx react-native init IosNativeComponentChangedProps  --template react-native-template-typescript

cd IosNativeComponentChangedProps

yarn ios

まずはMKMapViewをJSXで表示できるようにします。

Xcodeでワークスペースを開いて、AppDelegate.mがある階層に以下のファイルを作成します。
最初のswiftファイルを作成するタイミングでBridging-Headerファイルを作成するか聞かれるので作成してください。

  • MapView.swift
  • MapManager.swift
  • MapManager.m

Bridging-Headerを以下のように編集します。

#import <React/RCTViewManager.h>

MapView.swiftを以下のように編集します。

import Foundation
import MapKit
import CoreLocation

class MapView: MKMapView {
  var locationManager = CLLocationManager()
  
  override public init (frame: CGRect) {
    super.init(frame: frame)
    locationManager.requestWhenInUseAuthorization()
    self.showsUserLocation = true
  }
  
  required init?(coder _: NSCoder) {
    fatalError("init(coder:) is not implemented.")
  }
}

MapManager.swiftを以下のように編集します。

import Foundation
import UIKit

@objc(MapManager)
class MapManager: RCTViewManager {
  override func view() -> UIView! {
    return MapView()
  }
  
  override var methodQueue: DispatchQueue! {
    return DispatchQueue.main
  }
  
  override static func requiresMainQueueSetup() -> Bool {
    return true
  }
}

MapManager.mを以下のように編集します。

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

@interface RCT_EXTERN_MODULE(MapManager, RCTViewManager)

@end

info.plistにLocation When In Use Usage Descriptionを追加してください。
これで一旦ネイティブ側はOKです。

App.tsxを以下のように編集します。

import {
  requireNativeComponent,
  StyleSheet,
  View,
  ViewProps,
} from 'react-native';

type MapViewProps = ViewProps;
const NativeMap = requireNativeComponent<MapViewProps>('Map');

const App = () => {
  return (
    <View>
      <NativeMap style={styles.map} />
    </View>
  );
};

const styles = StyleSheet.create({
  map: {
    width: '100%',
    height: '100%',
  },
});

export default App;

これで再度ビルドしてください。アプリが起動するとマップが表示されていて、位置情報の使用を許可すると自分の青いマークが表示されていると思います。

propsを渡す

<NativeMap />にpropsを渡せるようにします。

ネイティブ側では、デフォルトは自分の青マークは非表示にするのでMapView.swiftの
self.showsUserLocation = trueは削除します。
代わりにshowUserLocationPointプロパティを追加します。このshowUserLocationPointをJS側からporpsで渡せるようにします。

MapView.swiftを以下のように編集してください。

import Foundation
import MapKit
import CoreLocation

class MapView: MKMapView {
  var locationManager = CLLocationManager()
  
  @objc var showUserLocationPoint = false // 追加
  
  override public init (frame: CGRect) {
    super.init(frame: frame)
    locationManager.requestWhenInUseAuthorization()
    // self.showsUserLocation 削除
  }
  
  required init?(coder _: NSCoder) {
    fatalError("init(coder:) is not implemented.")
  }
}

実行ファイルでRCT_EXPORT_VIEW_PROPERTYマクロを使用してpropsとして宣言します。
MapManager.mを以下のように編集してください。

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

@interface RCT_EXTERN_MODULE(MapManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(showUserLocationPoint, BOOL) // 追加
@end

これでporpsとして認識されます。App.tsxを以下のように編集してください。

import React, {useState} from 'react';
import {
  requireNativeComponent,
  StyleSheet,
  TouchableOpacity,
  View,
  ViewProps,
} from 'react-native';

type MapViewProps = ViewProps & {
  showUserLocationPoint: boolean; // 追加
};
const NativeMap = requireNativeComponent<MapViewProps>('Map');

const App = () => {
  // 追加
  const [showUserLocationPoint, setShowUserLocationPoint] = useState(true);

  return (
    <View>
      <NativeMap
        style={styles.map}
        showUserLocationPoint={showUserLocationPoint} // 追加
      />

      {/* 追加  */}
      <TouchableOpacity
        style={styles.button}
        onPress={() => {
          setShowUserLocationPoint(!showUserLocationPoint);
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  map: {
    width: '100%',
    height: '100%',
  },
  button: {
    position: 'absolute',
    bottom: 90,
    backgroundColor: 'pink',
    height: 70,
    width: 70,
    borderRadius: 70,
    alignSelf: 'center',
  },
});

export default App;

propsの変更を検知する

ネイティブ側のプロパティをJS側のporpsで渡すことができるようになりましたが、これでは青マークの表示非表示の変更をすることはできません。

didSetPropsというメソッドをオーバーライドすることで、porpsがセットされた毎に実行したい処理を書くことができます。
changedPropsというArrayのパラメータが渡され、更新されたpropsの名前が格納されています。
MapView.swiftを以下のように編集してください。

import Foundation
import MapKit
import CoreLocation

class MapView: MKMapView {
  var locationManager = CLLocationManager()
  
  @objc var showUserLocationPoint = false
  
  // didSetPropsの追加
  override func didSetProps(_ changedProps: [String]!) {
    // 更新されたかどうかの判定
    let shouldReconfigureUserLocationVisible = changedProps.contains("showUserLocationPoint")
    
    if shouldReconfigureUserLocationVisible {
      // ネイティブ側のプロパティの更新
      configureUserLocationVisible()
    }
  }
  
  func configureUserLocationVisible() {
    self.showsUserLocation = showUserLocationPoint
  }
  
  override public init (frame: CGRect) {
    super.init(frame: frame)
    locationManager.requestWhenInUseAuthorization()
  }
  
  required init?(coder _: NSCoder) {
    fatalError("init(coder:) is not implemented.")
  }
}

これでSwiftで作成したクラスのプロパティをJS側からのpropsで更新させることができました!

Discussion