Slide 1

Slide 1 text

同じアプリを Flutter と SwiftUI で書いてみる Mobile勉強会 Wantedly × チームラボ × Sansan #14 @swiftty

Slide 2

Slide 2 text

自己紹介 林達也 ウォンテッドリー株式会社  swiftty  _swiftty

Slide 3

Slide 3 text

今日話すこと iOS エンジニア視点で Flutter を学習した際に感じた 違いなどを SwiftUI の場合と比較しつつ感想を述べます ※ Flutter のコードはもっと良い書き方ができると思います こうするとよいなどあれば懇親会でぜひ教えて下さい 🙏

Slide 4

Slide 4 text

作ったものの紹介 SwiftUI Flutter Apple のドキュメント閲覧アプリ swiftty/apple-documentation swiftty/apple-documentation-flutter モジュール一覧画面と記事詳細画面 Flutter は riverpod + freezed を使用

Slide 5

Slide 5 text

記事のデータ構造 "content": [ { "type": "heading", ... }, { "type": "paragraph", "inlineContent": [ { "type": "text", ... }, { "type": "reference", ... }, { "type": "image", ... }, ... ] }, { "type": "aside", "content": [...] }, ] 行ごとのデータ構造 paragraph などはイ ンライン要素でテキ ストやリンクなどの 種類を持つ

Slide 6

Slide 6 text

enum BlockContent { case paragraph(Paragraph) case heading(Heading) case aside(Aside) ... struct Aside { public var style: String public var name: String? public var contents: [BlockContent] } } enum InlineContent { case text(Text) case codeVoice(CodeVoice) case strong(Strong) case emphasis(Emphasis) case image(Image) ... } @Freezed(unionKey: 'type') sealed class BlockContent with _$BlockContent { const factory BlockContent.heading({ required String text, required int level, String? anchor, }) = BlockContentHeading; const factory BlockContent.paragraph( List inlineContent, ) = BlockContentParagraph; const factory BlockContent.aside({ required List content, required String style, required String? name, }) = BlockContentAside; ... } @Freezed(unionKey: 'type', fallbackUnion: 'unknown') sealed class InlineContent with _$InlineContent { const factory InlineContent.text({ required String text, }) = InlineContentText; ... } Flutter は freezed を 用いてデータを定義 unionKey: 'type' で 直和型にマッピング 再帰的に BlockContent を持 つ →再帰的なデータに対 するビューを作る必要 がある 記事のデータ構造

Slide 7

Slide 7 text

1. 記事データの再帰的ビュー比較 Flutter では基底クラスの Widget を再帰的に返す構造で実現 Widget render(BlockContent block) { block.when( aside: (content) { return Container( padding: ..., decoration: ..., child: render(content), ); }, ... ); }

Slide 8

Slide 8 text

SwiftUI では some View は実際は型がつくため再帰的なビューを作る ときは struct で型を確定させる必要がある // error: Function opaque return type was inferred as '...', // which defines the opaque type in terms of itself @ViewBuilder func render(_ block: BlockContent) -> some View { switch block { case .aside(let content): render(content) .padding() .border(.black) ... } } 1. 記事データの再帰的ビュー比較

Slide 9

Slide 9 text

SwiftUI では some View は実際は型がつくため再帰的なビューを作る ときは struct で型を確定させる必要がある struct InnerView: View { let block: BlockContent var body: some View { switch block { case .aside(let content): InnerView(block: content) .padding() .border(.black) ... } } } 1. 記事データの再帰的ビュー比較

Slide 10

Slide 10 text

2. 子ビューの構築方法比較 Swift の場合は ViewBuilder を活用して要素を構築 Dart の場合はリスト内包表記を活用すると似たように要素が構築で きる 例)数値の配列の要素それぞれを倍にする [ for (final number in numbers) 2 * number ]

Slide 11

Slide 11 text

リスト内包表記とスプレッド演算子を組み合わせると、要素の構造 を直感的に記述可能 if / for で返せる要素は一つだけだが、スプレッド演算子で囲むと複 数要素を扱える ListView( children: [ for (final ref in detail.references) ...[ if (ref is ReferenceTopic) for (final fragment in ref.fragments) FragmentWidget(fragment) ], if (detail.relationshipsSections.isNotEmpty) ...[ const SizedBox(height: 8), const Divider(), ], ], 2. 子ビューの構築方法比較

Slide 12

Slide 12 text

感想 今回のようなデータ構造の場合は freezed がとても便利 リスト内包表記によってコードと結果がより直感的に記述できる lazy を気にせず雑にビューを構築したが今回の範囲ではパフォーマ ンスは特に気にならなかった 装飾を加えるたびに様々な Widget で外側を囲むためネストが深く なってしまうのは大変