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
400
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
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
10
930
Faster Mobile Websites
deanohume
307
31k
The Straight Up "How To Draw Better" Workshop
denniskardys
234
140k
Building Better People: How to give real-time feedback that sticks.
wjessup
367
19k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
130
19k
Documentation Writing (for coders)
carmenintech
72
4.9k
The World Runs on Bad Software
bkeepers
PRO
69
11k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
48
2.8k
Art, The Web, and Tiny UX
lynnandtonic
299
21k
Designing Experiences People Love
moore
142
24k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
507
140k
Making Projects Easy
brettharned
116
6.3k
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
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ɻ