Slide 1

Slide 1 text

© 2023 Wantedly, Inc. SwiftUI + Kotlin Multiplatform 開発で 見えた長所と短所 iOSDC Japan 2023 スポンサーセッション Sep. 2 2023 - ウォンテッドリー株式会社 / 林達也

Slide 2

Slide 2 text

自己紹介 id/tatsuya_hayashi_ar swiftty © 2023 Wantedly, Inc. ウォンテッドリー株式会社 2023 年 3 月〜 Wantedly Visit の iOS アプリの開発

Slide 3

Slide 3 text

今日話すこと 前提 ● iOS エンジニアで Kotlin Multiplatform (KMP) には触れたことがない ● 過去のプロダクトは基本 UIKit で SwiftUI は部分的な導入程度 話すこと ● KMP や SwiftUI での開発の際にどのような取り組みがあったかを紹介 ○ プロフィールリニューアルについて ○ SwiftUI 事例 ○ KMP と Swift/SwiftUI の組み合わせ © 2023 Wantedly, Inc.

Slide 4

Slide 4 text

はじめに © 2023 Wantedly, Inc.

Slide 5

Slide 5 text

プロフィールリニューアル プロジェクト ● 各プラットフォームでのプロフィール体験を 揃える ● ウォンテッドリーではもともと KMP を採用し てマルチプラットフォームでの開発を行って いた ○ Android が先行していたため KMP 側の実装は完了していた 🎉 ● SwiftUI の採用 🎉 © 2023 Wantedly, Inc. 引用: https://www.wantedly.com/companies/wantedly/post_articles/489565

Slide 6

Slide 6 text

SwiftUI の採用 ● 今までも設定画面など部分的な導入はあっ た ● 今回のリニューアルでは SwiftUI で 実現できるのかの検証も含めて始まった © 2023 Wantedly, Inc.

Slide 7

Slide 7 text

KMP とは ● Kotlin Multiplatform ● ビジネスロジックを共通化 ● UI は各プラットフォームで実装 © 2023 Wantedly, Inc. 引用: https://kotlinlang.org/lp/multiplatform/

Slide 8

Slide 8 text

KMP とは ● Kotlin Multiplatform ● ビジネスロジックを共通化 ● UI は各プラットフォームで実装 © 2023 Wantedly, Inc.

Slide 9

Slide 9 text

ビジネスロジックの共通化 Reactor ● 単方向データフローアーキテクチャを採用 ○ View → Action → (Mutation) → State → View → … ○ アプリケーション全体でひとつの状態管理ではなく集約されたビュー単位で状態管理 ● Kotlin で定義されたクラスなので ObservableObject に適合していない ○ 後付けで ObservableObject が要求する objectWillChange を 実装するのは大変 © 2023 Wantedly, Inc.

Slide 10

Slide 10 text

ビジネスロジックの共通化 Reactor ● 単方向データフローアーキテクチャを採用 ○ View → Action → (Mutation) → State → View → … ○ アプリケーション全体でひとつの状態管理ではなく集約されたビュー単位で状態管理 ● Kotlin で定義されたクラスなので ObservableObject に適合していない ○ 後付けで ObservableObject が要求する objectWillChange を 実装するのは大変 © 2023 Wantedly, Inc. → Swift 側で Reactor をラップする  

Slide 11

Slide 11 text

SwiftUI とReactor © 2023 Wantedly, Inc.

Slide 12

Slide 12 text

SwiftUI とReactor © 2023 Wantedly, Inc.

Slide 13

Slide 13 text

SwiftUI とReactor © 2023 Wantedly, Inc. → 以降は通常の SwiftUI と同じ  

Slide 14

Slide 14 text

SwiftUI とReactor © 2023 Wantedly, Inc. → 以降は通常の SwiftUI と同じ  

Slide 15

Slide 15 text

SwiftUI とReactor © 2023 Wantedly, Inc. → 以降は通常の SwiftUI と同じ  

Slide 16

Slide 16 text

SwiftUI 事例 プロフィールヘッダー © 2023 Wantedly, Inc.

Slide 17

Slide 17 text

プロフィールヘッダー © 2023 Wantedly, Inc. UIKit でもよくある UI ● コンテンツをスクロールするとヘッダーが縮む ● タブを切り替えたらヘッダーはそれに追従する

Slide 18

Slide 18 text

プロフィールヘッダー SwiftUI ではどうする? ● ScrollView のスクロールを起点にレイアウトの制御を行いたい ● SwiftUI の ScrollView の問題 ○ contentInset が調整できない ○ オフセットを監視できない ○ オフセットを変更できない © 2023 Wantedly, Inc.

Slide 19

Slide 19 text

プロフィールヘッダー SwiftUI ではどうする? ● contentInset が調整できない → 空のビューでヘッダー分コンテンツの開始位置をずらす? ● オフセットを監視できない → 子要素を GeometryReader で監視? ● オフセットを変更できない → ScrollViewReader を使って scroll ならできる? ● 最終的にオフセットをもとにヘッダービューの高さを変更すれば 当初のレイアウトを実現できそう 💭 © 2023 Wantedly, Inc.

Slide 20

Slide 20 text

プロフィールヘッダー © 2023 Wantedly, Inc.

Slide 21

Slide 21 text

プロフィールヘッダー © 2023 Wantedly, Inc. TabView を page スタイルで表示

Slide 22

Slide 22 text

