🦭

React Native で SwiftUI を使ってみよう

2024/04/25に公開

こんにちは!アルダグラムでエンジニアをしている渡辺です

今回は React Native で SwiftUI を使って開発を行う方法を書いていこうと思います

アルダグラムではアプリ開発を React Native を使って開発を行っていますが、新機能開発や既存機能を SwiftUI を使ってリプレイスしたりしています。

iOS エンジニアではなかった僕が Native UI Components を開発するのに1から UIKit を学習するよりも SwiftUI を学習するほうがコストが少なく、そしてより早く開発が行えると思ったことがきっかけで React Native で SwiftUI を使う方針に舵を取りました

当時は iOS エンジニアが1人もいなくて Native UI Components を導入するだけで四苦八苦していたので React Native で SwiftUI を使って開発することをサポートしていないことを知らず辛い日々を過ごしたことを覚えています。

そんな中ある記事を見つけて転機が訪れました。それがこちらです。

この記事をざっくりと要約すると React Native の Native UI Components を開発する機能を用いるが SwiftUI は直接使えないので UIHostingController を用いて SwiftUI を UIKit 経由で利用するということでした。

その過程では複雑な処理が複数出てきますがそちらをサンプルコードを用いて書いていきます。

App.tsx

今回はサンプルコードなので App.tsx に記述しています。

SampleSwiftUIViewType はNativeComponent の型チェックが行えるように定義しています。これはなくても動きます。

NativeSampleSwiftUIView コンポーネントは requireNativeComponent を利用して取得できます。

取得する際に指定する値は manager.m の RCT_EXPORT_MODULE で指定した値を利用します。

あとは React Native と同じ用に利用することができます。

// App.tsx
import React from 'react'
import { requireNativeComponent, NativeSyntheticEvent, ViewStyle } from 'react-native'

type SampleSwiftUIViewType = {
  text: string
  onChangeText: (e: NativeSyntheticEvent<{message: string}>) => void
  style?: ViewStyle
}

function App(): React.JSX.Element {
  const NativeSampleSwiftUIView = requireNativeComponent<
      SampleSwiftUIViewType
      >('SampleSwiftUIView')
  return (
      <NativeSampleSwiftUIView
        text={"HelloWorld"}
        onChangeText={(e) => {
          console.log(e.nativeEvent.message)
        }}
        style={{ flex: 1}}
      />
  )
}

export default App

Manager.m

manager ファイルは React Native から呼び出されるファイルです。

ここでは Native UI Components を表示するために React Native に Proxy で定義した UIView を返しています。

Native UI Components の詳しい概要はこちらを参照ください

// SampleSwiftUIManager.m
#import <Foundation/Foundation.h>
#import "React/RCTViewManager.h"
#import "React/RCTComponentEvent.h"
#import "ReactNativeSwiftUIApp-Bridging-Header.h"
#import "ReactNativeSwiftUIApp-Swift.h"

@interface SampleSwiftUIManager : RCTViewManager
@end

@implementation SampleSwiftUIManager

RCT_EXPORT_MODULE(SampleSwiftUIView)
RCT_EXPORT_SWIFTUI_PROPERTY(text, string, SampleSwiftUIProxy);
RCT_EXPORT_SWIFTUI_CALLBACK(onChangeText, RCTDirectEventBlock, SampleSwiftUIProxy);

- (UIView *)view {
    SampleSwiftUIProxy *proxy = [[SampleSwiftUIProxy alloc] init];
    UIView *view = [proxy view];
    NSMutableDictionary *storage = [OfflineCmListProxy storage];
    storage[[NSValue valueWithNonretainedObject:view]] = proxy;
    return view;
}

@end

Bridging-Header.h

React Native を開発している人には馴染がない拡張子ですが、Objective-C で記述されるファイルです。

このファイルは Objective-C と Swift をつなげる架け橋となる役割を担っています。

このファイルで import することで Swift からObjective-C, Objective-C から Swift のコードを呼び出すことが可能になります。

今回は SwiftUI を利用できるようにしたいので独自のマクロを2つ作成する必要があります

RCT_EXPORT_SWIFTUI_PROPERTY

React Native から渡された Prop を Swift側で受け取れるようにするためのものです

このあと記述する Proxy ファイルに定義されているプロパティに React Native から渡された値を代入しています

RCT_EXPORT_SWIFTUI_CALLBACK

RCT_EXPORT_SWIFTUI_PROPERTY と同じでこちらは callback 関数を受け取れるようにしています

上記2つのマクロが受け取る props は同じで下記の通りです

name: React Native から受け取って SwiftUI で利用するプロパティ名

type: 受け取るプロパティの型(RCT_EXPORT_SWIFTUI_CALLBACK の場合は RCTDirectEventBlockを利用する)

proxyClass: SwiftUI で利用するプロパティが定義された Swift の class

