Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Swift Concurrencyを利用したMVVMについて考える
Search
coffmark
September 20, 2024
1
430
Swift Concurrencyを利用したMVVMについて考える
Chiba.swift #1で発表したスライドです。
https://chibaswift.connpass.com/event/328367
coffmark
September 20, 2024
Tweet
Share
More Decks by coffmark
See All by coffmark
RenovateをiOSアプリ開発に導入・運用した話
coffmark
0
420
Featured
See All Featured
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.3k
Statistics for Hackers
jakevdp
799
220k
KATA
mclloyd
PRO
32
15k
Why Our Code Smells
bkeepers
PRO
340
57k
Writing Fast Ruby
sferik
630
62k
Embracing the Ebb and Flow
colly
88
4.9k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
48
9.7k
BBQ
matthewcrist
89
9.9k
Build The Right Thing And Hit Your Dates
maggiecrowley
38
2.9k
Bash Introduction
62gerente
615
210k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
10
630
Code Review Best Practice
trishagee
72
19k
Transcript
Swi$ ConcurrencyΛར༻ͨ͠ MVVMʹ͍ͭͯߟ͑Δ Chiba.swi) #1
ࣗݾհ • coffmarkͱ͍͍·͢ɻ • υϫϯΰͰಇ͍͍ͯ·͢ɻ • 22৽ଔͰ͢ɻ • ઍ༿ݝʹॅΜͰ͍·͢ɻ
ڥ • Xcode 15.4 • Swi/ 5.10 • ίϯύΠϥϑϥά •
-strict-concurrency=targeted
࣍ • RxSwi'Λར༻ͨ͠MVVM • Swi' ConcurrencyΛར༻ͨ͠MVVM • ํ • ࣮༰
(APIݺͼग़͠ViewModelͷϝιουݺͼग़͠) • Preview • ·ͱΊ
RxSwi&Λར༻ͨ͠MVVMͷશମ૾
Swi$ ConcurrencyΛ ར༻ͨ͠MVVM
ํ • task(priority:_:)Λ͍ɺΩϟϯηϧॲཧΛࣗલ࣮͢Δ͜ͱ͕গ ͳ͍ܗͰಋೖΛਐΊ͍ͨͱߟ͍͑ͯ·͢ɻ • View͕ඇදࣔʹͳͬͨλΠϛϯάͰඇಉظॲཧ͕Ωϟϯηϧ ͞ΕΔ͔ΒͰ͢ɻ
Swi$ ConcurrencyΛར༻ͨ͠MVVMͷશମ૾
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 { ... } }
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) { ... } }
APIݺͼग़͠ extension ChibaViewModel { // Actorִ͞Εͳ͍ nonisolated func resolveResponse() async
throws -> Response { // (Repositoryʹ࣮) func resolve() -> Single<Response> try await repository.resolve().value } }
Swi$ ConcurrencyΛར༻ͨ͠MVVMͷશମ૾ (࠶ܝ)
View͔ΒViewModelͷϝιουݺͼग़͠ ଞʹ͋Δͱࢥ͍·͕͢ɺࠓճҎԼͷ3ͭΛߟ͑·͢ɻ 1. ॳճಡΈࠐΈ 2. Pull to Refresh 3. ΤϥʔΞϥʔτͷϦτϥΠ
ॳճಡΈࠐΈ (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() } } } }
ॳճಡΈࠐΈ (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) } } }
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() } } }
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 } } }
ΤϥʔΞϥʔτͷϦτϥΠ (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") } } } }
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() } } }
ΤϥʔΞϥʔτͷϦτϥΠ (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 } } }
Preview #Preview { // MockoloΛར༻ let repository = RepositoryProtocolMock() repository.resolve
= { @Sendable in .just(Response(...)) } return ChibaView( viewModel: ChibaViewModel( repository: repository ) ) }
·ͱΊ • task(priority:_:)Λར༻ͯ͠ΩϟϯηϧॲཧΛࣗલ࣮͢Δ͜ͱ͕ গͳ͍ܗͰSwi1 ConcurrencyಋೖΛਐΊ͍ͯ͘͜ͱ͕Ͱ͖·͠ ͨɻ • طଘ࣮ΑΓϩʔΧϧมେ෯ʹগͳ͘ͳΔܗͰViewModel Λ࣮͢Δ͜ͱ͕Ͱ͖·ͨ͠ɻ
ࠓޙ • ը໘ભҠपΓSwi%UIʹد͍͖͍ͤͯͨͱߟ͍͑ͯ·͢ɻ
ࢀߟ • Swi%UI ͷ Bu+on ͳͲͰτϦΨʔ͢Δ async ͳϝιουͷ࣮ߦ ΛɺΩϟϯηϧͷ͜ͱ·Ͱߟ࣮͑ͯ͢Δํ๏ 2બ
• treastrain / Tanaka Ryoga͞Μ • h+ps:/ /zenn.dev/treastrain/arBcles/3effccd39f4056
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ɻ