🌾
[Swift] [Combine] ローディング状態を表す enum に値を持たせない方法
伝えたいこと
- ローディング状態を表す enum に値を持たせない方法
- 『値』と『ローディング状態を表す enum』 を分離する
- 『値』のストリームに対応して、『ローディング状態を表す enum』を更新する
- 宣言的な記述ができる
Combine
で記述することがオススメ(didSet
はやめよう!)
- 宣言的な記述ができる
- ローディング状態を表す enum に値を持たせないことのメリット
- ViewModel側のメリット: 『APIの呼び出し』と『ローディング状態の更新』を分けて記述することができる
- View側のメリット: 『値』と『ローディング状態を表す enum』 の関心事を分離できる
- 前提として、ローディング状態を表す enum に値を持たせない方がよい(はず)
- (というよりもそもそもローディング状態を表す enum は必要であれば、その値を管理すべきであるが、画面ごとにローディング状態を管理する必要はなく、共通のローディングを描画する UI コンポーネントのみで、その値を用いるようにした方が良い)(この記事ではそれについて特に触れていません。)
前提
以下のような、HogeResponse
と MogeResponse
の2つの API のレスポンスとして受け取とる画面において、画面側でローディングの状態に合わせて UI を変化させるための変数(LoadState
)を公開する ObservableObject
を適応させた class を考えます。
struct HogeResponse {
let hoge: Int
}
struct MogeResponse {
let moge: Int
}
struct TestError: Error {}
(パターン1)ローディング状態を表す enum に値を持たせた場合
ViewModel の実装
enum LoadState {
case notLoad
case loading
case loaded(Result<(HogeResponse, MogeResponse), TestError>) // 値を持たせる
}
class ViewModel: ObservableObject {
@Published private(set) var loadState: LoadState = .notLoad
func onAppear() async {
loadState = .loading
// API の呼び出し(省略した記述)
// HogeResponse の取得
let hogeResponse = HogeResponse(hoge: 1)
// MogeResponse の取得
let mogeResponse = MogeResponse(moge: 2)
loadState = .loaded(.success((hogeResponse, mogeResponse)))
}
}
View 側のハンドリング
var cancellables: Set<AnyCancellable> = []
let viewModel = ViewModel()
viewModel.$loadState
.sink { completion in
switch completion {
case .finished:
print("finished: \(completion)")
case let .failure(error):
print("error: \(error)")
}
} receiveValue: { result in
switch result {
case .notLoad:
print("notLoad: \(result)")
case .loading:
print("loading: \(result)")
case .loaded(.success((let hogeResponse, let mogeResponse))):
print("loaded: (hogeResponse: \(hogeResponse), mogeResponse: \(mogeResponse))")
case .loaded(.failure(let error)):
print("error: \(error)")
}
}
.store(in: &cancellables)
ローディングの処理
Task {
await viewModel.onAppear()
}
// notLoad: notLoad
// loading: loading
// loaded: (hogeResponse: HogeResponse(hoge: 1), mogeResponse: MogeResponse(moge: 2))
(パターン2)ローディング状態を表す enum に値を持たせない場合
ViewModel の実装
enum LoadState {
case notLoad
case loading
case loaded // 値を持たせない
}
class ViewModel: ObservableObject {
@Published private(set) var loadState: LoadState = .notLoad
@Published private(set) var error: TestError?
@Published private(set) var hogeResponse: HogeResponse?
@Published private(set) var mogeResponse: MogeResponse?
private var cancellables: Set<AnyCancellable> = []
init() {
// ローディングのステータスの更新を $hogeResponse と $mogeResponse のストリームから生成する
$hogeResponse
.combineLatest($mogeResponse)
.map { hogeResponse, mogeResponse -> LoadState in
switch (hogeResponse, mogeResponse) {
case (.none, .none):
return .notLoad
case (.some, .none):
return .loading
case (.none, .some):
return .loading
case (.some, .some):
return .loaded
}
}
.removeDuplicates()
.assign(to: \.loadState, on: self)
.store(in: &cancellables)
}
func onAppear() async {
// API の呼び出し(省略した記述)
// HogeResponse の取得
hogeResponse = HogeResponse(hoge: 1)
// MogeResponse の取得
mogeResponse = MogeResponse(moge: 2)
}
}
一見、複雑になりましたが、init()
では hogeResponse
や mogeResponse
の値に応じて loadState
の値を更新して、onAppear()
では API の呼び出しの処理をしているだけになります。
パターン1
と比較して、記述量は増えましたが、onAppear()
でやることが、API の呼び出しに集中することができます。
View 側のハンドリング
パターン1
と比較して、View側の記述が圧倒的に減ります。
var cancellables: Set<AnyCancellable> = []
let viewModel = ViewModel()
viewModel.$loadState
.sink(receiveValue: { print("loadState: \($0)") })
.store(in: &cancellables)
viewModel.$error
.compactMap { $0 }
.sink(receiveValue: { print("error: \($0)") })
.store(in: &cancellables)
viewModel.$hogeResponse
.compactMap { $0 }
.sink(receiveValue: { print("hogeResponse: \($0)") })
.store(in: &cancellables)
viewModel.$mogeResponse
.compactMap { $0 }
.sink(receiveValue: { print("mogeResponse: \($0)") })
.store(in: &cancellables)
ローディングの処理
Task {
await viewModel.onAppear()
}
// loadState: notLoad
// loadState: loading
// hogeResponse: HogeResponse(hoge: 1)
// loadState: loaded
// mogeResponse: MogeResponse(moge: 2)
伝えたいこと(おさらい)
- ローディング状態を表す enum に値を持たせない方法
- 『値』と『ローディング状態を表す enum』 を分離する
- 『値』のストリームに対応して、『ローディング状態を表す enum』を更新する
- 宣言的な記述ができる
Combine
で記述することがオススメ(didSet
はやめよう!)
- 宣言的な記述ができる
- ローディング状態を表す enum に値を持たせないことのメリット
- ViewModel側のメリット: 『APIの呼び出し』と『ローディング状態の更新』を分けて記述することができる
- View側のメリット: 『値』と『ローディング状態を表す enum』 の関心事を分離できる
- 前提として、ローディング状態を表す enum に値を持たせない方がよい(はず)
- (というよりもそもそもローディング状態を表す enum は必要であれば、その値を管理すべきであるが、画面ごとにローディング状態を管理する必要はなく、共通のローディングを描画する UI コンポーネントのみで、その値を用いるようにした方が良い)(この記事ではそれについて特に触れていません。)
以上です。
Discussion