Help us understand the problem. What is going on with this article?

React NativeのiOSネイティブをSwiftで書く

More than 1 year has passed since last update.

この記事の対象

この記事は、

  • ReactNativeアプリを作っていて
  • iOS側でネイティブを実装する必要があり
  • かつそれをSwiftで書きたいんだけど
  • Swiftもネイティブのブリッジの仕方も分からない!

という方が対象です。

かく言う私がそうでして、ここで書かれているコードは動くものの、間違いはあるかもしれませんのでお気をつけください!!

(私の場合、とあるサービスの実装ガイドがSwift版しかなく、jsでの実装が難しそうだったので泣く泣くSwiftでネイティブを実装しました。。。)

この記事で分かるようになる(と願う)こと

数を増やしたり減らしたりするカウンターアプリを通して、以下3つを共有できればと思います。

  1. ネイティブ側でSwift使うのに必要な最低限のファイル3つ(.swift, .m, .h)の最低限の書き方
  2. ネイティブ側からjs側への値の渡し方(コールバックプロミスの2パターン)
  3. js側で値を受け取り方(同様にコールバックとプロミスの2パターン)

この記事で説明しないこと

  • ネイティブ実装の諸々の詳細(使える型とか、その他のパターンとか)
  • React Nativeのプロジェクトの作り方
  • React Nativeのjs部分の書き方(stateの概念とか更新の仕方とか)
  • Androidのネイティブ実装

手っ取り早くソース見たい方は

GitHubに上げてるのでこちらをご覧ください。

なお、この記事は以下の記事の簡易版なので、詳細はそちらを参考にしてください。

環境

    System:
      OS: macOS 10.14
    Binaries:
      Node: 10.5.0 - ~/.nodebrew/current/bin/node
      Yarn: 1.12.3 - /usr/local/bin/yarn
      npm: 6.1.0 - ~/.nodebrew/current/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    SDKs:
      iOS SDK:
        Platforms: iOS 12.1, macOS 10.14, tvOS 12.1, watchOS 5.1
    IDEs:
      Xcode: 10.1/10B61 - /usr/bin/xcodebuild
    npmPackages:
      react: 16.6.1 => 16.6.1 
      react-native: 0.57.7 => 0.57.7 
    npmGlobalPackages:
      create-react-native-app: 1.0.0
      react-native-cli: 2.0.1
      react-native-git-upgrade: 0.2.7

あとSwiftは3.0に指定しています。

この記事でやること

簡単なカウンターアプリの作り方の紹介を通じてネイティブ実装の方法を共有します。

swiftsample.gif

機能は以下3つです。

1. 数を1増やす (Increment)
2. 数を1減らす (Decrement)
3. 数が0未満になる時にはエラーメッセージを表示する

すごく単純ですね。

js側ではstateで現在の値(初期値は0)を持っていて、その現在値をネイティブ側に渡し、swiftコードで足したり引いたりした値をjs側で受け取って、stateを更新し画面を再描画します。

js(現在の値) -> swift(計算) -> js(計算後の値)

また、ネイティブ側からjs側に値を渡す時ですが、

  • Incrementの時はコールバック
  • Decrementの時はプロミス

を使っています。

それでは、以下の順に見ていきます。

  1. ネイティブ側で必要な最低限のファイル3つ(.swift, .m, .h)の最低限の書き方
  2. ネイティブ側からjs側への値の渡し方(コールバックプロミスの2パターン)
  3. js側で値を受け取り方(同様にコールバックとプロミスの2パターン)

ネイティブ側で必要な最低限のファイル3つ

Swiftでネイティブコードを書くには、最低限以下三つのファイルが必要です。

  1. Swiftファイル(.swift)
  2. Objective-C Bridging Headerファイル(.h)
  3. Objective-C Macrosファイル(.m)

Macrosがなんなのかとか、私は全然分かっておりません。。

1. Swiftファイルを作る

  1. ReactNativeプロジェクトのiOSディレクトリを、Xcodeで開く
  2. AppDelegete.hなどがある階層に、New Fileで新規ファイルを作る(Swift File を選択)

※ この時に「Objective-C bridging headerを作りますか?」的なことを聞かれたら作成してください。

