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

Swift Concurrencyを利用したMVVMについて考える

coffmark
September 20, 2024
280

Swift Concurrencyを利用したMVVMについて考える

Chiba.swift #1で発表したスライドです。
https://chibaswift.connpass.com/event/328367

coffmark

September 20, 2024
Tweet

Transcript

  1. ໨࣍ • RxSwi'Λར༻ͨ͠MVVM • Swi' ConcurrencyΛར༻ͨ͠MVVM • ํ਑ • ࣮૷಺༰

    (APIݺͼग़͠΍ViewModelͷϝιουݺͼग़͠) • Preview • ·ͱΊ
  2. Viewͷॳظ࣮૷ struct ChibaView: View { /// Viewͷঢ়ଶ enum ViewState {

    /// ಡΈࠐΈத case loading /// ಡΈࠐΈ੒ޭ࣌ /// - response: Ϩεϙϯε /// - isLoading: ϩʔσΟϯάΠϯδέʔλͷදࣔ༗ແ case success(response: Response, isLoading: Bool) /// ಡΈࠐΈࣦഊ࣌ /// - isLoading: ϩʔσΟϯάΠϯδέʔλͷදࣔ༗ແ case error(isLoading: Bool) } var body: some View { ... } }
  3. ViewModelͷॳظ࣮૷ @MainActor final class ChibaViewModel: ObservableObject { /// ViewState @Published

    private(set) var viewState: ChibaView.ViewState = .loading /// ΤϥʔΞϥʔτදࣔϑϥά @Published var isPresentAlert: Bool = false private let repository: RepositoryProtocol init(repository: RepositoryProtocol) { ... } }
  4. APIݺͼग़͠ extension ChibaViewModel { // Actorִ཭͞Εͳ͍ nonisolated func resolveResponse() async

    throws -> Response { // (Repositoryʹ࣮૷) func resolve() -> Single<Response> try await repository.resolve().value } }
  5. ॳճಡΈࠐΈ (View࣮૷) struct ChibaView: View { @StateObject viewModel: ChibaViewModel //

    ը໘͕ॳճදࣔͨ͠λΠϛϯάͰॳճಡΈࠐΈΛ࣮ߦ͢ΔͨΊͷϑϥά @State private var viewDidLoad: Bool = false var body: some View { Group { switch viewModel.viewState { ... } } .task { if !viewDidLoad { viewDidLoad.toggle() await viewModel.initialLoad() } } } }
  6. ॳճಡΈࠐΈ (ViewModel࣮૷) final class ChibaViewModel: ObservableObject { /// ॳճಡΈࠐΈΛ࣮ߦ func

    initialLoad() async { do { let response = try await resolveResponse() // ੒ޭ࣌ͷը໘Λදࣔ self.viewState = .success(response: response, isLoading: false) } catch { // Τϥʔը໘Λදࣔ self.viewState = .error(isLoading: false) } } }
  7. Pull to Refresh (View࣮૷) struct ChibaView: View { // ...

    var body: some View { switch viewModel.viewState { case .success: successView(...) } } func successView(...) -> some View { ScrollView { ... } .refreshable { await viewModel.refresh() } } }
  8. Pull to Refresh (ViewModel࣮૷) final class ChibaViewModel: ObservableObject { @Published

    var isPresentAlert: Bool = false func refresh() async { do { let response = try await resolveResponse() // ੒ޭ࣌ͷը໘Λදࣔ self.viewState = .success(response: response, isLoading: false) } catch { // ΤϥʔΞϥʔτΛදࣔ isPresentAlert = true } } }
  9. ΤϥʔΞϥʔτͷϦτϥΠ (View࣮૷) struct ChibaView: View { var body: some View

    { Group { switch viewState { ... } } .alert("Error", isPresented: $viewModel.isPresentAlert) { Button("Cancel", role: .cancel) { } /// task(priority:_:)Ͱඇಉظؔ਺Λ࣮ߦ͢ΔϘλϯ AsyncButton { await viewModel.retry() } label: { Text("Retry") } } } }
  10. AsyncBu(on.swi- /// ඇಉظؔ਺Λ࣮ߦ͢ΔϘλϯ struct AsyncButton<Label>: View where Label: View {

    @State private var isExecuting = false let action: () async -> Void let label: () -> Label public var body: some View { Button { if isExecuting { return } isExecuting.toggle() } label: { label() } .task(id: isExecuting) { guard isExecuting else { return } await action() isExecuting.toggle() } } }
  11. ΤϥʔΞϥʔτͷϦτϥΠ (ViewModel࣮૷) final class ChibaViewModel: ObservableObject { func retry() async

    { // ϩʔσΟϯάදࣔ͢ΔͨΊviewStateͷisLoadingΛtrueʹมߋ transitionToLoading() do { let cards = try await resolveResponse() self.viewState = .success(cards: cards, isLoading: false) } catch { // ϩʔσΟϯάΛඇදࣔ͢ΔͨΊviewStateͷisLoadingΛfalseʹมߋ transitionToLoaded() isPresentAlert = true } } }
  12. Preview #Preview { // MockoloΛར༻ let repository = RepositoryProtocolMock() repository.resolve

    = { @Sendable in .just(Response(...)) } return ChibaView( viewModel: ChibaViewModel( repository: repository ) ) }
  13. ࢀߟ • Swi%UI ͷ Bu+on ͳͲͰτϦΨʔ͢Δ async ͳϝιουͷ࣮ߦ ΛɺΩϟϯηϧͷ͜ͱ·Ͱߟ࣮͑ͯ૷͢Δํ๏ 2બ

    • treastrain / Tanaka Ryoga͞Μ • h+ps:/ /zenn.dev/treastrain/arBcles/3effccd39f4056