TCAのAlert実装でハマったポイントと解決策
はじめに
最近TCAの勉強を始めたばかりなのですが、alertの実装で行き詰まって1週間くらい
解決できず、苦労しました。。。
ただその過程でTCAについての知識が少し深まったので、その内容を記事にしてみました。
対象としては、TCAのチュートリアルをやり始めた人、または一通りやり終えた人を想定しています。
気になる点や間違いなどがございましたらコメントいただけると幸いです。
今回ハマったポイント
では、早速今回私が行き詰まったエラーの部分を見てみましょう!
import SwiftUI
import ComposableArchitecture
struct AlertView: View {
@Bindable var store: StoreOf<AlertReducer>
var body: some View {
VStack {
Button(action: {
store.send(.alertButtonTapped)
}, label: {
Text("Button")
})
}// 🔥🔥ここでエラーが発生🔥🔥
.alert(store: $store.scope(state: \.alert, action: \.alert))
}
}
ちなみに、Reducerは正しく実装されている前提で、Viewのエラーだけを探してみてください!
今回alertの部分でエラーが出ており、メッセージは下記のようになっています。
Cannot convert value of type Binding<Store<AlertState<AlertReducer.Action.Alert>, AlertReducer.Action.Alert>?> to expected argument type Store<PresentationState<AlertState<ButtonAction>>, PresentationAction<ButtonAction>>
おそらくタイプエラーなのは分かったものの、どう修正したらいいのか分からず。。。
TCAのチュートリアルそのまま書いたつもりなのに(泣)
どうですか?みなさんはエラーの原因わかりましたでしょうか?
では、まずエラーの原因だけ先に確認しましょう。
エラーの原因と解決策
エラーの原因自体は至ってシンプルで、alert(_:)
ではなくalert(store:)
を使っていたからでした。
なので、先ほどのコードのalertの部分を下記に修正したらエラーは解消できます。
.alert($store.scope(state: \.alert, action: \.alert))
間違い自体は初心者がやりがちなミスと言えると思いますが、「TCAでいかにも使いそうなstoreを引数に持つものもあるなんて意地悪だな(笑)」とか思ったので、ここからもう少し根本の原因を探っていきたいと思います。
そもそもこの.alert(store:)
というのが何か確認していきましょう。
alert(store:)
って何?
その答えはTCAのドキュメントのMigrating to 1.7に記載されてました。
ここに取り上げられてるテーマは
Replacing navigation view modifiers with SwiftUI modifiersで、
TCAでこれまで使っていたTCA独自のnavigation view modifiersからSwiftUI Modifierに移行しようという趣旨のものです。
さらにalertについて、このページには以下の記載があります。
The
alert(store:)
andconfirmationDialog(store:)
modifiers have been used to drive alerts and dialogs from stores, but new modifiers are now available that can drive alerts and dialogs from the same store binding scope operation that can power vanilla SwiftUI presentation, likesheet(item:)
.
alert(store:)
はTCA用で以前まで使われていたmodifierで、SwiftUI標準のalert(_:)
がTCAでも使用できるようになったということです。
つまりalert(store:)
は少し前までの書き方だったということが、ようやく分かりました。
ちなみにalert(store:)
を使った場合は、下記のようにすると実装できます。
import SwiftUI
import ComposableArchitecture
struct AlertView: View {
// storeの@Bindableをなくす
let store: StoreOf<AlertReducer>
var body: some View {
VStack {
Button(action: {
store.send(.addButtonTapped)
}, label: {
Text("Button")
})
}// bidingしてる$の位置も変わってることに注意‼️
.alert(store: store.scope(state:\.$alert , action: \.alert))
}
}
TCAの進化の変遷を見ることで、少し理解が深まったのではないでしょうか?
TCAは日々アップデートされており、古い書き方のコードを目にする機会もあるので
知っておいて損はないと思います。
なぜ@Bindable
をなくすのか?やなぜ$
の位置が変わるのか?はBindingの実装の変遷やWithViewStoreの話に発展するものなので、また別の機会に記事にしたいと思います。
今回はalertを題材に話をしましたが、TCAのドキュメントのMigrating to 1.7には
sheet(item:)
やNavigationStack
も同様にstoreをbindingするだけでいいと記載があります。
要するに、stateではなくstoreをbindingするっていうことを確認してもらえれば十分です。
// sheet
.sheet(item: $store.scope(
state: \.destination?.editForm,
action: \.destination.editForm
)
) { store in
ChildView(store: store)
}
// NavigationStack
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
RootView()
} destination: { store in
//ここに遷移先のViewを書く
}
}
詳しいことが気になる方は、TCAのドキュメントのMigrating to 1.7をご確認ください。
ここまで少し長くなってしましたが、お付き合いいただきありがとうございます。
最後に新しいalertの実装例を下記に載せましたので、参考にしていただければと思います。
Alertの実装例
import SwiftUI
import ComposableArchitecture
// View
struct AlertView: View {
@Bindable var store: StoreOf<AlertReducer>
var body: some View {
VStack {
Button(action: {
store.send(.alertButtonTapped)
}, label: {
Text("Button")
})
}
.alert($store.scope(state: \.alert, action: \.alert))
}
}
// Reducer
@Reducer
struct AlertReducer {
@ObservableState
struct State: Equatable {
@Presents var alert: AlertState<Action.Alert>?
}
enum Action {
case alertButtonTapped
case alert(PresentationAction<Alert>)
enum Alert: Equatable {
case confirmButtonTapped
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .alertButtonTapped:
state.alert = AlertState {
TextState("Alert")
} actions: {
ButtonState(role: .cancel) {
TextState("キャンセル")
}
ButtonState(action: .confirmButtonTapped) {
TextState("OK")
}
} message: {
TextState("アラートを消しますか?")
}
return .none
case .alert(.presented(.confirmButtonTapped)):
return .none
case .alert:
return .none
}
}
.ifLet(\.$alert, action: \.alert)
}
}
まとめ
alert(store:)
とalert(_:)
を間違えない!- 慣れてきたらTCAのマイグレーションにも目を通そう!
参考記事
Discussion