SampleProject
 ∟ SampleProject <- ここのディレクトリにSwiftファイルを新規追加
    ∟ Counter.swift <- 追加したSwiftファイル
 ∟ Libraries
 ∟

以上!

2. Objective-C Bridging Headerファイルを作る

Swiftファイルをjs側にブリッジするために、 Objective-C Bridging Headerファイルというのが必要なようで。SwiftだけじゃなくObjective-Cも書かないといけないんすね。

ここのステップは、先ほどSwiftファイルを作った段階で「「Objective-C bridging headerを作りますか?」で作成した方はもう完了です。

ただ、私の場合は聞かれないこともありました。その場合は、自分で作る必要があります。

まずファイルを作る前に、ファイル名を確認します。

基本的には、

プロジェクト名-Bridging-Header.h

です。

SampleProject-Bridging-Header.h

など。

TargetのBuild SettingsからもObjective-C Bridging Headerという項目でファイル名を確認できます。

スクリーンショット 2018-11-30 23.05.39.jpeg

上記で確認したファイル名で、新規ファイルを作成します。
ファイル作成時にはHeader Fileを選択します。

SampleProject
 ∟ SampleProject
    ∟ Counter.swift 
    ∟ SampleProject-Bridging-Header.h <- 追加したヘッダーファイル
 ∟ Libraries
 ∟

完了!

3. Objective-C Macrosファイルを作る

これまでと同様にして新規ファイルを作ります。
今度は、Objective-C Fileを選択してください。

SampleProject
 ∟ SampleProject
    ∟ Counter.swift 
    ∟ SampleProject-Bridging-Header.h
    ∟ Counter.m <- 追加したObj-Cファイル
 ∟ Libraries
 ∟

完了!

ネイティブ側からjs側への値の渡し方

ここからが本題。

ネイティブ側を書いていきます。

以下の順番で見ていきます。

Swift
1. Counterクラスの用意
2. Incrementメソッド実装(コールバック使う)
3. Decrementメソッド実装(プロミス使う)

Objective-C(.m)
4. Counterクラスのエクスポート
5. Incrementメソッドのエクスポート
6. Decrementメソッドのエクスポート

ブリッジヘッダー(.h)
7. ブリッジを完成させる

1. Counterクラスの用意

Swiftファイル

import Foundation

@objc(Counter)
class Counter: NSObject {

}

Swiftの書き方分からないので、おまじないだと思ってください。。
Objective-Cに読み取ってもらうためには@objc が大事だそうです。

2. Incrementメソッド実装(コールバック使う)

SwiftファイルのCounterクラス内に以下を追加します。

 @objc
  func increment(_ origin: Int, withCallback callback: RCTResponseSenderBlock) -> Void {
    callback([origin + 1])
  }

入力値に1を加えた数を、コールバックで返すようにしています。

簡単に説明すると、

origin: Int
=>引数1で計算元の数値。

withCallback callback: RCTResponseSenderBlock
=>引数2でコールバック。 withCallbackは呼び出し元に表示される引数名で、 callbackがメソッド内で使う引数名。

callback([origin + 1])
=>計算元に1加えた数を引数に、コールバックを呼び出し。
コールバック呼び出し時の引数は、引数が一個でも配列にする

また、このメソッド自体は返り値がないです。
以下みたいに普通にメソッドの返り値使えばいいじゃんって私は思ったのですが、ReactNativeのブリッジではそれはNGのようです。Androidも同じです。なので、js側に値を渡すために、コールバックかプロミスを使っています(他にも方法あるのかもしれませんが)。

 @objc
  func increment(_ origin: Int) -> Int {
    return origin + 1
  }

3. Decrementメソッド実装(プロミス使う)

同様に以下を追加します。

  @objc
  func decrement(_ origin: Int, withResolve resolve: RCTPromiseResolveBlock, withReject reject: RCTPromiseRejectBlock) -> Void{

    if(origin <= 0){
      reject("E_COUNT", "You can't decrement any more!!", nil)
      return
    }

    resolve(origin - 1)

  }

計算元の数値が0以下の場合にはエラーを返すようにしています。
それ以外の場合には、計算元から1引いた数をプロミスの渡し手に返します(今回は配列じゃないです)。

エラーハンドリングはよくわかってないので誤魔化しています(nilの部分とか)。