プロフィールヘッダー © 2023 Wantedly, Inc. 各 content 内の ScrollView に高さ 0 で設置した ビューの位置をコンテンツの開始位置として anchorPreference で監視

Slide 23

Slide 23 text

プロフィールヘッダー © 2023 Wantedly, Inc. 各 content 内の ScrollView に高さ 0 で設置した ビューの位置をコンテンツの開始位置として anchorPreference で監視 overlayPreferenceValue でその位置をもとに ヘッダーの高さを算出しオーバーレイで重ねる

Slide 24

Slide 24 text

プロフィールヘッダー © 2023 Wantedly, Inc. オフセットが変化するたびに選択されたタブ以外の スクロール位置を scrollTo で同期させる

Slide 25

Slide 25 text

プロフィールヘッダー © 2023 Wantedly, Inc.

Slide 26

Slide 26 text

プロフィールヘッダー © 2023 Wantedly, Inc. → 思ったより素直なコードで実現できた 🎉  

Slide 27

Slide 27 text

プロフィールヘッダー 良かった点 ● 複雑なレイアウトを SwiftUI でシンプルに実現 ○ UIKit なら数百行↑ 悪かった点 ● ScrollView と List で挙動が変わる ○ scrollTo の対象(ビュー単位、セル単位) ● OS バージョンでの挙動の違いやパフォーマンスの観点でリリース間際まで格闘 ○ 検証時は GeometryReader/onPreferenceChange を利用 ○ safeAreaInset なども試したがその領域の奥へのインタラクションが反応しないなど問 題あり © 2023 Wantedly, Inc.

Slide 28

Slide 28 text

Xcode Previews © 2023 Wantedly, Inc.

Slide 29

Slide 29 text

Xcode Previews Wantedly Visit アプリ ● Preview を積極的に活用して開発 ○ 動作確認しやすいのでチームで自然とそうなった ● Feature Modules + KMP 構成(に移行中) ○ ビジネスロジック(KMP)がビルド済みの XCFramework ○ 依存の少ない新規 Profile モジュール ● Preview 時は AnySwiftUIReactor に任意の State を設定してパターンを用意 © 2023 Wantedly, Inc.

Slide 30

Slide 30 text

Xcode Previews © 2023 Wantedly, Inc.

Slide 31

Slide 31 text

Xcode Previews © 2023 Wantedly, Inc.

Slide 32

Slide 32 text

Xcode Previews © 2023 Wantedly, Inc.

Slide 33

Slide 33 text

Xcode Previews 良かった点 ● ビルド時間がほぼなく快適な体験 ● 任意の State の注入によるパターンの確認しやすさ 悪かった点 ● 任意の State を注入できるため、コンポーネントの適切な設計は 要検討 ○ State のバケツリレー ■ どこまでまとまったデータで扱うか、どこでプリミティブな値で扱うか © 2023 Wantedly, Inc.

Slide 34

Slide 34 text

KMP を Swift で扱う際の短所 © 2023 Wantedly, Inc.

Slide 35

Slide 35 text

KMP を Swift で扱う際の短所 ● ネストされた型の階層の制限 ● sealed class の網羅性チェック © 2023 Wantedly, Inc.

Slide 36

Slide 36 text

KMP を Swift で扱う際の短所 ● ネストされた型の階層の制限 ● sealed class の網羅性チェック © 2023 Wantedly, Inc.

Slide 37

Slide 37 text

ネストされた型の階層の制限 © 2023 Wantedly, Inc. KMP は Objective-C のモジュールを生成する

Slide 38

Slide 38 text

ネストされた型の階層の制限 © 2023 Wantedly, Inc. KMP は Objective-C のモジュールを生成する → swift_name が複数階層をサポートしていない  

Slide 39

Slide 39 text

ネストされた型の階層の制限 © 2023 Wantedly, Inc. Swift で見ると3階層以降はまとめられてしまう Swift だと型推論を活かして `.` の左側は省略する ことが多い 階層が定義通りなら KMP の場合でも省略できるが …

Slide 40

Slide 40 text

KMP を Swift で扱う際の短所 ● ネストされた型の階層の制限 ● sealed class の網羅性チェック © 2023 Wantedly, Inc.

Slide 41

Slide 41 text

sealed class の網羅性チェック © 2023 Wantedly, Inc. Swift では default が必要になる

Slide 42

Slide 42 text

sealed class の網羅性チェック © 2023 Wantedly, Inc. Enum を再定義しコンパイルタイムで検知できるよう にインターフェースを用意

Slide 43

Slide 43 text

sealed class の網羅性チェック © 2023 Wantedly, Inc. Enum を再定義しコンパイルタイムで検知できるよう にインターフェースを用意 ※ ライブラリ管理の移行が落ち着いたら自動生成などを検討 https://github.com/icerockdev/moko-kswift → 利用するときは網羅性チェックが効くように 🎉  

Slide 44

Slide 44 text

まとめ KMP と SwiftUI ● 100% SwiftUI でリニューアルを実現 🎉 ● KMP も SwiftUI の組み合わせも(癖はありつつ)体験は 👍 ● Android も含めたモバイル開発チームの連携で大きくつまづくこともなかった 🤝 © 2023 Wantedly, Inc.

Slide 45

Slide 45 text

ご清聴ありがとうございました © 2023 Wantedly, Inc.

Slide 46

Slide 46 text

© 2023 Wantedly, Inc.