FlutterとSwiftUiの宣言的UI実装比較 -- 某ポータルオマージュ

宣言的UIであるFlutterとSwiftUiの実装を比較してみました。典型的なUIの実装比較では実戦で使えないと思ったので、実際に存在しているアプリのUIをオマージュすることで、より実践的な、実務に近いソースコードを掲載できるようにしました。
まずは、完成画像を見てください。当然のことですが、FlutterとSwiftUi共に某ポータルサイトのUIをほぼ完璧に再現できました。
ただ、できそうでできなかったこともあるので、それは記事のなかでふれます。
![swiftuiflutter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/e5d32b73-2c51-d410-8bd7-8a26d95cd02a.gif)
#完成版
##Flutter
<img width="300" alt="flutter.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/22e4e73f-591a-f6d1-a7c0-31e0abbf7cc6.png">
##SwiftUi
<img width="300" alt="swiftui.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/465fc976-f77a-b925-11a7-1ec255bb9349.png">

それでは順番に実装を比較していきましょう。

#UIの土台
各UI componentを構成するために必要な土台について比較します。共に決まった書き方があります。
まず、Flutterです。
Flutterには *MaterialApp class* が用意されています。全てのappは、このclassのconstructorにthemeやhome等を渡すことから始まります。*theme property* はapp共通のcolorやfontを適用するためのものです。*home property* には最初に表示する画面classを適用します。
*theme property* には *LightTheme.pattern(context)* を適用し、*darkTheme property*には *DarkTheme.pattern(context)* を適用しています。 これはFlutterはiOSのlight modeとdark modeに容易に対応できることを意味します。
*home property* には、 *TopScreen object* を適用し、これが全てのUIの始まりとなります。

```dart:main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App Name',
      theme: LightTheme.pattern(context),
      darkTheme: DarkTheme.pattern(context),
      home: TopScreen(),
    );
  }
}
```
次は、SwiftUiです。
SwiftUiでは *SceneDelegate class* で最初の画面である *TopView object* を生成します。
*TopView object* は *UIWindow class* の *rootViewController property* に適用します。

```swift:SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let topView = TopView()

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: topView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}
```

FlutterとSwiftUi共に決まった記述方法です。Flutterと異なり、SwiftUiは、App共通のcolorやfontを指定することはできません。

次に、画面下部の「ホーム」、「ツール」、「お知らせ」、「その他」の４画面への遷移を行うtabの実装です。
まず、Flutterです。
Flutterには、material design の足場となる、*Scaffold class* が用意されています。*Scaffold class* には *appBar property*、*body property*、*bottomNavigationBar property* があり、これらにWidgetを適用すると画面UIの出来上がります。*appBar property* はアプリの上部になり、画面のtitleやback buttonが配置されます。*body property* はこの画面のmain contentが配置されます。*bottomNavigationBar property* は「ホーム」、「ツール」、「お知らせ」、「その他」の４画面への遷移を行うtabが配置されます。*appBar property* や *bottomNavigationBar property* は画面上部と下部に固定されてしまうので、配置するWidgetには注意が必要です。うっかり input fieldのある画面で *bottomNavigationBar property* にsend buttonを配置してしまうと、keyboardがbuttonと重なってしまい、send buttonが押せないという事態になりかねません。
tab表現は *BottomNavigationBar class* を使用します。*BottomNavigationBar class* には、tab用のWidgetをListにして適用します。tab tapでの画面遷移は *onTap property* で tap eventと何番目のtabがtapされたかの情報を受け取って、この情報で、遷移先の画面をハンドリングします。 *BottomNavigationBar class* を *bottomNavigationBar property* に適用すると画面下部のtab表現を実現できます。

```dart:top_screen.dart
class TopScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return _LayoutWidget();
  }
}

class _LayoutWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: _appBar(),
        body: HomeScreen(),
        bottomNavigationBar: _bottomNavigationBar(context));
  }

  void _tap(BuildContext context, StatelessWidget screen) {
    Navigator.of(context).push(MaterialPageRoute(builder: (context) {
      return screen;
    }));
  }

  AppBar _appBar() {
    return AppBar(
        titleSpacing: 5.0,
        elevation: 0.0,
        title: SearchBar(),
        actions: [
          IconButton(
              icon: Icon(Icons.filter_none, color: Colors.grey),
              onPressed: () => null),
        ],
        backgroundColor: Colors.white,
        bottom: PreferredSize(
            child: Container(
              color: Color(0xffC0C0C0),
              height: 0.4,
            ),
            preferredSize: Size.fromHeight(0.4)));
  }

  Widget _bottomNavigationBar(BuildContext context) {
    return Container(
        decoration: BoxDecoration(color: Colors.transparent, boxShadow: [
          BoxShadow(
              color: Colors.transparent, spreadRadius: 0, blurRadius: 0)
        ]),
        child: SizedBox(
            height: 100,
            child: BottomNavigationBar(
                type: BottomNavigationBarType.fixed,
                iconSize: 30,
                selectedItemColor: Colors.red,
                unselectedItemColor: Colors.grey,
                onTap: (int index) {
                  switch (index) {
                    case 0:
                      _tap(context, HomeScreen());
                      break;
                    case 1:
                      _tap(context, ToolScreen());
                      break;
                    case 2:
                      _tap(context, InformationScreen());
                      break;
                    case 3:
                      _tap(context, OtherScreen());
                      break;
                  }
                },
                items: <BottomNavigationBarItem>[
                  _bottomNavigationBarItem(Icons.home, 'ホーム'),
                  _bottomNavigationBarItem(Icons.view_module, 'ツール'),
                  _bottomNavigationBarItem(Icons.notifications, 'お知らせ'),
                  _bottomNavigationBarItem(Icons.more_horiz, 'その他'),
                ])));
  }

  BottomNavigationBarItem _bottomNavigationBarItem(
      IconData icon, String label) {
    return BottomNavigationBarItem(
      icon: Stack(children: [
        Padding(
          padding: EdgeInsets.only(left: 4, right: 4),
          child: Icon(icon),
        ),
        Positioned(
            right: 0,
            child: Container(
                decoration: BoxDecoration(
                    color: Colors.red, borderRadius: BorderRadius.circular(4)),
                constraints: BoxConstraints(minHeight: 8, minWidth: 8),
                child: Container()))
      ]),
      title: Text(label, style: TextStyle(fontSize: 10)),
    );
  }
}
```

