Edited at

React NativeでObjective-C/SwiftのAPIを扱う(Native Component編)

More than 1 year has passed since last update.


この記事の目的

React NativeでもNativeのUIを触りたいときってありますよね。例えばこんなやつ(GradientView)を扱いたいとか。

(ちなみに、今回参考にした記事はこちら→http://browniefed.com/blog/react-native-how-to-bridge-a-swift-view/

そこに対してもReact Nativeでサポートしているのですが、案の定公式の説明やコード例が十分でない&日本語の資料もなさそうなのでまとめてみました。

※ 公式が不十分でソースコードを読みつつの理解となるため、正しくない内容になってしまうかもしれない点をご了承ください。(そもそも正解がない)

※ 筆者はObjective-C/Swift初心者な点もご了承ください。

※ Android版はこちらをどうぞ→http://qiita.com/uryyyyyyy/items/012009524c126d4888d8

※ ソースコードはこちら→https://github.com/uryyyyyyy/RNBindingSample/tree/ios-native-component


ゴール


  • React Nativeから、既に定義された独自クラス(UIViewを継承しているもの)を表示できる

  • プロパティ値などを設定できる

  • イベントの発火をjs側で扱える

擬似コードで書くとこんな感じが理想。


function _onChange(data) {
//use `data` object
}

<CustomUIView
style={styles.hoge}
value={this.props.value}
onChange={_onChange}
/>


作り方


  1. React Native化したいUIView(今回はUIDatePicker)を継承して、propsで入れたいプロパティ値とその使われ方を定義した独自クラスを作る。

  2. RCTViewManagerを継承したラッパーを用意し、そこで用意されたマクロで紐付けることでJSから呼べるようにする。

  3. JS側で、上記独自UIViewをJSで扱うためのUtilityクラスを定義する。

という流れです。簡単ですね!

マクロを使うことで、既存のViewライブラリも簡単に使えるようになるのがReact Nativeの良いところです。

(なお深くは追ってないですが、Managerというクラスがあることで、Reactの差分描画アルゴリズムを機能させているものと思います。)


試しに作ってみた(Objective-C)

公式にある例がわかりにくかったので、ここでは、UIKitに備わっているUIDatePickerを例に上げます。


ゴール


  • React NativeからUIDatePickerを表示できる

  • UIDatePickerが表示する値を設定できる

  • UIDatePickerの値が変わったらイベントが飛び、別で用意したtextの表示が変わる

画面イメージはこんな感じです。普通ですね。。


とりあえずロジック抜きでUIDatePickerを呼び出してみる

まずは一番簡単な実装を見ていきます。

UIDatePickerはそのまま使って、Manager部分だけ作る方式です。


MyDatePickerManager.h

//①

#import "RCTViewManager.h"

//②
@interface MyDatePickerManager : RCTViewManager

@end


MyDatePickerManagerはRCTViewManagerを継承するというのを②で定義して、①でそれ用のクラスをimportするだけです。


MyDatePickerManager.m

#import "MyDatePickerManager.h"


@implementation MyDatePickerManager

//①
RCT_EXPORT_MODULE(MyDatePicker)

//②
- (UIView *)view {
return [[UIDatePicker alloc] init];
}

//③
RCT_EXPORT_VIEW_PROPERTY(date, NSDate)

//④
RCT_CUSTOM_VIEW_PROPERTY(mode, UIDatePickerMode, UIDatePicker){
UIDatePickerMode pickerMode = UIDatePickerModeDateAndTime;
view.datePickerMode = pickerMode;
}

@end


①で、このコンポーネントはJSからMyDatePicker という名前で呼ばれるようにしました。

②は、呼び出したいViewクラス(ここでは素のUIDatePicker)を初期化しています。これは必須のようです。

③ここでは、NSDateのdateというプロパティを、JS側でpropsから渡ってくる値の一つとして受け取るよ、ということになります。(JSコードは後述します。)

なお、NSDateはJS側でのnumber(unixTime)などから自動でconvertされます。

④では、datePickerModeを指定しています。(日付だけ、時間だけ、日時ともに、などの表示方式をここで選べます。)

ここで扱いたいUIDatePickerModeは、JSオブジェクトからは自動でconvertできないため、RCT_CUSTOM_VIEW_PROPERTYというマクロ定義の中で必要な変換を掛けれることで対応します。

まずは簡単な例ということでUIDatePickerModeDateAndTimeをべた書きしましたが、後でちゃんと書きます。

最後にJSコードを書きます。


MyDatePicker.ios.js

//@flow

import React, { Component } from 'react';
import requireNativeComponent from 'requireNativeComponent';
const RCTDatePickerIOS = requireNativeComponent('MyDatePicker');

export default class DatePickerIOS extends React.Component {
render() {
return (
<RCTDatePickerIOS
style={this.props.style}
date={this.props.date.getTime()}
mode={this.props.mode}
/>
);
}
}



index.ios.js

//@flow

import React, {Component} from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import DatePickerIOS from './MyDatePicker';

export default class DatePickerExample extends React.Component {
state = {date: new Date()};

render() {
return (
<View>
<DatePickerIOS
style={{height: 200}}
date={this.state.date}
mode="datetime"
/>
<Text>
{this.state.date.toString()}
</Text>
</View>
);
}
}

AppRegistry.registerComponent('RNBindingSample', () => DatePickerExample);


これで、アプリを立ち上げるとDatePickerが現在日付で表示されるはずです。


イベントを仕込んでみる

上記だと値が変わったときのイベントを拾えない&datetimeの表示しかできない、のでもう少し作り込みます。

まず、素のUIDatePickerではイベントリスナを貼り付けてないので、そこから直す必要があります。UIDatePickerを継承した独自クラスを定義しましょう

(これが書ければ、他の独自クラスでも同じように展開できるはずですよ。)


MyDatePicker.h

#import <UIKit/UIKit.h>

//①
#import "UIView+React.h"

@interface MyDatePicker : UIDatePicker

//②
@property (nonatomic, copy) RCTBubblingEventBlock onMyChange;

@end


UIDatePickerを継承したMyDatePickerを作ります。このとき、プロパティにonMyChange(名前適当)を付けます。これは後ほどManagerクラスの方でセットされるのですが、EventEmitterみたいなものと思ってもらえばいいかと。


MyDatePicker.m

#import "MyDatePicker.h"


@implementation MyDatePicker

//①
- (instancetype)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self addTarget:self action:@selector(didChange:)
forControlEvents:UIControlEventValueChanged];
}
return self;
}

