Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Swift Zoomin' #9 報告会

とち🐹
February 12, 2022

Swift Zoomin' #9 報告会

https://swift-tweets.connpass.com/event/237293/
第2回リファクタリングチャレンジの報告会で発表したスライドです。

実装したコードはこちら。
https://github.com/tochi86/login-challenge

Android公式ドキュメントの「アプリ アーキテクチャ ガイド」を、ひたすらオススメする内容となっています。
https://developer.android.com/jetpack/guide?hl=ja

とち🐹

February 12, 2022
Tweet

More Decks by とち🐹

Other Decks in Programming

Transcript

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

    View full-size slide

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

    機能を変更しやすい
    不具合を修正しやすい
    テストを追加しやすい
    2

    View full-size slide

  3. リファクタリング成果
    https://github.com/tochi86/login-challenge
    Android
    公式が推奨するアーキテクチャに

    沿ってリファクタリングしました
    https://developer.android.com/jetpack
    /guide?hl=ja
    3
    層のレイヤ構造
    データレイヤ
    ドメインレイヤ(オプション)
    UI
    レイヤ
    3

    View full-size slide

  4. データレイヤ
    リポジトリ
    データの永続化処理(CRUD

    複数データソース間(ローカルと

    リモートなど)の競合を解決
    テスト時はモックに置き換えられる
    データソース
    ファイル/
    ネットワーク/
    ローカルDB
    1
    つのデータソースのみを扱う場合は
    省略可能
    4

    View full-size slide

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

    View full-size slide

  6. UI
    レイヤ
    UI
    状態(UiState

    UI
    を構築する元となるデータ群
    状態ホルダー(ViewModel

    UI
    状態を保持して、更新する
    入力はイベント、出力は状態

    (単方向データフロー:UDF

    UI
    要素(ViewController

    データの表現方法を決める
    イベントを状態ホルダーに伝える
    6

    View full-size slide

  7. 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

    View full-size slide

  8. 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

    View full-size slide

  9. 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

    View full-size slide

  10. 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

    View full-size slide

  11. 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

    View full-size slide

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

    View full-size slide

  13. まとめ
    今回は、Android
    公式のアーキテクチャガイドの素晴らしさを、

    ぜひ皆さんにも知っていただきたくて発表しました
    Android
    アプリだけでなく、iOS
    やFlutter
    も含め、モバイルアプリ

    の開発なら汎用的に採用できるアーキテクチャです
    以下の理由から、今後皆さんがアーキテクチャを考える際のベース

    として、活用していくことをオススメします!
    iOS
    やFlutter
    には、公式が推奨するアーキテクチャがない
    比較的低い学習コストで、堅牢なアプリを構築できる
    RxSwift / Combine / Swift Concurrency / SwiftUI
    などの

    採用する技術によらず、統一的な考え方で実装できる 13

    View full-size slide