*Scaffold class* の *appBar property* には、*_appBar function* を適用し、 *body property* には、 *HomeScreen instance* を適用します。また、*bottomNavigationBar property* には *_bottomNavigationBarItem function*を適用しています。宣言的UIは階層が深くなりがちなので、可能な限り小分けにして、ソースの見通しを良くしています。*_appBar function* には後述する *SearchBar instance* を配置しています。シンプルに画面上部にWidgetを配置したい時は、*appBar property*を使いましょう。面倒なpaddingやmarginでの微調整が不要になります。

次に、SwiftUiです。
SwiftUiには、Flutterの *Scaffold class* のようなものはありません。 *TabView class* を使って、tab表現を実現します。*TabView class* はtab tapで遷移したい画面classを適用するだけで、画面遷移を実現してくれます。Flutterの*BottomNavigationBar class*のように、tap eventで画面遷移をハンドリングする必要ありません。

```swift:TopView.swift
struct TopView: View {
    var body: some View {
        TabView{
             HomeScreen()
                 .tabItem {
                     VStack {
                         Image(systemName: "house.fill")
                         Text("ホーム")
                     }
             }.tag(1)
             ToolScreen()
                 .tabItem {
                     VStack {
                         Image(systemName: "square.grid.2x2.fill")
                         Text("ツール")
                     }
             }.tag(2)
             InformationScreen()
                 .tabItem {
                     VStack {
                         Image(systemName: "bell.fill")
                         Text("お知らせ")
                     }
             }.tag(3)
             OtherScreen()
                 .tabItem {
                     VStack {
                         Image(systemName: "ellipsis")
                         Text("その他")
                     }
             }.tag(4)
         }
    }
}

struct TopView_Previews: PreviewProvider {
    static var previews: some View {
        TopView()
    }
}
```
SwiftUiでは *TabView class* がある程度UIを整えてくれるので、ソースはシンプルです。Flutterでは、paddingやtextStyleを適用しているためソースが複雑に見えます。あと、SwiftUiの *TabView class* はbackgroundがdefualtで若干透過になっています。オマージュしたポータルアプリもtab部分のbackgroundが若干透過になっていました。なので、SwiftUiの場合は何も考えることなく実現できました。
一方、Flutterの *BottomNavigationBar class* では実現できませんでした。

#SearchBar
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/00a7de20-2508-930b-c0d9-12ed3c033267.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/898a336c-8bbf-8263-dd94-dca5e6dc2092.png">
画面上部の検索枠の実装を比較します。
Flutter、SwiftUi共に、検索枠ようのclass(or struct)として *SearchBar object* を作成しました。Flutterの場合は、これを前述の *Scaffold class* の *appBar property* に適用します。これでだけで画面上部に良い感じに表示してくれます。
SwiftUiでは、*Scaffold class* のように画面UIを構築するためのclassは用意されていないので、普通に、画面の一要素として、適用します。*VStack struct* の先頭に配置することで、画面上部に表示します。
それでは、*SearchBar object* の中身を見ていきましょう。

まず、Flutterです。FlutterではUIをWidgetと表現します。
ソースでは *build function* のreturn値としてWidgetを作成します。 Widgetはたくさん用意されていますが、他のWidgetのベースとなるWidgetが *Container widget* です。私は 頻繁に、*Container widget* を使って、Widget全体のサイズや、背景色を決定します。今回は、*Container widget* の *decoration property* で検索枠の青枠を作成します。また、*width property* には、最大サイズとなる *double.infinity* を適用しました。これにより画面サイズが大きくなっても、そのサイズに合わせて検索枠のサイズを調整できるようになります。

```dart:TopView.dart
class SearchBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 40,
      width: double.infinity,
      decoration: BoxDecoration(
          border: Border.all(color: Color(0xff0066FF), width: 2),
          borderRadius: BorderRadius.circular(5)),
      child: Row(
        children: [
          Expanded(flex: 1, child: Icon(Icons.search, color: Colors.grey)),
          Expanded(
              flex: 6,
              child: Text("xxxxxxx検索",
                  style: TextStyle(color: Colors.grey, fontSize: 14))),
          Expanded(flex: 1, child: Icon(Icons.mic, color: Colors.grey)),
          Expanded(
              flex: 2,
              child: FlatButton(
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.all(Radius.circular(0.0))),
                  color: Color(0xff0066FF),
                  child: Text("検索",
                      style: TextStyle(
                          color: Colors.white,
                          fontSize: 16,
                          fontWeight: FontWeight.bold)),
                  onPressed: () => {}))
        ],
      ),
    );
  }
}
```

