Slide 1

Slide 1 text

Swift Zoomin' #9 報告会 tochi86 (とち ) @tochi86_

Slide 2

Slide 2 text

自己紹介 @tochi86_ iOS / Android / Flutter アプリエンジニアをやっています 個人開発アプリ「アイテムリスト for あつ森」を各ストアで公開中 ドメイン駆動設計とクリーンアーキテクチャが大好きな設計オタク ソフトウェア設計を考えるときに重視しているポイント3 選 機能を変更しやすい 不具合を修正しやすい テストを追加しやすい 2

Slide 3

Slide 3 text

リファクタリング成果 https://github.com/tochi86/login-challenge Android 公式が推奨するアーキテクチャに 沿ってリファクタリングしました https://developer.android.com/jetpack /guide?hl=ja 3 層のレイヤ構造 データレイヤ ドメインレイヤ(オプション) UI レイヤ 3

Slide 4

Slide 4 text

データレイヤ リポジトリ データの永続化処理(CRUD ) 複数データソース間(ローカルと リモートなど)の競合を解決 テスト時はモックに置き換えられる データソース ファイル/ ネットワーク/ ローカルDB 1 つのデータソースのみを扱う場合は 省略可能 4

Slide 5

Slide 5 text

ドメインレイヤ データレイヤとUI レイヤの間に位置 ユースケースクラスを配置する 複雑なビジネスロジックを処理 1 クラス1 メソッド ロジックが単純な場合は省略可能 1 つのリポジトリの1 つのメソッドを 呼ぶだけ 複数のViewModel で再利用されない 5

Slide 6

Slide 6 text

UI レイヤ UI 状態(UiState ) UI を構築する元となるデータ群 状態ホルダー(ViewModel ) UI 状態を保持して、更新する 入力はイベント、出力は状態 (単方向データフロー:UDF ) UI 要素(ViewController ) データの表現方法を決める イベントを状態ホルダーに伝える 6

Slide 7

Slide 7 text

Repository の実装 モックライブラリにはMockolo を使用 protocol に @mockable を付与すると、モッククラスを自動生成 /// @mockable protocol AuthRepository: AnyObject { func login(id: String, password: String) async throws } final class AuthRepositoryImpl: AuthRepository { func login(id: String, password: String) async throws { try await AuthService.logInWith(id: id, password: password) } } 7

Slide 8

Slide 8 text

UiState の実装 不変性が求められるので、Swift では struct で定義する struct LoginUiState: Equatable { var id: String = "" var password: String = "" var isLoading: Bool = false var showHomeView: Bool = false var showErrorAlert: ErrorAlert? var isLoginButtonEnabled: Bool { return !isLoading && !id.isEmpty && !password.isEmpty } } 8

Slide 9

Slide 9 text

ViewModel の実装 final class LoginViewModel: ObservableObject { @Published private(set) var state: LoginUiState private let authRepository: AuthRepository func onLoginButtonDidTap() async { do { state.isLoading = true try await authRepository.login(id: state.id, password: state.password) state.isLoading = false state.showHomeView = true } catch { state.isLoading = false state.showErrorAlert = ErrorAlert(error: error) } } } 9

Slide 10

Slide 10 text

ViewController の実装 final class LoginViewController: UIViewController { private let viewModel = LoginViewModel() private var cancellables = Set() @IBOutlet private var loginButton: UIButton! override func viewDidLoad() { super.viewDidLoad() viewModel.$state.map(\.isLoginButtonEnabled) .removeDuplicates() .receive(on: DispatchQueue.main) .assign(to: \.isEnabled, on: loginButton) .store(in: &cancellables) } } 10

Slide 11

Slide 11 text

ViewController の実装 viewModel.$state.map(\.showErrorAlert) .removeDuplicates() .receive(on: DispatchQueue.main) .compactMap { $0 } .sink { [weak self] errorAlert in guard let self = self else { return } let alertController: UIAlertController = .init( title: errorAlert.title, message: errorAlert.message, preferredStyle: .alert ) alertController.addAction(.init(title: errorAlert.buttonTitle, style: .default, handler: nil)) self.present(alertController, animated: true) self.viewModel.onErrorAlertDidShow() } .store(in: &cancellables) 11

Slide 12

Slide 12 text

ViewController の実装 @IBAction private func loginButtonPressed(_ sender: UIButton) { Task { await viewModel.onLoginButtonDidTap() } } 詳細な実装は、GitHub のコードをご覧ください ホーム画面はSwiftUI で実装しています UIKit と全く同じアーキテクチャを適用できました UiState およびViewModel のテストも頑張って書きました こちらも合わせて読んでいただけると嬉しいです 12

Slide 13

Slide 13 text

まとめ 今回は、Android 公式のアーキテクチャガイドの素晴らしさを、 ぜひ皆さんにも知っていただきたくて発表しました Android アプリだけでなく、iOS やFlutter も含め、モバイルアプリ の開発なら汎用的に採用できるアーキテクチャです 以下の理由から、今後皆さんがアーキテクチャを考える際のベース として、活用していくことをオススメします! iOS やFlutter には、公式が推奨するアーキテクチャがない 比較的低い学習コストで、堅牢なアプリを構築できる RxSwift / Combine / Swift Concurrency / SwiftUI などの 採用する技術によらず、統一的な考え方で実装できる 13