dely Tech Blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

宣言的UIフレームワーク 「SwiftUI」と「Flutter」を比較してみた

こんにちは!クラシルiOSアプリを開発しているknchstです。

昨今のモバイルアプリケーション開発では様々な要件があり、それらを満たすよう実装するには数々の苦難がありました。その一つとしてUIの状態、所謂State管理が難しくなってきています。ネットワークに接続し、またUIをアニメーションさせたりと、データとUIを同期するのは困難を極めます。

Rxなどのリアクティブフレームワークの利用が当たり前になり、ReactNaviteやFlutterなどのフレームワークをプロダクトに採用する企業も増えてきて、モバイルアプリのトレンドの風も、まさにこの方向に向かって吹き始めていました。

そして今年のWWDCでAppleがSwiftUIを発表してついに、その風は大きくなり今後のモバイルアプリの方向性を決定付けたと言っても過言ではないでしょうか。

今回は、SwiftUIとFlutterでアプリを開発する際の実装の違いをサンプルアプリを通して比較していきたいと思います。

概要

SwiftUIとは

SwiftUIは今年のWWDC2019でサプライズ発表された「宣言型UIフレームワーク」です。

特徴としては

  • iOS, iPadOS, macOS, watchOS, tvOSなどのすべてのAppleプラットフォーム向けに開発することができる
  • 宣言型シンタックス(Swift)
  • Live Preview
  • デザインツール

Flutterとは

FlutterはGoogleが2年ほど前にベータ版としてリリースされたフレームワークになります。クロスプラットフォームなアプリの開発フレームワークとして最近は様々な企業が導入しています。

特徴としては

  • 宣言的なシンタックス(Dart)
  • iOS, Android, Web に対応したクロスプラットフォーム
  • ネイティブパフォーマンス
  • 豊富な標準コンポーネント(Widget)
  • Hot Reload
  • Pluginによる高い拡張性

比較してみる

SwiftUIとFlutterの比較はSwiftUIの公式チュートリアルでも、紹介されていた Landmarks というアプリで行います。Flutterで同じ Landmarks を再現したものがあるので、それを用いて比較していきます。

ソースコードはそれぞれ以下から取得できます。

開発環境

  • SwiftUI - Xcode

SwiftUIの開発環境でも、Appleのプラットフォームのアプリ開発を長年支えてきたXcodeが担当します。SwiftUIの開発環境にはコードの変更がリアルタイムにプレビューされる機能(画像右)があり、iOSなどの様々な機能(ダークモード、アクセシビリティなど)もシミュレーションすることができます。


  • Flutter - Android Studio / Visual Studio Code

Flutterの開発環境はAndroid開発でもおなじみのAndroid StudioまたはMicrosoftが開発したコードエディタのVisual Studio Codeで開発することができます。



XcodeにはSwiftUI向けにデザインツールが提供されており、GUIでコンポーネントを追加したりプロパティを変更することができ、Flutterの開発環境よりも強力です。

UI

  • SwiftUI - View

ViewはProtocolとして定義されており、Viewを定義するにはこれに準拠し、bodyプロパティを実装する必要があります。bodyプロパティでコンポーネントを返すことでそのViewが描画されます。

以下のサンプルコードでは、Hello Worldと表示するTextコンポーネントを返しています。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}


  • Flutter - Widget

FlutterのUIコンポーネントはWidgetクラスを継承しています。

Flutter で用意されている Widget にはStateを持たないStatelessWidget と Stateを持つStatefulWidget があります。

以下のサンプルコードでは中央にHello Worldと表示するTextコンポーネントを使ったことになります。

import 'package:flutter/material.dart';

class ContentView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text("Hello World"),
    );
  }
}



Layout

SwiftUIとFlutterのレイアウト方法は大きく違いはありません。それぞれX, Y, Zの方向にレイアウトをすることができます。

Landmarksサンプルアプリでそれぞれの実装の違いを見ていきます。

LandmarksアプリのホームはリストのUIになっていて、各行は水平レイアウトで構築されています。

SwiftUIはHStackを利用し、その中にImage, Text, Spacerなどのコンポーネントを配置していきます。

FultterはRowを利用し、同じくコンポーネント配置していきます。

それぞれサンプルコードは以下になります。

  • SwiftUI
HStack {
    landmark.image
        .resizable()
        .frame(width: 50, height: 50)
    Text(verbatim: landmark.name)
    Spacer()

    if landmark.isFavorite {
        Image(systemName: "star.fill")
            .imageScale(.medium)
            .foregroundColor(.yellow)
    }
}


  • Flutter