次に、SwiftUiです。
*HStack struct* をベースにUIを構成していきます。*HStack struct* はFlutterでいうところの *Row widget* です。
*HStack struct* はデフォルトでcomponent間に隙間ができてしまうので、パラメータspacingに0を適用して隙間ができないようにしています。個人的にはspacingはdefault 0でいい気がしています。

```swift:TopView.swift
struct SearchBar: View {
    fileprivate func searchBar() -> some View {
        return HStack(spacing: 0) {
            HStack(spacing: 0) {
                Spacer().frame(width:10)
                Image(systemName:"magnifyingglass")
                    .resizable()
                    .frame(width: 15, height: 15)
                    .foregroundColor(.gray)
                Spacer().frame(width:10)
                Text("xxxxxxx検索")
                    .foregroundColor(Color.gray)
                    .font(.system(size: 12))
                Spacer()
                Image(systemName:"mic.fill" ).foregroundColor(.gray)
                Spacer().frame(width:10)
                Button(action: {}) {
                    Text("検索")
                        .foregroundColor(Color.white)
                        .font(.system(size: 15))
                        .fontWeight(.bold)
                }
                .padding(EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10))
                .background(Color.blue)
            }
            .frame(maxWidth: .infinity)
            .padding(EdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1))
            .overlay(
                RoundedRectangle(cornerRadius: 2)
                    .stroke(Color.blue, lineWidth: 2)
            )
            Spacer().frame(width: 10)
            Image(systemName:"square.on.square" ).foregroundColor(.gray)
        }
        .padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
    }
    
    var body: some View {
        VStack(spacing: 0.0) {
            searchBar()
        }
    }
}
```

#ログイン・ID取得
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/5e91d5fe-53b0-ad91-2105-3716a54338ca.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/8e6b371b-6d82-08b3-1dbe-5099aafe961e.png">
まずは、Flutterです。
*Container widget* の中に、*Row widget* でログインボタンとID新規登録ボタンを横に並べています。ボタンには *FlatButton widget* を使いました。ログインボタンとID新規登録ボタンの間にあるグレーのセパレータの実現方法を少し迷いました。横線であれば *Divider class* で簡単に実現できますが、縦線となるとそうはいきませんでした。結論としては、*Container widget* に *height: 20.0, width: 0.4, color: Colors.grey* を適用して縦向きのセパレータを実現しました。

```dart:login.dart
class Login extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        decoration: BoxDecoration(
          border:
              Border(bottom: BorderSide(width: 0.4, color: Color(0xffC0C0C0))),
        ),
        child: Row(children: [
          Expanded(
              flex: 1,
              child: FlatButton(
                  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  shape: RoundedRectangleBorder(),
                  color: Color(0xffF5F5F5),
                  child: Text("ログイン",
                      style: TextStyle(
                          color: Colors.black87,
                          fontSize: 13,
                          fontWeight: FontWeight.bold)),
                  onPressed: () => {})),
          Container(height: 20.0, width: 0.4, color: Colors.grey),
          Expanded(
              flex: 1,
              child: FlatButton(
                  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  shape: RoundedRectangleBorder(),
                  color: Color(0xffF5F5F5),
                  child: Text("ID新規取得",
                      style: TextStyle(
                          color: Colors.black87,
                          fontSize: 13,
                          fontWeight: FontWeight.bold)),
                  onPressed: () => {}))
        ]));
  }
}
```
次に、SwiftUiです。
*HStack struct* の中に *Button struct* を配置しています。Flutterで少し苦戦した縦線については、SwiftUiでは、*Divider()* と書くだけで実現できました。*HStack struct* で、配下のcomponentの配置位置の指定と自分自身の装飾が指定できるのは便利だと思いました。Flutterでは、*Container widget* を使って装飾する必要がありますので、SwiftUiの方がFlutterよりソースの階層を浅くできそうです。

```swift:Login.swift
struct Login: View {
    fileprivate func login() -> some View {
        return HStack {
            Button(action: {}){
                Text("ログイン")
                .foregroundColor(Color.black)
                .font(.system(size: 12))
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            Divider()
            Button(action: {}){
                Text("ID新規取得")
                .foregroundColor(Color.black)
                .font(.system(size: 12))
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .padding(EdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0))
        .frame(height: 30.0, alignment: .center)
        .frame(maxWidth: .infinity)
        .background(Color.init(hex: "eee"))
        .border(Color.init(hex: "bbb"), width: 0.5)
    }
    
    var body: some View {
        VStack(spacing: 0.0) {
            login()
        }
    }
}

```

##タイルメニュー : メール、天気、占い等
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/65ddf694-678e-86f4-926b-94dbe8a2086c.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/02fe416b-ca4e-694b-787e-9a8999b8b618.png">
まずは、Flutterです。
*Column widget*　配下に *Row widget* を二つ配置することで、２列のメニューを表現します。*Row widget* の children には、各メニューのwidgetを設定しますが、 widgetは共通のfunctionで生成可能です。 この共通functionを使用するために、widget tree 内でループ処理を行いたいので、 *List class* のconstractorである *generate* を使用しました。