// ReactNativeSwiftUIApp-Bridging-Header.h
#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>
#import "React/RCTComponentData.h"
#import <Foundation/Foundation.h>
#import <React/RCTUIManager.h>
#import <React/RCTBridge.h>

#define RCT_EXPORT_SWIFTUI_PROPERTY(name, type, proxyClass)                                                 \
RCT_REMAP_VIEW_PROPERTY(name, __custom__, type)                                                             \
- (void)set_##name:(id)json forView:(UIView *)view withDefaultView:(UIView *)defaultView RCT_DYNAMIC {      \
  NSMutableDictionary *storage = [proxyClass storage];                                                      \
  proxyClass *proxy = storage[[NSValue valueWithNonretainedObject:view]];                                   \
  BOOL isString = [json isKindOfClass:[NSString class]];                                                    \
  if (isString) {                                                                                           \
    proxy.name = json;                                                                                      \
  } else {                                                                                                  \
    proxy.name = [json type##Value];                                                                        \
  }                                                                                                         \
}                                                                                                           \

#define RCT_EXPORT_SWIFTUI_CALLBACK(name, type, proxyClass)                                                 \
RCT_REMAP_VIEW_PROPERTY(name, __custom__, type)                                                             \
- (void)set_##name:(id)json forView:(UIView *)view withDefaultView:(UIView *)defaultView RCT_DYNAMIC {      \
  NSMutableDictionary *storage = [proxyClass storage];                                                      \
  proxyClass *proxy = storage[[NSValue valueWithNonretainedObject:view]];                                   \
  void (^eventHandler)(NSDictionary *event) = ^(NSDictionary *event) {                                      \
  RCTComponentEvent *componentEvent = [[RCTComponentEvent alloc] initWithName:@""#name                      \
                                                                        viewTag:view.reactTag               \
                                                                           body:event];                     \
    [self.bridge.eventDispatcher sendEvent:componentEvent];                                                 \
  };                                                                                                        \
  proxy.name = eventHandler;                                                                                \
}

Proxy.swift

Proxy ファイルは NSObject で class を定義します。

初期化処理で UIHostingController を利用して SwiftUI のインスタンスを ViewController を生成しています。

生成した ViewController から UIView を取り出し Manager.m 経由で React Native に返しています。

また React Native から渡される Prop も get-set でプロパティで定義して上記で作成したマクロから更新を行えるようにしていて SwiftUI View を初期化する時に Props を渡すことで React Native から渡された値を利用することができます。

// SampleSwiftUIProxy.swift
import Foundation
import SwiftUI

final class RNProps {
    var text: String = ""
    var onChangeText: RCTBubblingEventBlock = { _ in }
}

@objcMembers class SampleSwiftUIProxy: NSObject {
    private var vc: UIHostingController<SampleView>
    static let storage = NSMutableDictionary()
    private let props: RNProps

    override init() {
        props = RNProps()
        let view = SampleView(props: props)
        vc = UIHostingController(rootView: view)
        super.init()
    }
  
    var text: String {
        get { return self.props.text }
        set { self.props.text = newValue }
    }

    var onChangeText: RCTBubblingEventBlock {
        get { return self.props.onChangeText }
        set { self.props.onChangeText = newValue }
    }

    var view: UIView {
        return vc.view
    }
}

SwiftUIView.swift

SwiftUI で実装された View です。

proxy で初期化された時に渡された Props を View に反映しています。

また Button をタップした際のアクションに React Native から渡された onChangeText を実行されるようになっています。

React Native から渡された Callback 関数は RCTDirectEventBlock 型のため引数にディクショナリを渡す必要があります。渡したディクショナリは React Native で受け取ることができるようになっています。

詳しくはこちらを参照ください。

// SampleView
struct SampleView: View {
    private let props: RNProps
    init(props: RNProps) {
      self.props = props
    }
    var body: some View {
        VStack {
            Text(props.text)
          Button {
            props.onChangeText(["message": "イベントを送信しました。"])
          } label: {
            Text("イベント送信")
          }
        }
        
    }
}

ビルド結果

今回は「イベント送信」をタップした時の挙動をコンソールへログ出力をしただけですが、State を利用して値を更新を SwiftUI で行うことなが可能です。

最後に

React Native で SwiftUI の開発には最初の基盤づくりが本当に大変でしたが、今ではこの基盤がなかったら開発が困難であった機能がたくさんありました。

SwiftUI を利用できることで複雑な画面作成も爆速で進められてかつ iOS 標準の API を利用できるという恩恵も得られます。

React Native での開発に限界を感じた時の一つの選択肢としていかがでしょうか?

こちらの記事は英語で書かれていますがわかりやすく1から説明を記述してくれていますのでより詳細な内容を知りたい方は見ていただけたらと思います

もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

アルダグラム Tech Blog

Discussion