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
410
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
410
Featured
See All Featured
Stop Working from a Prison Cell
hatefulcrawdad
271
21k
Measuring & Analyzing Core Web Vitals
bluesmoon
8
540
Bootstrapping a Software Product
garrettdimon
PRO
307
110k
GitHub's CSS Performance
jonrohan
1031
460k
Agile that works and the tools we love
rasmusluckow
329
21k
Building an army of robots
kneath
306
45k
How to train your dragon (web standard)
notwaldorf
96
6.2k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
18
1.1k
Automating Front-end Workflow
addyosmani
1370
200k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
21
1.4k
How GitHub (no longer) Works
holman
314
140k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
229
22k
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
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ɻ