Row(
  children: <Widget>[
    Image.asset(
      'assets/${landmark.imageName}.jpg',
      width: 50.0
    ),
    SizedBox(
      width: 16,
    ),
    Text(
      landmark.name,
      style: TextStyle(fontSize: 16),
    ),
    Expanded(
      child: Container(),
    ),
    landmark.isFavorite ? StarButton(isFavorite: landmark.isFavorite) : Container(),
    Icon(
      Icons.arrow_forward_ios,
      size: 15.0,
      color: const Color(0xFFD3D3D3),
    ),
  ]
)


SwiftUIにはデフォルトでXcodeがマージンなどを設定してくれる分コードの記述が減りシンプルに見えますが、Flutterのレイアウトに関する命名の方がより直感的でわかりやすいように感じました。


List

UIKitで利用していた、UITableViewはSwiftUIではListになります。

Flutterでは、複数の選択肢が提供されています。ListViewを使用して複数行のコンテンツを表示したり、SingleChildScrollViewを使用してスクロール可能なコンテンツを表示することができます。

LandmarksアプリではホームはリストのViewになります。 SwiftUIではForEachを使用してこのリストの要素を作成し、FlutterリストはSliverListを使用します。 両方のコードは次のとおりです。

  • SwiftUI
List {
    ForEach(landmarks) { landmark in
        LandmarkRow(landmark: landmark)
    }
}


  • Flutter
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      final landmark = landmarks[index];
      return LandmarkCell(
        landmark: landmark,
      );
    },
    childCount: landmarks.length,
  ),
)

SwiftUIのNavigationはNavigationViewとNavigationLinkで実現します。

対してFlutterはRouteとNavigatorを使います。以下はそれぞれのサンプルコードです。

  • SwiftUI
NavigationView { // NavigationViewでラップする
    List {
        ForEach(userData.landmarks) { landmark in
            NavigationLink(
                destination: LandmarkDetail(landmark: landmark) // 遷移先を指定
            ) {
                LandmarkRow(landmark: landmark)
            }
        }
    }
    .navigationBarTitle(Text("Landmarks")) // タイトルバーを設置
}


  • Flutter
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      final landmark = landmarks[index];
      return LandmarkCell(
        landmark: landmark,
        onTap: () { // セルにタップイベント
          Navigator.push(
            context,
            Route(
              builder: (context) => LandmarkDetail(
                landmark: landmark,
              ),
            ),
          );
        },
      );
    },
    childCount: landmarks.length
  )
)


Flutterはタップのイベントに対してNavigationを記述しないといけませんが、SwiftUIでは遷移先のLinkを設定します。

State Management

Stateの管理は宣言型UIでもっとも大事な要素の一つです。

Landmarksアプリ内にお気に入り追加したlandmarkを絞り込むスイッチがあります。

SwiftUIとFlutterのStateの管理方法は別々のアプローチを取っています。 SwiftUIはStatefulなデータをUIにバインドさせます。 一方でFlutterはデータが更新された後、setStateメソッドで呼び出すことWidgetを更新しています。

  • SwiftUI
struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

@Stateが付いているプロパティshowFavoritesOnlyがStateを管理しています。 $マークをプロパティの前につけることによって UIにデータをバインドさせています。 showFavoritesOnlyの変更に合わせてバインドされているUIが変わり、UIが変わるとshowFavoritesOnlyの値も変わります。


  • Flutter
CupertinoSwitch(
  value: _showFavoritesOnly,
  onChanged: (state) {
    setState(() {
      _showFavoritesOnly = state;
    });
  },
)

CupertinoSwitchはスイッチボタンであり、値が変更されるとonChangedメソッドが呼び出され、次にsetStateメソッドを呼び出して変数showFavoritesOnlyの新しい値を設定し、UIを更新します。


まとめ

サンプルアプリを見ながらSwiftUIとFlutterの実装の比較をしてきました。もちろん現実のアプリは様々な要件があり、サンプルアプリよりも多くの比較対象があるかと思います。

今回紹介した宣言型UIに基づいた2つのフレームワークは多くの類似点がある印象でした。 とはいえ、それぞれが独自の思想と機能を持っているので要件に合わせて使い分ける必要もあるかと思います。サンプルアプリ程度ではわからない「つらみ」などもあると思うので、今後チャンスを伺ってプロダクトに導入していきたいです。


delyでは新しいメンバーを積極的に募集しています!
もしご興味があればご応募・ご連絡ください!

speakerdeck.com