```dart:menu1.dart
class Menu1 extends StatelessWidget {
  final List<Map<String, dynamic>> menuData1 = [
    {"icon": Icons.mail_outline, "text": "メール"},
    {"icon": Icons.wb_sunny, "text": "天気"},
    {"icon": Icons.stars, "text": "占い"},
    {"icon": Icons.star_border, "text": "お気に入り"},
    {"icon": Icons.add_shopping_cart, "text": "ショッピング"}
  ];

  final List<Map<String, dynamic>> menuData2 = [
    {"icon": Icons.accessibility, "text": "スポーツナビ"},
    {"icon": Icons.account_balance, "text": "オク"},
    {"icon": Icons.train, "text": "路線情報"},
    {"icon": Icons.account_balance_wallet, "text": "スロットくじ"},
    {"icon": Icons.all_inclusive, "text": "すべて"}
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
            children: List.generate(
                menuData1.length,
                (index) => Expanded(
                    child: _menu(
                        icon: menuData1[index]["icon"],
                        text: menuData1[index]["text"])))),
        Container(
            color: Color(0xffEEEEEE),
            padding: EdgeInsets.only(bottom: 6.0),
            child: Row(
                children: List.generate(
                    menuData2.length,
                    (index) => Expanded(
                        child: _menu(
                            icon: menuData2[index]["icon"],
                            text: menuData2[index]["text"])))))
      ],
    );
  }

  Widget _menu({IconData icon, String text}) {
    return text != "すべて"
        ? GestureDetector(
            onTap: () => {},
            child: Container(
                decoration: BoxDecoration(
                    color: Colors.white,
                    border: Border(
                        right: BorderSide(width: 0.4, color: Colors.black12),
                        bottom: BorderSide(width: 0.4, color: Colors.black12))),
                padding: EdgeInsets.only(top: 7, bottom: 7),
                child: Column(children: [
                  Icon(icon),
                  Text(text, style: TextStyle(fontSize: 10))
                ])))
        : Container(
            decoration: BoxDecoration(
                color: Color(0xffEEEEEE),
                border: Border(
                    right: BorderSide(width: 0.4, color: Colors.black12),
                    bottom: BorderSide(width: 0.4, color: Colors.black12))),
            child: SizedBox(
                height: 53.0,
                child: Padding(
                    padding: EdgeInsets.only(top: 18.0),
                    child: Text("すべて",
                        style: TextStyle(
                            fontSize: 13,
                            fontWeight: FontWeight.bold,
                            color: Colors.black54),
                        textAlign: TextAlign.center))));
  }
}
```

次に、SwiftUiです。
*VStack struct* 配下に *HStack struct* を二つ配置することで、2列のメニューを表現します。
Flutterと同様に、各メニューは共通のfunctionで生成しました。Flutterの時のように、*HStack struct* 内でForEachを使って、ループ処理内で共通functionを起動させようとしましたが、作成した共通functionはgenericなViewをreturnするため、ForEach内に記載するとコンパイルエラーとなってしました。なので、ForEachは使いませんでした。 冗長になってしまいますが、*HStack struct* 内でメニューの数だけ、共通functionを起動するようにしました。

```swift:Menu1.swift
struct Menu1: View {
    @State private var isMailScreenPresented = false
    @State private var isWeatherScreenPresented = false
    
    fileprivate func menu1() -> some View {
        
        return VStack(spacing: 0) {
            Divider()
            HStack(spacing: 0) {
                tile(imageName: "envelope",
                     label: "メール",
                     isPresented: $isMailScreenPresented,
                     tapGesture: {self.isMailScreenPresented.toggle()},
                     screen: MailScreen(isMailScreenPresented: self.$isMailScreenPresented))
                Divider()
                tile(imageName: "sun.max",
                     label: "天気",
                     isPresented: $isWeatherScreenPresented,
                     tapGesture: {self.isWeatherScreenPresented.toggle()},
                     screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                tile(imageName: "star.circle",
                    label: "占い",
                    isPresented: $isWeatherScreenPresented,
                    tapGesture: {self.isWeatherScreenPresented.toggle()},
                    screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                tile(imageName: "star",
                    label: "お気に入り",
                    isPresented: $isWeatherScreenPresented,
                    tapGesture: {self.isWeatherScreenPresented.toggle()},
                    screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                tile(imageName: "cart",
                    label: "ショッピング",
                    isPresented: $isWeatherScreenPresented,
                    tapGesture: {self.isWeatherScreenPresented.toggle()},
                    screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
            }
            Divider()
            HStack(spacing: 0) {
                tile(imageName: "sportscourt",
                     label: "スポーツナビ",
                     isPresented: $isWeatherScreenPresented,
                     tapGesture: {self.isWeatherScreenPresented.toggle()},
                     screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                tile(imageName: "hammer",
                     label: "オク！",
                     isPresented: $isWeatherScreenPresented,
                     tapGesture: {self.isWeatherScreenPresented.toggle()},
                     screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                tile(imageName: "tram.fill",
                     label: "経路情報",
                     isPresented: $isWeatherScreenPresented,
                     tapGesture: {self.isWeatherScreenPresented.toggle()},
                     screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                tile(imageName: "cube.box",
                     label: "スロットくじ",
                     isPresented: $isWeatherScreenPresented,
                     tapGesture: {self.isWeatherScreenPresented.toggle()},
                     screen: WeatherScreen(isWeatherScreenPresented: self.$isWeatherScreenPresented))
                Divider()
                Text("すべて").font(.system(size: 12))
                .frame(height: 52, alignment: .center)
                .frame(maxWidth: .infinity)
                .background(Color.init(hex: "eee"))
                .onTapGesture {}
            }
            Divider()
                .padding(EdgeInsets(top: 0, leading: 0, bottom: 7, trailing: 0))
                .background(Color(red: 230/255, green: 230/255, blue: 230/255))
        }
    }
    
    fileprivate func tile<T:View>(imageName: String,
                          label: String,
                          isPresented: Binding<Bool>,
                          tapGesture: @escaping () -> Void,
                          screen: T) -> some View {
        return VStack(spacing: 0) {
            Image(systemName:imageName ).foregroundColor(.gray)
            .padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
            Text(label).font(.system(size: 8))
        }
        .frame(height: 48, alignment: .center)
        .frame(maxWidth: .infinity)
        .padding(EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0))
        .onTapGesture {tapGesture()}
        .sheet(isPresented: isPresented) {
            screen
        }
    }
    
    var body: some View {
        VStack(spacing: 0.0) {
            menu1()
        }
    }
}
```

