動作
実装
App.swift
import SwiftUI
import ComposableArchitecture
@main
struct testTCAApp: App {
var body: some Scene {
WindowGroup {
ContentView(
store: Store(initialState: RockScissorPaper.State()) {
RockScissorPaper()
}
)
}
}
}
ContentView.swift
import SwiftUI
import ComposableArchitecture
struct ContentView: View {
let store: StoreOf<RockScissorPaper>
var body: some View {
VStack {
Spacer()
if let result = store.state.result {
Text(result.description)
.font(.system(size: 64))
.multilineTextAlignment(.center)
}
Spacer()
HStack {
VStack {
Text(store.handOfPlayerA?.rawValue ?? "?")
.fixedSize()
.font(.system(size: 64))
Button("Random Hand for A") {
store.send(
.setHand(.random(), to: .playerA)
)
}
}
VStack {
Text(store.handOfPlayerB?.rawValue ?? "?")
.fixedSize()
.font(.system(size: 64))
Button("Random Hand for B") {
store.send(
.setHand(.random(), to: .playerB)
)
}
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
#Preview {
ContentView(
store: Store(initialState: RockScissorPaper.State()) {
RockScissorPaper()
}
)
}
RockScissorPaper.swift
import ComposableArchitecture
@Reducer
struct RockScissorPaper {
@ObservableState
struct State: Equatable {
var handOfPlayerA: Hand?
var handOfPlayerB: Hand?
var result: GameResult?
}
enum Action {
case setHand(Hand, to: Player)
case checkHands
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .setHand(hand, to: player):
switch player {
case .playerA: state.handOfPlayerA = hand
case .playerB: state.handOfPlayerB = hand
}
return .send(.checkHands)
case .checkHands:
guard let handOfPlayerA = state.handOfPlayerA,
let handOfPlayerB = state.handOfPlayerB else {
return .none
}
let playerAOutcome = handOfPlayerA.compete(with: handOfPlayerB)
switch playerAOutcome {
case .win: state.result = GameResult(winner: .playerA)
case .lose: state.result = GameResult(winner: .playerB)
case .draw: state.result = GameResult(winner: nil)
}
return .none
}
}
}
}
struct GameResult: Equatable{
let winner: Player?
var description: String {
switch winner {
case .playerA: "winner \nA!"
case .playerB: "winner \nB!"
case .none: "Drew!"
}
}
}
enum Player {
case playerA
case playerB
}
enum Hand: String {
case rock = "✊"
case scissor = "✌️"
case paper = "🖐️"
func compete(with hand: Hand) -> Outcome {
guard self != hand else { return .draw }
return switch self {
case .rock: hand == .scissor ? .win : .lose
case .scissor: hand == .paper ? .win : .lose
case .paper: hand == .rock ? .win : .lose
}
}
}
extension Hand: CaseIterable {
static func random() -> Hand {
Hand.allCases.randomElement()!
}
}
enum Outcome {
case win
case lose
case draw
}
感想
自分の場合、MVVMではViewから使いたいAPIの形から考えて実装していたが、構造が少し複雑になると途端にどのように部品同士を結合するかで混乱していた。TCAを用いると、Stateに状態を、Actionに動作を定義し、ReducerにActionの実装を書けばよい、とかっちりと型が決まっているため、その点においては楽かもしれない。多少冗長な気もするが一貫性を保ちやすそうなので、拡張性の高さが求められる大規模なプロジェクトに向いている気がする。
まだ副作用に関してはきちんと理解していないので、そちらも調べていきたい。
参考資料