//②
- (void)didChange:(MyDatePicker*)sender {
if (sender.onMyChange) {
//③
sender.onMyChange(@{ @"changedDate": @(self.date.timeIntervalSince1970 * 1000.0) });
}
}

@end


ここでは、独自Viewでイベントを扱えるようにしています。

まず、②の中で、didChangeが呼ばれたらonMyChangeがNSDictionary(JS側では{changedDate: unixTime}みたいなJSON)のオブジェクトを返すように書きます。

そして、①のinitWithFrameの中でaddTargetでイベントハンドラを設定しています。

(余談ですがこの記事ではstyleの説明は出てきませんが、iOSの場合はpropsにstyleを指定すると、どこかで描画領域が確保され、initWithFrameで渡してくれるっぽいです。つまり実装者が気にしなくても適切な描画領域をReact Native側で計算してくれるというわけです。流石ですね。)

Managerクラスも見ていきましょう。


MyDatePickerManager.m

#import "MyDatePickerManager.h"


#import "RCTBridge.h"
#import "MyDatePicker.h"

@implementation MyDatePickerManager

RCT_EXPORT_MODULE(MyDatePicker)

- (UIView *)view {
return [[MyDatePicker alloc] init];
}

//①
- (UIDatePickerMode) myAction:(NSString *)modeStr {
if([modeStr isEqualToString:@"time"]){
return UIDatePickerModeTime;
}else if([modeStr isEqualToString:@"date"]){
return UIDatePickerModeDate;
}else { //datetime
return UIDatePickerModeDateAndTime;
}
}

RCT_EXPORT_VIEW_PROPERTY(date, NSDate)

//②
RCT_EXPORT_VIEW_PROPERTY(onMyChange, RCTBubblingEventBlock)