##スクロールメニュー：StayHome、ニュース、クーポン、芸能等
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/211f079b-dbf4-20f5-05d7-a1cec137c2d8.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/df86a1e0-7a54-adeb-3786-83a1d472ae59.png">
まずは、Flutterです。
横にスクロールさせる必要がありますので、*SingleChildScrollView widget* を使い、*scrollDirection property* に Axis.horizontalを適用しました。 defaultだと縦スクロールになってしまいます。前出と同様に、共通functionで各メニューを作成するために、widget tree 内でループ処理を行いたいので、 *List class* のconstractorである *generate* を使用しました。
また、tap eventを取得するために *GestureDetector widget*を使用しています。*GestureDetector widget*でラップされたwidgetをtapすると、*onTap property* に適用した処理が呼び出されます。今回は処理を適用していませんが、メニューの切り替えやページ遷移をここで行います。

```dart:menu2.dart
class Menu2 extends StatelessWidget {
  final List<String> menuData = [
    "すべて",
    "ニュース",
    "新型コロナ",
    "クーポン",
    "芸能",
    "スポーツ",
    "話題",
    "フォロー",
    "東京五輪"
  ];
  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(
            children: List.generate(
                menuData.length, (index) => _tabMenu(menuData[index]))));
  }

  Widget _tabMenu(String label) {
    return GestureDetector(
        onTap: () => {},
        child: Container(
            color: Color(0xffF5F5F5),
            padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 9.0),
            child: Row(children: [
              Text(label,
                  style: TextStyle(
                      color: Color(0xff333333),
                      fontSize: 12,
                      fontWeight: FontWeight.bold),
                  textAlign: TextAlign.center),
              label != "東京五輪"
                  ? Padding(
                      padding: EdgeInsets.only(left: 15.0),
                      child: Container(
                          height: 20.0, width: 0.4, color: Colors.grey))
                  : Container()
            ])));
  }
}
```

次に、SwiftUiです。
*HStack struct* を使い、メニューを表現しました。横スクロールさせるために、*ScrollView struct* を使用しました。defaultだと縦スクルールになってしまうので、 *.horizontal* を与えることで、横スクロールにします。*HStack struct* 内で ForEachを使用して、９個のメニューを *Button struct*で表現しました。

```swift:Menu2.swift
struct Menu2: View {
    @Binding var selectedPage: Int
    
    let menu2s = ["すべて","StayHome","ニュース","クーポン","芸能","スポーツ","話題","フォロー","東京五輪"]
    
    var body: some View {
        VStack(spacing: 0) {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 0) {
                    ForEach(0..<9) { index in
                        Button(
                            action: {
                                self.selectedPage = index
                            })
                            {
                                Text(self.menu2s[index])
                                    .foregroundColor(self.selectedPage == index ? .white : .gray)
                                .font(.system(size: 13))
                                .fontWeight(.bold)
                            
                            }
                        .padding(EdgeInsets(top: 12, leading: 10, bottom: 12, trailing: 10))
                        .background(self.selectedPage == index ? Color.init(hex: "ed615b") : Color.init(hex: "eee"))
                        Divider().padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
                    }
                }
            }
            Divider()
        }
    }
}

```

##ニュースリスト1:最新ニュース
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/99b50f80-2518-a56d-35f1-c402c757f712.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/d2d7ae6b-fbe3-7a28-c444-9a2050dd23b2.png">
ニュースコンテンツのUIパターンが４つあります。まず１つ目です。
左に写真、右にタイトル、更新日時等を1つのニュースコンテンツとして、これを6つ配置するもので、ニュースコンテンツの中では一番小さいものになります。　

まずは、Flutterです。左に写真、右にタイトル、更新日時等を配置し、１つのニュースコンテンツである*NewsList class* を実装しました。左に配置する写真と右に配置するタイトル、更新日時等のwidgetを常に同じ比率で表示するために、*Expanded widget* を使用しました。
*NewsList class* を *Column widget* 内に6つ配置してニュースリスト1の完成です。

```dart:news_list.dart
class NewsList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        padding: EdgeInsets.all(5.0),
        decoration: BoxDecoration(
            border: Border(
                bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
        child: Row(children: [
          Expanded(
              flex: 1,
              child: SizedBox(
                width: 50.0,
                height: 50.0,
                child: Image.asset("assets/images/150x150.png"),
              )),
          SizedBox(width: 10),
          Expanded(
              flex: 5,
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text("東京の感染者数　経路不明5割弱",
                        style: TextStyle(
                            fontSize: 16.0,
                            fontWeight: FontWeight.bold,
                            color: Color(0xff333333))),
                    SizedBox(width: 10),
                    Row(children: [
                      Icon(Icons.chat_bubble_outline,
                          size: 12, color: Colors.pinkAccent),
                      SizedBox(width: 3),
                      Text("47",
                          style: TextStyle(
                              fontSize: 12.0, color: Colors.pinkAccent)),
                      SizedBox(width: 10),
                      Text("7/12(日) 0:24",
                          style: TextStyle(
                              fontSize: 12.0,
                              color: Colors.grey,
                              textBaseline: TextBaseline.ideographic)),
                      SizedBox(width: 10),
                      Container(
                        padding: EdgeInsets.symmetric(horizontal: 4.0),
                        decoration: BoxDecoration(
                            borderRadius:
                                BorderRadius.all(Radius.circular(5.0)),
                            color: Colors.orangeAccent),
                        child: Text("NEW",
                            style: TextStyle(
                                fontWeight: FontWeight.w600,
                                fontSize: 10.0,
                                color: Colors.white,
                                letterSpacing: -0.5)),
                      )
                    ])
                  ]))
        ]));
  }
}
```

