From daaeed1295be8c0f8b1e4a8aa5aa3a1f27bf6ab6 Mon Sep 17 00:00:00 2001 From: Satoshi Komatsu Date: Tue, 10 Mar 2026 12:07:28 +0900 Subject: [PATCH] refactor: remove TCA dependency and migrate to @Observable MVVM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace all Reducers (Home, Result, PastResults, Counter, Initial, Round) with @Observable ViewModels using Swift Concurrency - Fix iPhoneConnecter nested delegate function bug and AsyncStream overwrite bug using AsyncStream.makeStream() pattern - Fix WatchConnecter typo: isGmaeStarted → isGameStarted - Fix RoundView Finish button that was incorrectly commented out - Convert RoundModel from NSObject class to struct with Equatable conformance - Remove TCA + 12 transitive dependencies from Package.resolved and project.pbxproj - Update CommonModel minimum watchOS target from v6 to v10 26 files changed, +273/-721 lines --- CommonModel/Package.swift | 2 +- .../Sources/CommonModel/RoundModel.swift | 19 ++- .../Models/WatchConnecter.swift | 12 +- .../Scenes/Counter/CounterReducer.swift | 42 ------ .../Scenes/Counter/CounterView.swift | 16 +-- .../Scenes/Counter/CounterViewModel.swift | 28 ++++ .../Scenes/Initial/InitialReducer.swift | 58 --------- .../Scenes/Initial/InitialView.swift | 27 ++-- .../Scenes/Initial/InitialViewModel.swift | 33 +++++ .../Scenes/Round/RoundReducer.swift | 70 ---------- .../Scenes/Round/RoundView.swift | 37 +++--- .../Scenes/Round/RoundViewModel.swift | 54 ++++++++ WhitePanda Watch App/WhitePandaApp.swift | 7 +- WhitePanda.xcodeproj/project.pbxproj | 27 +--- .../xcshareddata/swiftpm/Package.resolved | 120 +----------------- .../xcshareddata/swiftpm/Package.resolved | 120 +----------------- WhitePanda/Models/iPhoneConnecter.swift | 15 +-- WhitePanda/Scenes/Home/HomeReducer.swift | 91 ------------- WhitePanda/Scenes/Home/HomeView.swift | 41 +++--- WhitePanda/Scenes/Home/HomeViewModel.swift | 42 ++++++ .../PastResults/PastResultsReducer.swift | 31 ----- .../Scenes/PastResults/PastResultsView.swift | 9 +- WhitePanda/Scenes/Result/ResultReducer.swift | 44 ------- WhitePanda/Scenes/Result/ResultView.swift | 15 +-- .../Scenes/Result/ResultViewModel.swift | 27 ++++ WhitePanda/WhitePandaApp.swift | 7 +- 26 files changed, 273 insertions(+), 721 deletions(-) delete mode 100644 WhitePanda Watch App/Scenes/Counter/CounterReducer.swift create mode 100644 WhitePanda Watch App/Scenes/Counter/CounterViewModel.swift delete mode 100644 WhitePanda Watch App/Scenes/Initial/InitialReducer.swift create mode 100644 WhitePanda Watch App/Scenes/Initial/InitialViewModel.swift delete mode 100644 WhitePanda Watch App/Scenes/Round/RoundReducer.swift create mode 100644 WhitePanda Watch App/Scenes/Round/RoundViewModel.swift delete mode 100644 WhitePanda/Scenes/Home/HomeReducer.swift create mode 100644 WhitePanda/Scenes/Home/HomeViewModel.swift delete mode 100644 WhitePanda/Scenes/PastResults/PastResultsReducer.swift delete mode 100644 WhitePanda/Scenes/Result/ResultReducer.swift create mode 100644 WhitePanda/Scenes/Result/ResultViewModel.swift diff --git a/CommonModel/Package.swift b/CommonModel/Package.swift index 9af4bad..d00d022 100644 --- a/CommonModel/Package.swift +++ b/CommonModel/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Model", platforms: [ .iOS(.v17), - .watchOS(.v6) + .watchOS(.v10) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. diff --git a/CommonModel/Sources/CommonModel/RoundModel.swift b/CommonModel/Sources/CommonModel/RoundModel.swift index d2a5ea9..86b6707 100644 --- a/CommonModel/Sources/CommonModel/RoundModel.swift +++ b/CommonModel/Sources/CommonModel/RoundModel.swift @@ -1,31 +1,28 @@ // -// File.swift -// +// RoundModel.swift +// // // Created by Satoshi Komatsu on 2024/04/13. // import Foundation -public final class RoundModel: NSObject { +public struct RoundModel: Equatable { public let type: RoundType public var counts: [Int: Int] = [:] -// public var totalScore: Int { -// return counts.reduce(0) { -// $0 + $1.count -// } -// } + public var totalScore: Int { + counts.values.reduce(0, +) + } public init(type: RoundType) { self.type = type } } -// FIXME: This cound be replaced with Int, but use struct just for further updates +// FIXME: This could be replaced with Int, but use struct just for further updates public struct CountModel: Equatable, Identifiable { public var id: UUID = UUID() - var count: Int -// var per: Int + public var count: Int public init(count: Int) { self.count = count diff --git a/WhitePanda Watch App/Models/WatchConnecter.swift b/WhitePanda Watch App/Models/WatchConnecter.swift index 7673f1a..2503c39 100644 --- a/WhitePanda Watch App/Models/WatchConnecter.swift +++ b/WhitePanda Watch App/Models/WatchConnecter.swift @@ -13,12 +13,12 @@ final class WatchConnecter: NSObject { static let shared: WatchConnecter = .init() private let session: WCSession - private var isGmaeStartedContinuation: AsyncStream.Continuation? - var isGmaeStartedStream: AsyncStream { + private var isGameStartedContinuation: AsyncStream.Continuation? + private(set) lazy var isGameStartedStream: AsyncStream = { AsyncStream { continuation in - self.isGmaeStartedContinuation = continuation + self.isGameStartedContinuation = continuation } - } + }() override init() { self.session = WCSession.default @@ -43,8 +43,8 @@ extension WatchConnecter: WCSessionDelegate { func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { print("========Message") print(message) - if let isGmaeStarted = message["isGameStarted"] as? Bool { - isGmaeStartedContinuation?.yield(isGmaeStarted) + if let isGameStarted = message["isGameStarted"] as? Bool { + isGameStartedContinuation?.yield(isGameStarted) replyHandler(["GameStarted": true]) } } diff --git a/WhitePanda Watch App/Scenes/Counter/CounterReducer.swift b/WhitePanda Watch App/Scenes/Counter/CounterReducer.swift deleted file mode 100644 index e94fa65..0000000 --- a/WhitePanda Watch App/Scenes/Counter/CounterReducer.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// CounterReducer.swift -// WhitePanda Watch App -// -// Created by satoshi on 2024/04/01. -// - -import Foundation -import ComposableArchitecture - -@Reducer -struct Counter { - - @ObservableState - struct State: Equatable { - var count = 0 - } - - enum Action { - case decrementButtonTapped - case incrementButtonTapped - case resetButtonTapped - } - - var body: some Reducer { - Reduce { state, action in - switch action { - case .decrementButtonTapped: - if state.count > 0 { - state.count -= 1 - } - return .none - case .incrementButtonTapped: - state.count += 1 - return .none - case .resetButtonTapped: - state.count = 0 - return .none - } - } - } -} diff --git a/WhitePanda Watch App/Scenes/Counter/CounterView.swift b/WhitePanda Watch App/Scenes/Counter/CounterView.swift index 0e489cb..f9ca4ba 100644 --- a/WhitePanda Watch App/Scenes/Counter/CounterView.swift +++ b/WhitePanda Watch App/Scenes/Counter/CounterView.swift @@ -6,15 +6,14 @@ // import SwiftUI -import ComposableArchitecture struct CounterView: View { - @Bindable var store: StoreOf + @State var viewModel = CounterViewModel() var body: some View { VStack { - Text("\(store.count)") + Text("\(viewModel.count)") .font(.title) .fontWeight(.semibold) @@ -22,7 +21,7 @@ struct CounterView: View { VStack { Button(action: { - store.send(.incrementButtonTapped) + viewModel.increment() }, label: { Text("+") .font(.title2) @@ -30,14 +29,14 @@ struct CounterView: View { HStack { Button(action: { - store.send(.decrementButtonTapped) + viewModel.decrement() }, label: { Text("-") .font(.title3) }) Button(action: { - store.send(.resetButtonTapped) + viewModel.reset() }, label: { Text("Reset") }) @@ -49,8 +48,5 @@ struct CounterView: View { } #Preview { - CounterView( - store: Store(initialState: Counter.State()) { - Counter() - }) + CounterView() } diff --git a/WhitePanda Watch App/Scenes/Counter/CounterViewModel.swift b/WhitePanda Watch App/Scenes/Counter/CounterViewModel.swift new file mode 100644 index 0000000..a33eee0 --- /dev/null +++ b/WhitePanda Watch App/Scenes/Counter/CounterViewModel.swift @@ -0,0 +1,28 @@ +// +// CounterViewModel.swift +// WhitePanda Watch App +// +// Created by satoshi on 2024/04/01. +// + +import Foundation +import Observation + +@Observable +final class CounterViewModel { + var count = 0 + + func increment() { + count += 1 + } + + func decrement() { + if count > 0 { + count -= 1 + } + } + + func reset() { + count = 0 + } +} diff --git a/WhitePanda Watch App/Scenes/Initial/InitialReducer.swift b/WhitePanda Watch App/Scenes/Initial/InitialReducer.swift deleted file mode 100644 index e7d4277..0000000 --- a/WhitePanda Watch App/Scenes/Initial/InitialReducer.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// InitialReducer.swift -// WhitePanda Watch App -// -// Created by satoshi on 2024/04/08. -// - -import Foundation -import ComposableArchitecture - -@Reducer -struct InitialFeature { - - @ObservableState - struct State: Equatable { - @Presents var counterState: Counter.State? - @Presents var roundState: RoundFeature.State? - } - - enum Action { - case createFreeButtonTapped - case createFree(PresentationAction) - case recivedGameStart - case roundStart(PresentationAction) - case onAppear - } - - var watchConnecter: WatchConnecter - - var body: some Reducer { - Reduce { state, action in - switch action { - case .createFreeButtonTapped: - state.counterState = Counter.State() - return .none - case .createFree: - return .none - case .recivedGameStart: - state.roundState = RoundFeature.State() - return .none - case .roundStart: - return .none - case .onAppear: - return .run { send in - for await _ in self.watchConnecter.isGmaeStartedStream { - await send(.recivedGameStart) - } - } - } - } - .ifLet(\.$counterState, action: \.createFree) { - Counter() - } - .ifLet(\.$roundState, action: \.roundStart) { - RoundFeature(connecter: watchConnecter) - } - } -} diff --git a/WhitePanda Watch App/Scenes/Initial/InitialView.swift b/WhitePanda Watch App/Scenes/Initial/InitialView.swift index 43fcb6b..48281db 100644 --- a/WhitePanda Watch App/Scenes/Initial/InitialView.swift +++ b/WhitePanda Watch App/Scenes/Initial/InitialView.swift @@ -6,11 +6,10 @@ // import SwiftUI -import ComposableArchitecture struct InitialView: View { - @Bindable var store: StoreOf + @State var viewModel = InitialViewModel() var body: some View { VStack { @@ -18,27 +17,25 @@ struct InitialView: View { .multilineTextAlignment(.center) Button(action: { - store.send(.createFreeButtonTapped) + viewModel.createFreeButtonTapped() }, label: { Text("Or free count mode") }) } - .navigationDestination(item: $store.scope(state: \.counterState, action: \.createFree), destination: { store in - CounterView(store: store) - }) - .navigationDestination(item: $store.scope(state: \.roundState, action: \.roundStart), destination: { store in - RoundView(store: store) - }) + .navigationDestination(isPresented: $viewModel.showCounter) { + CounterView() + } + .navigationDestination(isPresented: $viewModel.showRound) { + RoundView() + } .onAppear { - store.send(.onAppear) + viewModel.onAppear() } } } #Preview { - InitialView( - store: Store(initialState: InitialFeature.State()) { - InitialFeature(watchConnecter: .init()) - } - ) + NavigationStack { + InitialView() + } } diff --git a/WhitePanda Watch App/Scenes/Initial/InitialViewModel.swift b/WhitePanda Watch App/Scenes/Initial/InitialViewModel.swift new file mode 100644 index 0000000..5fbb81f --- /dev/null +++ b/WhitePanda Watch App/Scenes/Initial/InitialViewModel.swift @@ -0,0 +1,33 @@ +// +// InitialViewModel.swift +// WhitePanda Watch App +// +// Created by satoshi on 2024/04/08. +// + +import Foundation +import Observation + +@Observable +final class InitialViewModel { + var showCounter = false + var showRound = false + + private let watchConnecter: WatchConnecter + + init(watchConnecter: WatchConnecter = .shared) { + self.watchConnecter = watchConnecter + } + + func onAppear() { + Task { @MainActor in + for await _ in watchConnecter.isGameStartedStream { + showRound = true + } + } + } + + func createFreeButtonTapped() { + showCounter = true + } +} diff --git a/WhitePanda Watch App/Scenes/Round/RoundReducer.swift b/WhitePanda Watch App/Scenes/Round/RoundReducer.swift deleted file mode 100644 index 4157ac2..0000000 --- a/WhitePanda Watch App/Scenes/Round/RoundReducer.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// RoundReducer.swift -// WhitePanda Watch App -// -// Created by satoshi on 2024/04/13. -// - -import Foundation -import ComposableArchitecture -import Model - -@Reducer -struct RoundFeature { - - @ObservableState - struct State: Equatable { - var count: Int = 0 - var roundCount: Int = 1 - var round: Model.RoundModel = .init(type: .full) - } - - enum Action { - // ViewEvents - case decrementButtonTapped - case incrementButtonTapped - case resetButtonTapped - case goNextButtonTapped - case finishButtonTapped - case goPreviousButtonTapped - - // transitions - case resultScene - } - - let connecter: WatchConnecter - - var body: some Reducer { - Reduce { state, action in - switch action { - case .decrementButtonTapped: - if state.count > 0 { - state.count -= 1 - } - return .none - case .incrementButtonTapped: - state.count += 1 - return .none - case .resetButtonTapped: - state.count = 0 - return .none - case .goNextButtonTapped: - state.round.counts[state.roundCount] = state.count - connecter.sendScore(context: ["round": state.round.counts]) - state.roundCount += 1 - state.count = state.round.counts[state.roundCount] ?? 0 - return .none - case .finishButtonTapped: - state.round.counts[state.roundCount] = state.count - connecter.sendScore(context: ["round": state.round.counts]) - return .none - case .goPreviousButtonTapped: - state.roundCount -= 1 - state.count = state.round.counts[state.roundCount] ?? 0 - return .none - case .resultScene: - return .none - } - } - } -} diff --git a/WhitePanda Watch App/Scenes/Round/RoundView.swift b/WhitePanda Watch App/Scenes/Round/RoundView.swift index a9919df..ef4c791 100644 --- a/WhitePanda Watch App/Scenes/Round/RoundView.swift +++ b/WhitePanda Watch App/Scenes/Round/RoundView.swift @@ -6,19 +6,18 @@ // import SwiftUI -import ComposableArchitecture struct RoundView: View { - @Bindable var store: StoreOf + @State var viewModel = RoundViewModel() var body: some View { ScrollView { VStack { - Text("Hole: \(store.roundCount)") + Text("Hole: \(viewModel.roundCount)") .font(.title3) .fontWeight(.regular) - Text("\(store.count)") + Text("\(viewModel.count)") .font(.title) .fontWeight(.semibold) @@ -26,30 +25,30 @@ struct RoundView: View { VStack { Button(action: { - store.send(.incrementButtonTapped) + viewModel.increment() }, label: { Text("+") .font(.title2) }) HStack { - if store.roundCount > 1 { + if viewModel.roundCount > 1 { Button(action: { - store.send(.goPreviousButtonTapped) + viewModel.goPrevious() }, label: { Text("Back") }) } - if store.round.type.rawValue != store.roundCount { -// Button(action: { -// store.send(.goNextButtonTapped) -// }, label: { -// Text("Finish") -// }) -// } else { + if viewModel.round.type.rawValue == viewModel.roundCount { Button(action: { - store.send(.goNextButtonTapped) + viewModel.finish() + }, label: { + Text("Finish") + }) + } else { + Button(action: { + viewModel.goNext() }, label: { Text("Next") }) @@ -58,14 +57,14 @@ struct RoundView: View { HStack { Button(action: { - store.send(.decrementButtonTapped) + viewModel.decrement() }, label: { Text("-") .font(.title3) }) Button(action: { - store.send(.resetButtonTapped) + viewModel.reset() }, label: { Text("Reset") }) @@ -78,7 +77,5 @@ struct RoundView: View { } #Preview { - RoundView(store: .init(initialState: RoundFeature.State(round: .init(type: .full)), reducer: { - RoundFeature(connecter: WatchConnecter.shared) - })) + RoundView() } diff --git a/WhitePanda Watch App/Scenes/Round/RoundViewModel.swift b/WhitePanda Watch App/Scenes/Round/RoundViewModel.swift new file mode 100644 index 0000000..70f4730 --- /dev/null +++ b/WhitePanda Watch App/Scenes/Round/RoundViewModel.swift @@ -0,0 +1,54 @@ +// +// RoundViewModel.swift +// WhitePanda Watch App +// +// Created by satoshi on 2024/04/13. +// + +import Foundation +import Observation +import Model + +@Observable +final class RoundViewModel { + var count: Int = 0 + var roundCount: Int = 1 + var round: Model.RoundModel = .init(type: .full) + + private let connecter: WatchConnecter + + init(connecter: WatchConnecter = .shared) { + self.connecter = connecter + } + + func increment() { + count += 1 + } + + func decrement() { + if count > 0 { + count -= 1 + } + } + + func reset() { + count = 0 + } + + func goNext() { + round.counts[roundCount] = count + connecter.sendScore(context: ["round": round.counts]) + roundCount += 1 + count = round.counts[roundCount] ?? 0 + } + + func finish() { + round.counts[roundCount] = count + connecter.sendScore(context: ["round": round.counts]) + } + + func goPrevious() { + roundCount -= 1 + count = round.counts[roundCount] ?? 0 + } +} diff --git a/WhitePanda Watch App/WhitePandaApp.swift b/WhitePanda Watch App/WhitePandaApp.swift index b52f6b6..a4446d2 100644 --- a/WhitePanda Watch App/WhitePandaApp.swift +++ b/WhitePanda Watch App/WhitePandaApp.swift @@ -6,18 +6,13 @@ // import SwiftUI -import ComposableArchitecture @main struct WhitePanda_Watch_AppApp: App { var body: some Scene { WindowGroup { NavigationStack { - InitialView( - store: Store(initialState: InitialFeature.State()) { - InitialFeature(watchConnecter: WatchConnecter.shared) - } - ) + InitialView() } } } diff --git a/WhitePanda.xcodeproj/project.pbxproj b/WhitePanda.xcodeproj/project.pbxproj index 22a04e0..c540436 100644 --- a/WhitePanda.xcodeproj/project.pbxproj +++ b/WhitePanda.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ 748150262BBADA1400C2EDB5 /* CounterReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748150252BBADA1400C2EDB5 /* CounterReducer.swift */; }; 7481502A2BBAE51800C2EDB5 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748150292BBAE51800C2EDB5 /* HomeView.swift */; }; 7481502C2BBAE52B00C2EDB5 /* HomeReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7481502B2BBAE52B00C2EDB5 /* HomeReducer.swift */; }; - 7481502F2BBAE61000C2EDB5 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 7481502E2BBAE61000C2EDB5 /* ComposableArchitecture */; }; + 74A16D412BC4281600B77F84 /* InitialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A16D402BC4281600B77F84 /* InitialView.swift */; }; 74A16D432BC4282C00B77F84 /* InitialReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A16D422BC4282C00B77F84 /* InitialReducer.swift */; }; 74A16D462BC4309C00B77F84 /* WatchConnecter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A16D452BC4309C00B77F84 /* WatchConnecter.swift */; }; @@ -38,7 +38,7 @@ 74B986932BBACC540072E8F1 /* WhitePanda_Watch_AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B986922BBACC540072E8F1 /* WhitePanda_Watch_AppTests.swift */; }; 74B9869D2BBACC540072E8F1 /* WhitePanda_Watch_AppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B9869C2BBACC540072E8F1 /* WhitePanda_Watch_AppUITests.swift */; }; 74B9869F2BBACC540072E8F1 /* WhitePanda_Watch_AppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B9869E2BBACC540072E8F1 /* WhitePanda_Watch_AppUITestsLaunchTests.swift */; }; - 74B986B72BBAD12C0072E8F1 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 74B986B62BBAD12C0072E8F1 /* ComposableArchitecture */; }; + 74ECC0062BC99BF900A41BB8 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ECC0052BC99BF900A41BB8 /* RoundView.swift */; }; 74ECC0082BC99C3400A41BB8 /* RoundReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ECC0072BC99C3400A41BB8 /* RoundReducer.swift */; }; /* End PBXBuildFile section */ @@ -139,7 +139,6 @@ buildActionMask = 2147483647; files = ( 19090A262BCA301B00B6439B /* Model in Frameworks */, - 7481502F2BBAE61000C2EDB5 /* ComposableArchitecture in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -162,7 +161,6 @@ buildActionMask = 2147483647; files = ( 19090A282BCA303600B6439B /* Model in Frameworks */, - 74B986B72BBAD12C0072E8F1 /* ComposableArchitecture in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -419,7 +417,6 @@ ); name = WhitePanda; packageProductDependencies = ( - 7481502E2BBAE61000C2EDB5 /* ComposableArchitecture */, 19090A252BCA301B00B6439B /* Model */, ); productName = WhitePanda; @@ -476,7 +473,6 @@ ); name = "WhitePanda Watch App"; packageProductDependencies = ( - 74B986B62BBAD12C0072E8F1 /* ComposableArchitecture */, 19090A272BCA303600B6439B /* Model */, ); productName = "WhitePanda Watch App"; @@ -563,7 +559,6 @@ ); mainGroup = 74B9864D2BBACC510072E8F1; packageReferences = ( - 74B986B52BBAD12C0072E8F1 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, 19090A242BCA301B00B6439B /* XCLocalSwiftPackageReference "CommonModel" */, ); productRefGroup = 74B986572BBACC510072E8F1 /* Products */; @@ -1226,14 +1221,6 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */ - 74B986B52BBAD12C0072E8F1 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.9.2; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1245,16 +1232,6 @@ isa = XCSwiftPackageProductDependency; productName = Model; }; - 7481502E2BBAE61000C2EDB5 /* ComposableArchitecture */ = { - isa = XCSwiftPackageProductDependency; - package = 74B986B52BBAD12C0072E8F1 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = ComposableArchitecture; - }; - 74B986B62BBAD12C0072E8F1 /* ComposableArchitecture */ = { - isa = XCSwiftPackageProductDependency; - package = 74B986B52BBAD12C0072E8F1 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; - productName = ComposableArchitecture; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 74B9864E2BBACC510072E8F1 /* Project object */; diff --git a/WhitePanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WhitePanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6721b46..f1447fb 100644 --- a/WhitePanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WhitePanda.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,123 +1,7 @@ { - "originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", + "originHash" : "", "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "e593aba2c6222daad7c4f2732a431eed2c09bb07", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture", - "state" : { - "revision" : "115fe5af41d333b6156d4924d7c7058bc77fd580", - "version" : "1.9.2" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-perception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", - "state" : { - "revision" : "83fc3d89676b33f1c3d49e9268e76ef62430339f", - "version" : "1.1.3" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", - "state" : { - "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", - "version" : "510.0.1" - } - }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", - "state" : { - "revision" : "d9e72f3083c08375794afa216fb2f89c0114f303", - "version" : "1.2.1" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "b13b1d1a8e787a5ffc71ac19dcaf52183ab27ba2", - "version" : "1.1.1" - } - } + ], "version" : 3 } diff --git a/WhitePanda.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WhitePanda.xcworkspace/xcshareddata/swiftpm/Package.resolved index db31bed..f1447fb 100644 --- a/WhitePanda.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WhitePanda.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,123 +1,7 @@ { - "originHash" : "e7024a9cd3fa1c40fa4003b3b2b186c00ba720c787de8ba274caf8fc530677e8", + "originHash" : "", "pins" : [ - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "79623dbe2c7672f5e450d8325613d231454390b3", - "version" : "1.3.2" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture", - "state" : { - "revision" : "115fe5af41d333b6156d4924d7c7058bc77fd580", - "version" : "1.9.2" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "d3a5af3038a09add4d7682f66555d6212058a3c0", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-perception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", - "state" : { - "revision" : "520c458a832d1287e6b698c5f657ae848fd696ff", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", - "state" : { - "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", - "version" : "510.0.1" - } - }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", - "state" : { - "revision" : "2ec6c3a15293efff6083966b38439a4004f25565", - "version" : "1.3.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", - "version" : "1.1.2" - } - } + ], "version" : 3 } diff --git a/WhitePanda/Models/iPhoneConnecter.swift b/WhitePanda/Models/iPhoneConnecter.swift index 7724ba4..3be1282 100644 --- a/WhitePanda/Models/iPhoneConnecter.swift +++ b/WhitePanda/Models/iPhoneConnecter.swift @@ -15,13 +15,10 @@ final class iPhoneConnecter: NSObject { private var session: WCSession! private var roundDataContinuation: AsyncStream<[Int: Int]>.Continuation? - var roundDataStream: AsyncStream<[Int: Int]> { - AsyncStream { continuation in - self.roundDataContinuation = continuation - } - } + let roundDataStream: AsyncStream<[Int: Int]> override init() { + (roundDataStream, roundDataContinuation) = AsyncStream.makeStream(of: [Int: Int].self) super.init() if WCSession.isSupported() { @@ -35,12 +32,10 @@ final class iPhoneConnecter: NSObject { extension iPhoneConnecter: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) { - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - print("session: \(session)") + print("session: \(session)") - if let error { - print("error: \(error.localizedDescription)") - } + if let error { + print("error: \(error.localizedDescription)") } } diff --git a/WhitePanda/Scenes/Home/HomeReducer.swift b/WhitePanda/Scenes/Home/HomeReducer.swift deleted file mode 100644 index 65e40fb..0000000 --- a/WhitePanda/Scenes/Home/HomeReducer.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// HomeReducer.swift -// WhitePanda -// -// Created by satoshi on 2024/04/01. -// - -import Foundation -import ComposableArchitecture - -@Reducer -struct Home { - - @ObservableState - struct State: Equatable { - var isLoading: Bool = false - @Presents var alert: AlertState? - @Presents var pastResultsState: PastResultsReducer.State? - @Presents var resultState: ResultReducer.State? - } - - enum Action { - case createNewRound - case seePastRound - case successConnection - case failureConnection - case establishingConnection - // For transion - case alert(PresentationAction) - case goPastResults(PresentationAction) - case goResult(PresentationAction) - - enum Alert: Equatable {} - } - - var connecter: iPhoneConnecter - - var body: some Reducer { - Reduce { state, action in - switch action { - case .createNewRound: - return .run { send in - await send(.establishingConnection) - do { - try await _ = connecter.gameOn() - await send(.successConnection) - } catch let error { - print(error) - await send(.failureConnection) - } - } - case .seePastRound: - state.pastResultsState = PastResultsReducer.State() - return .none - case .successConnection: - print("successConnection") - state.isLoading = false - state.resultState = ResultReducer.State() - return .none - case .failureConnection: - print("failureConnection") - state.isLoading = false - state.alert = AlertState { - TextState("Connection Failed!\nMake sure WhitePanda App is opened on your Apple Watch") - } actions: { - ButtonState(role: .cancel) { - TextState("Close") - } - } - return .none - case .establishingConnection: - print("establishingConnection") - state.isLoading = true - return .none - case .goPastResults: - return .none - case .goResult: - return .none - case .alert: - return .none - } - } - .ifLet(\.$alert, action: \.alert) - .ifLet(\.$pastResultsState, action: \.goPastResults) { - PastResultsReducer() - } - .ifLet(\.$resultState, action: \.goResult) { - ResultReducer(connecter: connecter) - } - } -} diff --git a/WhitePanda/Scenes/Home/HomeView.swift b/WhitePanda/Scenes/Home/HomeView.swift index 05e3b1a..cd5290c 100644 --- a/WhitePanda/Scenes/Home/HomeView.swift +++ b/WhitePanda/Scenes/Home/HomeView.swift @@ -6,11 +6,10 @@ // import SwiftUI -import ComposableArchitecture struct HomeView: View { - @Bindable var store: StoreOf + @State var viewModel = HomeViewModel() var body: some View { ZStack { @@ -24,19 +23,21 @@ struct HomeView: View { .padding(.bottom, 30) Button("Create New Round") { - store.send(.createNewRound) + Task { + await viewModel.createNewRound() + } } .buttonStyle(FilledButtonStyle()) .padding(.horizontal, 20) Button("See Past Results") { - store.send(.seePastRound) + viewModel.seePastResults() } .buttonStyle(FilledButtonStyle()) .padding(.horizontal, 20) } - if store.isLoading { + if viewModel.isLoading { BlurView(style: .dark) .edgesIgnoringSafeArea(.all) VStack { @@ -56,24 +57,22 @@ struct HomeView: View { } } } - .alert($store.scope(state: \.alert, action: \.alert)) - .navigationDestination( - item: $store.scope(state: \.pastResultsState, action: \.goPastResults), - destination: { store in - PastResultView(store: store) - }) - .navigationDestination( - item: $store.scope(state: \.resultState, action: \.goResult), - destination: { store in - ResultView(store: store) - }) + .alert("Error", isPresented: $viewModel.showAlert) { + Button("Close", role: .cancel) {} + } message: { + Text(viewModel.alertMessage) + } + .navigationDestination(isPresented: $viewModel.showPastResults) { + PastResultView() + } + .navigationDestination(isPresented: $viewModel.showResult) { + ResultView() + } } } #Preview { - HomeView( - store: Store(initialState: Home.State()) { - Home(connecter: iPhoneConnecter.shared) - } - ) + NavigationStack { + HomeView() + } } diff --git a/WhitePanda/Scenes/Home/HomeViewModel.swift b/WhitePanda/Scenes/Home/HomeViewModel.swift new file mode 100644 index 0000000..d15a225 --- /dev/null +++ b/WhitePanda/Scenes/Home/HomeViewModel.swift @@ -0,0 +1,42 @@ +// +// HomeViewModel.swift +// WhitePanda +// +// Created by satoshi on 2024/04/01. +// + +import Foundation +import Observation + +@Observable +final class HomeViewModel { + var isLoading = false + var showAlert = false + var alertMessage = "" + var showResult = false + var showPastResults = false + + private let connecter: iPhoneConnecter + + init(connecter: iPhoneConnecter = .shared) { + self.connecter = connecter + } + + func createNewRound() async { + isLoading = true + do { + _ = try await connecter.gameOn() + isLoading = false + showResult = true + } catch { + print(error) + isLoading = false + alertMessage = "Connection Failed!\nMake sure WhitePanda App is opened on your Apple Watch" + showAlert = true + } + } + + func seePastResults() { + showPastResults = true + } +} diff --git a/WhitePanda/Scenes/PastResults/PastResultsReducer.swift b/WhitePanda/Scenes/PastResults/PastResultsReducer.swift deleted file mode 100644 index 4bda7bb..0000000 --- a/WhitePanda/Scenes/PastResults/PastResultsReducer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PastResultFeature.swift -// WhitePanda -// -// Created by Satoshi Komatsu on 2024/04/14. -// - -import Foundation -import ComposableArchitecture -import Model - -@Reducer -struct PastResultsReducer { - - @ObservableState - struct State: Equatable { - } - - enum Action { - case hoge - } - - var body: some Reducer { - Reduce { state, action in - switch action { - case .hoge: - return .none - } - } - } -} diff --git a/WhitePanda/Scenes/PastResults/PastResultsView.swift b/WhitePanda/Scenes/PastResults/PastResultsView.swift index 442535c..8ebd62c 100644 --- a/WhitePanda/Scenes/PastResults/PastResultsView.swift +++ b/WhitePanda/Scenes/PastResults/PastResultsView.swift @@ -6,21 +6,14 @@ // import SwiftUI -import ComposableArchitecture struct PastResultView: View { - @Bindable var store: StoreOf - var body: some View { Text("Sorry it's not available, please wait for update!") } } #Preview { - PastResultView( - store: Store(initialState: PastResultsReducer.State()) { - PastResultsReducer() - } - ) + PastResultView() } diff --git a/WhitePanda/Scenes/Result/ResultReducer.swift b/WhitePanda/Scenes/Result/ResultReducer.swift deleted file mode 100644 index 1a1cdaa..0000000 --- a/WhitePanda/Scenes/Result/ResultReducer.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ResultsReducer.swift -// WhitePanda -// -// Created by Satoshi Komatsu on 2024/04/14. -// - -import Foundation -import ComposableArchitecture -import Model - -@Reducer -struct ResultReducer { - - @ObservableState - struct State: Equatable { - @Presents var data: [Int: Int]? - } - - enum Action { - case recivedData(data: [Int: Int]) - case onAppear - } - - var connecter: iPhoneConnecter - - var body: some Reducer { - Reduce { state, action in - switch action { - case .recivedData(let data): - state.data = data - return .none - case .onAppear: - return .run { send in - for await data in self.connecter.roundDataStream { - print("===data") - print(data) - await send(.recivedData(data: data)) - } - } - } - } - } -} diff --git a/WhitePanda/Scenes/Result/ResultView.swift b/WhitePanda/Scenes/Result/ResultView.swift index 823f7ab..049f5d9 100644 --- a/WhitePanda/Scenes/Result/ResultView.swift +++ b/WhitePanda/Scenes/Result/ResultView.swift @@ -6,24 +6,19 @@ // import SwiftUI -import ComposableArchitecture struct ResultView: View { - @Bindable var store: StoreOf + @State var viewModel = ResultViewModel() var body: some View { - ResultListView(result: store.data) - .onAppear { - store.send(.onAppear) + ResultListView(result: viewModel.data) + .task { + await viewModel.startListening() } } } #Preview { - ResultView( - store: Store(initialState: ResultReducer.State()) { - ResultReducer(connecter: iPhoneConnecter.shared) - } - ) + ResultView() } diff --git a/WhitePanda/Scenes/Result/ResultViewModel.swift b/WhitePanda/Scenes/Result/ResultViewModel.swift new file mode 100644 index 0000000..bc2323e --- /dev/null +++ b/WhitePanda/Scenes/Result/ResultViewModel.swift @@ -0,0 +1,27 @@ +// +// ResultViewModel.swift +// WhitePanda +// +// Created by Satoshi Komatsu on 2024/04/14. +// + +import Foundation +import Observation + +@Observable +final class ResultViewModel { + var data: [Int: Int]? + private let connecter: iPhoneConnecter + + init(connecter: iPhoneConnecter = .shared) { + self.connecter = connecter + } + + func startListening() async { + for await data in connecter.roundDataStream { + print("===data") + print(data) + self.data = data + } + } +} diff --git a/WhitePanda/WhitePandaApp.swift b/WhitePanda/WhitePandaApp.swift index ce9227f..57941b8 100644 --- a/WhitePanda/WhitePandaApp.swift +++ b/WhitePanda/WhitePandaApp.swift @@ -6,18 +6,13 @@ // import SwiftUI -import ComposableArchitecture @main struct WhitePandaApp: App { var body: some Scene { WindowGroup { NavigationStack { - HomeView( - store: Store(initialState: Home.State()) { - Home(connecter: iPhoneConnecter.shared) - } - ) + HomeView() } } }