//③
RCT_CUSTOM_VIEW_PROPERTY(mode, UIDatePickerMode, UIDatePicker){
NSString *modeStr = json;
UIDatePickerMode pickerMode = [self myAction:modeStr];
view.datePickerMode = pickerMode;
}

@end


先程と変わった点は2点です。まず、③のMode選択のロジックが変更され、①のメソッドを用いてpropsの値に応じてmodeが切り替わるようになりました。

また、②でonMyChangeというプロパティにイベントのcallbackが返るようになりました。

ここまででObject-Cの変更は終わりです。最後にJSのutility部分を見ていきます。


MyDatePicker.ios.js

//@flow

import React, { Component } from 'react';
import requireNativeComponent from 'requireNativeComponent';
const RCTDatePickerIOS = requireNativeComponent('MyDatePickerManagerSwift');

export default class DatePickerIOS extends React.Component {

_onChange(event) {
this.props.onDateChange(new Date(event.nativeEvent.changedDate));
}

render() {
return (
<RCTDatePickerIOS
style={this.props.style}
date={this.props.date.getTime()}
mode={this.props.mode}
onMyChange={this._onChange.bind(this)}
/>
);
}
}


ここで、 先程イベントを扱うために追加したonMyChangeが追加されているのがわかるかと思います。

これを呼び出すと、上述したgifのようにDatePickerの変更をjsで操作してtextの値を変えると行ったことができるようになります。

ここまでで一旦完成です!お疲れ様でした。


試しに作ってみた(Swift)

え、Swiftでも試したいって?

仕方ない、やりましょう!

手順は先程と同じです。UIViewとManagerをswiftで書いてみます。


headerファイルを用意する

Objective-CプロジェクトでSwiftコードをnewするとXcodeが勝手に作ってくれるはずです。


RNBindingSample-Bridging-Header.h

#import "RCTBridgeModule.h"

#import "RCTViewManager.h"

RCTViewManagerも読み込みます。


Objective-Cファイルを用意する


MyDatePickerSwift.m

#import "RCTViewManager.h"


//①
@interface RCT_EXTERN_MODULE(MyDatePickerManagerSwift, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(date, NSDate)
RCT_EXPORT_VIEW_PROPERTY(onMyChange, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(mode, UIDatePickerMode, UIDatePicker){
NSString *modeStr = json;
UIDatePickerMode pickerMode = [self myAction:modeStr];
view.datePickerMode = pickerMode;
}

- (UIDatePickerMode) myAction:(NSString *)modeStr {
if([modeStr isEqualToString:@"time"]){
return UIDatePickerModeTime;
}else if([modeStr isEqualToString:@"date"]){
return UIDatePickerModeDate;
}else { //datetime
return UIDatePickerModeDateAndTime;
}
}

@end


基本的には上述のものと同じですが、モジュール紐付けが RCT_EXTERN_MODULEになっています。ここでSwiftで書くManagerクラスを指定します。


Managerを作る


MyDatePickerManagerSwift.swift

import Foundation

@objc(MyDatePickerManagerSwift)
class MyDatePickerManagerSwift : RCTViewManager {
override func view() -> UIView! {
return MyDatePickerSwift();
}
}


swiftで書くまでもないですね。。やってることは同じです。React Native感ゼロのSwiftコードです。

(上述のmyActionメソッドもこちらに持ってこれそうですかね。試してないです。。)


UIVewを作る


MyDatePickerSwift.swift

import UIKit

import Foundation

class MyDatePickerSwift : UIDatePicker {

var onMyChange:RCTBubblingEventBlock? = nil

override init(frame: CGRect) {
super.init(frame: frame);
self.addTarget(self, action: #selector(self.changeEvent(sender:)), for: UIControlEvents.valueChanged)
self.frame = frame;
}

func changeEvent(sender: MyDatePickerSwift) {
let time = self.date.timeIntervalSince1970 * 1000.0
let dict: Dictionary = ["changedDate": time];
sender.onMyChange?(dict)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}


こちらもやってることは同じです。特に説明は不要かと


まとめ

どうでしょう。書いてみると意外と簡単だったかと思います。

どんどんReact なちゔぇライブラリを作っていきましょう。