次に、SwiftUiです。
左に写真、右にタイトル、更新日時等を配置し、１つのニュースコンテンツである *NewsList struct* を実装しました。*VStack struct* と *HStack struct* を使って、UIを表現します。
*NewsList struct* を *VStack struct* 内に6つ配置してニュースリスト1の完成です。

```swift:NewsList.swift
struct NewsList: View {
    fileprivate func newsDetail() -> some View {
        return HStack(spacing: 0.0){
            Image("150x150").resizable().frame(width: 50.0, height: 50.0)
            Spacer().frame(width:10)
            VStack(alignment: .leading){
                Text("東京の感染者数 経路不明5割弱")
                    .font(.system(size: 15))
                    .fontWeight(.bold)
                    .foregroundColor(Color.init(hex: "333333"))
                HStack{
                    Image(systemName: "bubble.right")
                        .resizable()
                        .frame(width: 12.0, height: 12.0, alignment: .center)
                        .foregroundColor(Color.init(hex: "FF4081"))
                    Spacer().frame(width:1)
                    Text("47")
                        .font(.system(size: 12))
                        .foregroundColor(Color.init(hex: "FF4081"))
                    Text("7/12(日) 0:24")
                        .font(.system(size: 12))
                        .foregroundColor(Color.init(hex: "9E9E9E"))
                    Text("NEW")
                        .padding(EdgeInsets.init(top: 2.0, leading: 4.0, bottom: 2.0, trailing: 4.0))
                        .font(.system(size: 10))
                        .foregroundColor(.white)
                        .background(Color.init(hex: "FFAB40"))
                        .cornerRadius(30)
                }
                Divider()
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
    
    }
    
    var body: some View {
            VStack(spacing: 0.0){
                newsDetail()
            }
    }
}
```
##ニュースリスト2:一番大きなニュース
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/e3d4f649-d1c4-4f45-52d3-ede746c83025.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/571d4014-04d5-213d-1d58-4c4273bb7d42.png">
画面横幅一杯の写真を配置し、その写真の下にニュースのタイトルと、このニュースの提供元を表示します。

まずは、Flutterです。
写真、ニュースタイトル、提供元を上から順番に配置すればいいので、*Column widget* を使用します。*Column widget*内のwidgetを全て左寄せにしたいので、*Column widget* の *crossAxisAlignment property* にCrossAxisAlignment.startを適用します。

```dart:big_news.dart
class BigNews extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
          border:
              Border(bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
      padding: EdgeInsets.all(5.0),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Image.asset("assets/images/collecting.jpeg"),
        Text("女性から寄生虫、刺身食べて侵入か？　日本",
            style: TextStyle(
                fontSize: 16.0,
                fontWeight: FontWeight.bold,
                color: Color(0xff333333))),
        SizedBox(width: 10),
        Text("NN.co.jp",
            style: TextStyle(
                fontSize: 12.0,
                color: Colors.grey,
                textBaseline: TextBaseline.ideographic)),
      ]),
    );
  }
}
```

次に、SwiftUiです。
Flutterと同様で、写真、ニュースタイトル、提供元を上から順番に配置すればいいので、*VStack struct* を使用します。*VStack struct*内のUIを全て左寄せにしたいので、*VStack struct* の *alignment property* に.leadingを適用します。


```swift:BigNews.swift
struct BigNews: View {
    fileprivate func newsDetail() -> some View {
        return VStack(alignment: .leading, spacing: 0.0) {
            Image("collecting").resizable()
                .frame(height: 200)
            Text("女性から寄生虫、刺身食べて侵入か？　日本")
            .font(.system(size: 16))
            .fontWeight(.bold)
            .foregroundColor(Color.init(hex: "333333"))
            Spacer().frame(height:10)
            Text("News.co.jp")
            .font(.system(size: 12))
            .foregroundColor(Color.gray)
        }
        .padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
    }
    
    var body: some View {
        VStack(spacing: 0.0) {
            newsDetail()
        }
    }
}
```

##ニュースリスト3:二番目に大きなニュース
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/d96c4610-21d9-bd7b-183e-125c52c1fc7e.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/11d872de-0aee-43c8-0919-c9693a2568ab.png">
まずは、Flutterです。
それぞれのニュースコンテンツの中身は、*ニュースリスト2:一番大きなニュース* と同じですが、横幅が異なります。
*Row widget* を使って、画面の横幅を半分ずつ使用して、２つのニュースコンテンツを配置します。