プロミスって何よっていうのは、私にはうまく説明することができないので、ググってください。。

これでSwiftファイルは完成です。

全体はこちらをご確認ください。

補足

上記ファイルには

 static func requiresMainQueueSetup() -> Bool {
    return false
  }

というのがあります。
これは、「requiresMainQueueSetupを実行しろ!」という警告が出るのを防ぐためです。非同期でもいい場合は falseを返すそうです。詳細は分かりません。すみません。

4. Counterクラスのエクスポート

ここから、Objective-Cファイル(Counter.m)をいじっていきます。

先ほどのCounter.swiftのCounterクラスをjs側にエクスポートするには、

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

@interface RCT_EXTERN_MODULE(Counter, NSObject)

@end

と書きます。

Counter(Swiftのやつ)をエクスポートします、これはオブジェクトです、という感じです。
Counter.swift

@objc(Counter)
class Counter: NSObject {
}

としましたが、 この@objc(Counter) の部分と紐づいているみたいです(ただの予想です。。)

5. Incrementメソッドのエクスポート

Swiftクラスのincrementメソッドをエクスポートします。

@interface RCT_EXTERN_MODULE(Counter, NSObject)

//↓を追加
RCT_EXTERN_METHOD(increment: (NSInteger)origin withCallback:(RCTResponseSenderBlock)callback)

@end

6. Decrementメソッドのエクスポート

Swiftクラスのdecrementメソッドをエクスポートします。

@interface RCT_EXTERN_MODULE(Counter, NSObject)
RCT_EXTERN_METHOD(increment: (NSInteger)origin withCallback:(RCTResponseSenderBlock)callback)

// ↓を追加
RCT_EXTERN_METHOD(decrement: (NSInteger)origin withResolve:(RCTPromiseResolveBlock)resolve withReject:(RCTPromiseRejectBlock)reject)

@end

特に説明なくてすみません。
引数とかは、Swiftファイルと見比べてください。

7. ブリッジを完成させる

Native側の最後は、Bridging-Headerファイルです。

#import "React/RCTBridgeModule.h"

はい、この一行で大丈夫です。

js側での値を受け取り方

さぁ、ようやくjs側まで来ました。

App.jsクラスをいじります。

レイアウトとかそこらへんは触れません。

前提
初期stateは以下にします。

  constructor(props) {
    super(props)

    this.state = {
      count: 0,
      errorText: ""
    }

  }

1. NativeModulesをimportする

import {
  Platform,
  StyleSheet,
  Text,
  View,
  //↓↓↓
  NativeModules,
  Button
} from "react-native"

2. incrementメソッドを呼び出して値を受け取る

  onPressIncrement = () => {
    const {count} = this.state

    const callBack = incrementedCount => {
      this.setState({count: incrementedCount, errorText: ""})
    }

    NativeModules.Counter.increment(count, callBack)
  }

コールバックを定義し、今の値とコールバックを引数にしてNativeModules.Counter.incrementを呼び出します。
incrementedCountが、swiftのincrementメソッド内で1足された数値です。

3. dncrementメソッドを呼び出して値を受け取る

  onPressDecrement = () => {
    const {count} = this.state

    NativeModules.Counter.decrement(count)
      .then(decrementedCount => this.setState({count: decrementedCount}))
      .catch(error => {
        this.setState({errorText: error.message})
      })

  }

または async/awaitを使うのであれば以下のようにも書けます。

  onPressDecrement = async () => {
    const {count} = this.state

    const decrementedCount = await NativeModules.Counter.decrement(count).catch(error =>
      this.setState({errorText: error.message})
    )

    if (decrementedCount) {
      this.setState({count: decrementedCount})
    }
  }

ネイティブの方では以下のように引数3つですが、js側では decrement(count)と1つだけ。不思議ですが、プロミスの場合はよろしくやってくれるようです。

  @objc
  // 引数3つ
  func decrement(_ origin: Int, withResolve resolve: RCTPromiseResolveBlock, withReject reject: RCTPromiseRejectBlock) -> Void{
   ///
  }
// 引数1つ
NativeModules.Counter.decrement(count)

さて、後半スピードアップした、または説明を面倒がりましたが、Swiftを使ってのネイティブ実装は以上です。

改めて、ソース全体はこちらでご確認ください。

参考サイト

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away