SwiftUI と React Native for Web で簡単な画面を作ってコードを比較してみた

2020.06.27

現在開催中の WWDC20 の「SwiftUI」に関するセッションを見ていて「これってReactに近いのでは...」と感じました。そこで実際に SwiftUI と 私が現在業務で使用している React Native for Web + TypeScript を使って簡単な画面を作って比べてみたのでご紹介します。

実装したもの

  • 「+」「ー」ボタンで数値を操作する簡単なカウンター画面
  • SwiftUI版は「Xcode Version 11.4.1」を、React Native for Web版は「Version 0.12.2」を使用しました。
  • パターン1(1つの画面で実装したもの)とパターン2(カウンターを部品化し、2画面に分けたもの)の2つのパターンで実装しました。

パターン1(1つの画面で実装したもの)

SwiftUI版

SwiftUIで使用できるUIコンポーネント(Button、Textなど)を使って「+」「ー」ボタンと、カウンター値を表示するTextを定義しました。また、View内の一時的な状態を管理するための変数として@Stateをつけたプロパティcountを定義し、「+」「ー」ボタンのタップ時に値を更新しています。

SimpleCounter.swift

import SwiftUI

struct SimpleCounter: View {

    @State private var count = 0

    var body: some View {
        HStack(spacing: 25) {
            Button("ー") {
                if (self.count > 0) {
                    self.count -= 1
                }
            }
            .font(.largeTitle)
            .foregroundColor(.black)

            Text("カウント: \(self.count)")
                .font(.title)

            Button("+") {
                self.count += 1
            }
            .font(.largeTitle)
            .foregroundColor(.black)
        }
    }
}

実行結果は以下のようになりました。

React Native for Web + TypeScript版

こちらはReact Native for Webで用意されているコンポーネント(View、Text、TouchableOpacityなど)を使って画面を定義しました。先ほどのSwiftUI版で@Stateをつけて宣言していた変数と同じ役割をするものが、React.useState()を使って宣言した変数になります。これは[count, setCount]とセットで定義する変数で、count現在の値を保持し、setCountを使って値の更新を行います。また、useState(0)の0は初期値です。

SimpleCounter.tsx

import React from 'react'
import { Text, TouchableOpacity, StyleSheet, View } from 'react-native'

const SimpleCounter = () => {

  const [count, setCount] = React.useState(0)

  return (
    <View style={styles.counter}>
      <TouchableOpacity 
        onPress={() => { if (count > 0) { setCount(count - 1) } }}
      >
        <Text style={styles.text}>ー</Text>
      </TouchableOpacity>

      <Text style={styles.text}>{`カウント: ${count}`}</Text>

      <TouchableOpacity onPress={() => setCount(count + 1)}>
        <Text style={styles.text}>+</Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  counter: {
    marginTop: 300,
    flexDirection: 'row',
    alignItems: 'stretch',
    justifyContent: 'center',
  },
  text: {
    fontSize: 24,
    marginHorizontal: 8
  }
})

export default SimpleCounter

こちらの実行結果は以下の通りとなり、SwiftUI版とほぼ同じUIとなっています。

パターン2(カウンターを部品化し、2画面に分けたもの)

SwiftUI版

パターン1では@Stateで定義したプロパティcountを「+」「ー」ボタンタップ時に更新していましたが、パターン2では、カウンターの箇所を部品化し、親の画面から値を受け取って表示・更新するようにしています。親の画面から値を受け取るための変数が@Bindingをつけたカウンター側にあるプロパティcountとなります。@Bindingをつけることで、親の画面から受け取った値を読み書きできるようになります。

CounterScreen.swift

import SwiftUI

struct CounterScreen: View {

    @State private var count = 0

    var body: some View {
        Counter(count: $count)
    }
}

struct Counter: View {

    @Binding var count: Int

    var body: some View {
        HStack(spacing: 25) {
            Button("ー") {
                if (self.count > 0) {
                    self.count -= 1
                }
            }
            .font(.largeTitle)
            .foregroundColor(.black)

            Text("カウント: \(self.count)")
                .font(.title)            

            Button("+") {
                self.count += 1
            }
            .font(.largeTitle)
            .foregroundColor(.black)
        }
    }
}

実行結果は、パターン1と同じになります。

React Native for Web + TypeScript版

こちらのReact Native for Web版ではSwiftUI版で@Bindingを使っていた箇所をpropsという仕組みを使って実現します。カウンター側は、親画面で定義したcount(カウンター値の読み取り用変数), setCount(カウンター値を更新するための関数)を、propsを使って受け取ります。そして、props.countを使ってカウンター値の表示をし、props.setCount()を使って値の更新を行ないます。

CounterScreen.tsx

import React from 'react'
import { Text, TouchableOpacity, StyleSheet, View } from 'react-native'

const CounterScreen = () => {

  const [count, setCount] = React.useState(0)

  return (
    <Counter count={count} setCount={setCount} />
  )
}

type CounterProps = {
  count: number
  setCount: (count: number) => void
}

const Counter = (props: CounterProps) => {
  return (
    <View style={styles.counter}>
      <TouchableOpacity 
        onPress={() => { if (count > 0) { props.setCount(count - 1) } }}
      >
        <Text style={styles.text}>ー</Text>
      </TouchableOpacity>

      <Text style={styles.text}>{`カウント: ${props.count}`}</Text>

      <TouchableOpacity onPress={() => props.setCount(count + 1)}>
        <Text style={styles.text}>+</Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  counter: {
    marginTop: 300,
    flexDirection: 'row',
    alignItems: 'stretch',
    justifyContent: 'center',
  },
  text: {
    fontSize: 24,
    marginHorizontal: 8
  }
})

export default CounterScreen

こちらの実行結果も、パターン1と同じです。

まとめ

SwiftUIReact Native for Webを使って簡単な画面を実装した内容についてご紹介しました。それぞれ用意されたコンポーネントを使って画面を組み立てていく点や、画面内の状態を保持する変数の役割・使い方などについて、類似していると感じました。

この記事がどなたかのお役に立てば幸いです。