```dart:middle_news.dart
class MiddleNews extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        decoration: BoxDecoration(
            border: Border(
                bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
        padding: EdgeInsets.all(5.0),
        child: Row(children: [
          Container(
              decoration: BoxDecoration(
                  border: Border(
                      right: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
              padding: EdgeInsets.only(right: 5.0),
              width: (MediaQuery.of(context).size.width / 2) - 10.0,
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Image.asset("assets/images/collecting.jpeg"),
                    Text("「100万円たまる貯金箱」を６年かけて満杯に　2000日の苦労を本人い聞いた",
                        style: TextStyle(
                            fontSize: 16.0,
                            fontWeight: FontWeight.bold,
                            color: Color(0xff333333))),
                    SizedBox(width: 10),
                    Text("タウンネット",
                        style: TextStyle(
                            fontSize: 12.0,
                            color: Colors.grey,
                            textBaseline: TextBaseline.ideographic)),
                  ])),
          Container(
              padding: EdgeInsets.only(left: 5.0),
              width: (MediaQuery.of(context).size.width / 2) - 10.0,
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Image.asset("assets/images/collecting.jpeg"),
                    Text("「当時は睡眠２時間」、再結成、引退。。。紆余曲折から。。。",
                        style: TextStyle(
                            fontSize: 16.0,
                            fontWeight: FontWeight.bold,
                            color: Color(0xff333333))),
                    SizedBox(width: 10),
                    Text("スポーツニュース",
                        style: TextStyle(
                            fontSize: 12.0,
                            color: Colors.grey,
                            textBaseline: TextBaseline.ideographic)),
                  ])),
        ]));
  }
}
```

次に、SwiftUiです。
Flutterと同様で、それぞれのニュースの中身は、*ニュースリスト2:一番大きなニュース* と同じですが、横幅が異なります。*HStack struct* を使って、画面の横幅を半分ずつ使用して、２つのニュースコンテンツを配置します。

```swift:MiddleNews.swift
struct MiddleNews: View {
    fileprivate func newsDetail() -> some View {
        return HStack {
            VStack(alignment: .leading, spacing: 0.0) {
                Image("collecting").resizable()
                    .frame(height: 150)
                Text("「100万円たまる貯金箱」を６年かけて満杯に　2000日の苦労を本人い聞いた")
                .font(.system(size: 16))
                .fontWeight(.bold)
                .foregroundColor(Color.init(hex: "333333"))
                Spacer().frame(height:10)
                Text("タウンネット")
                .font(.system(size: 12))
                .foregroundColor(Color.gray)
            }
            VStack(alignment: .leading, spacing: 0.0) {
                Image("collecting").resizable()
                    .frame(height: 150)
                Text("「当時は睡眠２時間」解散、再結成、引退。。。紆余曲折から。。。")
                .font(.system(size: 16))
                .fontWeight(.bold)
                .foregroundColor(Color.init(hex: "333333"))
                Spacer().frame(height:10)
                Text("スポーツニュース")
                .font(.system(size: 12))
                .foregroundColor(Color.gray)
            }
        }
        .padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
    }
    
    var body: some View {
        VStack(spacing: 0.0) {
            newsDetail()
        }
    }
}
```

##ニュースリスト4:一番シンプルなUIのニュース
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/d26097f9-257e-012f-bf8f-6e1d03842d6b.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/e3c6cda9-e363-b25a-5fbd-a0e52beee42b.png">
まずは、Flutterです。
それぞれのニュースの中身は、*ニュースリスト2:一番大きなニュース* と同じですが、配置が少し異なります。左に写真、右にニュースタイトル、提供元を配置します。
左右の分割は *Row widget* を実現し、ニュースタイトルと提供元の縦並びは *Column widget* で実現しました。


```dart:small_news.dart
class SmallNews extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        padding: EdgeInsets.all(5.0),
        decoration: BoxDecoration(
            border: Border(
                bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
        child: Row(children: [
          Expanded(
            flex: 2,
            child: Image.asset("assets/images/150x150.png"),
          ),
          SizedBox(width: 10),
          Expanded(
              flex: 5,
              child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text("映画４作品の再上映。異例のヒット",
                        style: TextStyle(
                            fontSize: 16.0,
                            fontWeight: FontWeight.bold,
                            color: Color(0xff333333))),
                    SizedBox(width: 10),
                    Text("シネマシネマ",
                        style: TextStyle(
                            fontSize: 12.0,
                            color: Colors.grey,
                            textBaseline: TextBaseline.ideographic)),
                  ]))
        ]));
  }
}
```

次に、SwiftUiです。
Flutterと同様で、それぞれのニュースの中身は、*ニュースリスト2:一番大きなニュース* と同じですが、配置が少し異なります。左に写真、右にニュースタイトル、提供元を配置します。
左右の分割は *HStack struct* を実現し、ニュースタイトルと提供元の縦並びは *VStack struct* で実現しました。

```swift:SmallNews.swift
struct SmallNews: View {
    fileprivate func newsDetail() -> some View {
        return HStack(spacing: 0.0){
            Image("150x150").resizable().frame(width: 120.0, height: 120.0)
            Spacer().frame(width:10)
            VStack(alignment: .leading){
                Text("映画４作品の再上映。異例のヒット")
                    .font(.system(size: 15))
                    .fontWeight(.bold)
                    .foregroundColor(Color.init(hex: "333333"))
                Spacer().frame(height:10)
                Text("タウンネット")
                    .font(.system(size: 12))
                    .foregroundColor(Color.gray)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
    
    }
    
    var body: some View {
            VStack(spacing: 0.0){
                newsDetail()
            }
    }
}
```

##動画ニュース
**flutter**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/05618c4b-d00e-5329-d63f-0f42d64b46f1.png">
**SwiftUi**
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/42e1d770-73d7-2e45-624f-1f52c4ad44f1.png">
まずは、Flutterです。
動画再生用に[video_player](https://pub.dev/packages/video_player) package を使用しました。
*Column widget* 内に、video_playerを使って動画用widget、ニュースタイトル、提供元を配置します。

```dart:big_movie.dart
class BigMovie extends StatefulWidget {
  @override
  _BigMovie createState() => _BigMovie();
}

class _BigMovie extends State<BigMovie> {
  VideoPlayerController _controller;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.network(
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4')
      ..initialize().then((_) {
        _controller.setLooping(true);
        _controller.play();
        setState(() {});
      });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
          border:
              Border(bottom: BorderSide(color: Color(0xffc5c5c5), width: 0.3))),
      padding: EdgeInsets.all(5.0),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        _controller.value.initialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller))
            : Container(),
        Text("女性から寄生虫、刺身食べて侵入か？　日本",
            style: TextStyle(
                fontSize: 16.0,
                fontWeight: FontWeight.bold,
                color: Color(0xff333333))),
        SizedBox(width: 10),
        Text("NN.co.jp",
            style: TextStyle(
                fontSize: 12.0,
                color: Colors.grey,
                textBaseline: TextBaseline.ideographic)),
      ]),
    );
  }
}
```

次に、SwiftUiです。
動画再生用の *PlayerView struct* を作り、動画のDLから再生まで行いました。
[このサイトで紹介されているコードを流用しました。](https://teratail.com/questions/256273)
*VStack struct* 内に、*PlayerView struct*、ニュースタイトル、提供元を配置します。

```swift:BigMovie.swift
struct BigMovie: View {
    fileprivate func movieDetail() -> some View {
        return VStack(alignment: .leading, spacing: 0.0) {
            PlayerView().frame(height: 200, alignment: .center)
            Text("女性から寄生虫、刺身食べて侵入か？　日本")
            .font(.system(size: 16))
            .fontWeight(.bold)
            .foregroundColor(Color.init(hex: "333333"))
            Spacer().frame(height:10)
            Text("NN.co.jp")
            .font(.system(size: 12))
            .foregroundColor(Color.gray)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(EdgeInsets.init(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0))
    }
    
    var body: some View {
        VStack(spacing: 0.0) {
            movieDetail()
        }
    }
}
```
##スクロール
上にスクロールすることで、search barとスクロールメニューをトップに固定して、その他のコンテンツは画面上部へと消えていきます。
下にスクロールすることで、上部に消えたコンテンツが再表示されます。

まずは、Flutterです。
*CustomScrollView widget*、*SliverAppBar widget*、*SliverToBoxAdapter widget* を使いました。*SliverAppBar widget* の *flexibleSpace property* に *Login widget*、*menu1 widget* を適用します。ここはスクロールが一番上の時に表示し、スクロールすると上部に消えるコンテンツを適用する場所になります。*bottom property* には上部にスクロールした際も消えないコンテンツを適用します。今回はスクロールメニューである*menu2 widget*を適用します。

```dart:home_screen.dart
class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverAppBar(
            pinned: true,
            backgroundColor: Color(0xffFFFAFA),
            expandedHeight: 242,
            flexibleSpace: FlexibleSpaceBar(
                background: Container(
                    child: Column(children: [Login(), PickUpNews(), Menu1()]))),
            bottom: PreferredSize(
                preferredSize: Size.fromHeight(-6), child: Menu2())),
        SliverToBoxAdapter(
            child:
                Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          _newsList(),
          _bigNewsList(),
          _middleNewsList(),
          _smallNewsList(),
          _bigMovieList(),
        ]))
      ],
    );
  }
}
```

次に、SwiftUiです。
*List struct* を使って実現します。まず、スクロールしていない状態では表示され、スクロールされると消えるコンテンツを *VStack struct* 内に適用します。今回は*Login class* や *Menu1 class* になります。スクロールしても消さない部分は、*Section struct* の *header property* に適用します。ここは*Menu2 class*を適用します。*content property*　には、*Menu2 class*の下部に表示する各メニューリストのclassを適用します。

```swift:HomeScreen.swift
var body: some View {
    VStack(spacing: 0) {
        SearchBar()
        List {
            VStack(spacing: 0) {
                Login()
                PickupNews()
                Menu1()
            }
            .background(Color.white)
            .listRowInsets(EdgeInsets())
            Section(header: Menu2(selectedPage: $selectedPage)) {
                self.minNewsList()
                self.minNewsList()
                self.bigNewsList()
                self.middleNewsList()
                self.smallNewsList()
                self.bigMovieList()
            }
            .listRowInsets(EdgeInsets())
        }
    }
}
```

##まとめ
今回、某ポータルアプリのUIをFlutterとSwiftUiで構築しました。FlutterとSwiftUiともにですが、font family、font size、padding、margin等を全く同じにするのはかなり工数がかかることがわかりました。宣言的UIの実装については、書きやすさ、ソースの見通しの良さに関して大きな差異は感じませんでした。(慣れの問題でFlutterの方が若干書きやすかったですが)
iOS開発で、storyboardを使ってきた人間からすると、SwiftUiに抵抗があるかもしれませんが、慣れてしまえばこっちの方が、速いし、軽いし、プログラミングぽくていいです。
画面遷移や画面内のwidgetの挙動に関してもっと実装したいという思いはありますが、今回は静的な部分を実装することにしました。あと、uiのrebuildについても比較してみたいです。Flutterは、rebuildをいかに美しく行うかを目指して、いろんなpackageが出ています。SwiftUiはどうなのでしょうか。気になるところです。