From d9e95ba9536d8569a291c2486fbb564e4e9112c3 Mon Sep 17 00:00:00 2001 From: Mick F Date: Thu, 19 Dec 2024 15:57:16 +0100 Subject: [PATCH 1/7] feat(tca): add counter feature from TCA tutorial --- .../HoodsApp.xcodeproj/project.pbxproj | 10 ++- .../HoodsApp/TCATutorial/Counter.swift | 68 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 23 +++++-- 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift diff --git a/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj b/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj index 14889ec..f1bc956 100644 --- a/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj +++ b/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -61,6 +61,10 @@ 00F84B012A66DE0B00A7EC95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 0068F70F2D146A1800295745 /* TCATutorial */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TCATutorial; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 00F6F5512A66CBE40088B530 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -135,6 +139,7 @@ 00F6F5562A66CBE40088B530 /* HoodsApp */ = { isa = PBXGroup; children = ( + 0068F70F2D146A1800295745 /* TCATutorial */, 0004E7C02B86A2B7008EBCF9 /* CopyTextDemo */, 00D120732C88E987001CDA65 /* ImagePickerDemo */, 00DCF43E2B62CFBE00082C13 /* MailButtonDemo */, @@ -188,6 +193,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 0068F70F2D146A1800295745 /* TCATutorial */, + ); name = HoodsApp; packageProductDependencies = ( 004F27732AE6DF3A00D118BA /* ComposableArchitecture */, diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift new file mode 100644 index 0000000..f638e03 --- /dev/null +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift @@ -0,0 +1,68 @@ +import ComposableArchitecture +import SwiftUI + +@Reducer +struct CounterFeature { + @ObservableState + struct State { + var count = 0 + } + + enum Action { + case decrementButtonTapped + case incrementButtonTapped + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .decrementButtonTapped: + state.count -= 1 + return .none + + case .incrementButtonTapped: + state.count += 1 + return .none + } + } + } +} + +struct CounterView: View { + let store: StoreOf + + var body: some View { + VStack { + Text("\(store.count)") + .font(.largeTitle) + .padding() + .background(Color.black.opacity(0.1)) + .cornerRadius(10) + HStack { + Button("-") { + store.send(.decrementButtonTapped) + } + .font(.largeTitle) + .padding() + .background(Color.black.opacity(0.1)) + .cornerRadius(10) + + Button("+") { + store.send(.incrementButtonTapped) + } + .font(.largeTitle) + .padding() + .background(Color.black.opacity(0.1)) + .cornerRadius(10) + } + } + } +} + +#Preview { + CounterView( + store: Store(initialState: CounterFeature.State()) { + CounterFeature() + } + ) +} diff --git a/Hoods.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Hoods.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ba32ec..9e41033 100644 --- a/Hoods.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Hoods.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,7 +42,7 @@ "location" : "https://github.com/dirtyhenry/swift-blocks", "state" : { "branch" : "main", - "revision" : "281d5d4627e3d5b11b524cf4223f2fbc09b07910" + "revision" : "b084b311c84fbaf3f5818a59890c5f2567da2588" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "69247baf7be2fd6f5820192caef0082d01849cd0", - "version" : "1.16.1" + "revision" : "d602618c628e5123f66643437151079d3664970d", + "version" : "1.17.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", - "version" : "1.3.0" + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "7d2eb4ad20efb2838269645410d26b64ca48d8aa", - "version" : "1.6.1" + "revision" : "5526c8a27675dc7b18d6fa643abfb64bcb200b77", + "version" : "1.6.2" } }, { @@ -162,6 +162,15 @@ "version" : "1.4.1" } }, + { + "identity" : "swift-sharing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-sharing", + "state" : { + "revision" : "b68bf99b05cb974392f6ffa380351e9b7391e233", + "version" : "1.1.0" + } + }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", From f46a2ba5b29792b8587ded6353bc620567126165 Mon Sep 17 00:00:00 2001 From: Mick F Date: Thu, 19 Dec 2024 16:03:58 +0100 Subject: [PATCH 2/7] feat(tca): add counter to the app via the root view --- Examples/HoodsApp/HoodsApp/RootView.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Examples/HoodsApp/HoodsApp/RootView.swift b/Examples/HoodsApp/HoodsApp/RootView.swift index 4496677..6a81fc6 100644 --- a/Examples/HoodsApp/HoodsApp/RootView.swift +++ b/Examples/HoodsApp/HoodsApp/RootView.swift @@ -6,6 +6,11 @@ struct RootView: View { @State var currentPath: Path? @State private var preferredColumn = NavigationSplitViewColumn.detail + static let counterStore = Store(initialState: CounterFeature.State()) { + CounterFeature() + ._printChanges() + } + func navigate(to path: Path) { currentPath = path preferredColumn = .detail @@ -16,6 +21,7 @@ struct RootView: View { case mailer case copyText case imagePicker + case tcaCounter } var body: some View { @@ -25,6 +31,7 @@ struct RootView: View { Button("Mailer") { navigate(to: .mailer) } Button("CopyText") { navigate(to: .copyText) } Button("ImagePicker") { navigate(to: .imagePicker) } + Button("TCA Counter") { navigate(to: .tcaCounter) } } } detail: { switch currentPath { @@ -52,6 +59,8 @@ struct RootView: View { ImagePickerDemoFeature() } ) + case .tcaCounter: + CounterView(store: Self.counterStore) case nil: VStack { Text("🏘️ Welcome to the ’hoods!") From a6a69a4e33dafd54a4e45427587576b6815602fd Mon Sep 17 00:00:00 2001 From: Mick F Date: Thu, 19 Dec 2024 17:40:59 +0100 Subject: [PATCH 3/7] feat(tca): add effects --- .../HoodsApp/TCATutorial/Counter.swift | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift index f638e03..612431c 100644 --- a/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift @@ -6,11 +6,22 @@ struct CounterFeature { @ObservableState struct State { var count = 0 + var fact: String? + var isLoading = false + var isTimerRunning = false } enum Action { case decrementButtonTapped case incrementButtonTapped + case factButtonTapped + case factResponse(String) + case timerTick + case toggleTimerButtonTapped + } + + enum CancelID { + case timer } var body: some ReducerOf { @@ -18,11 +29,47 @@ struct CounterFeature { switch action { case .decrementButtonTapped: state.count -= 1 + state.fact = nil + return .none + + case .factButtonTapped: + state.fact = nil + state.isLoading = true + return .run { [count = state.count] send in + let (data, _) = try await URLSession.shared + .data(from: URL(string: "https://www.statium.app/newsletter/api/latest?query=\(count)")!) + let fact = String(decoding: data, as: UTF8.self) + await send(.factResponse(String(fact.prefix(10)))) + } + + case let .factResponse(fact): + state.fact = fact + state.isLoading = false return .none case .incrementButtonTapped: state.count += 1 + state.fact = nil + return .none + + case .timerTick: + state.count += 1 + state.fact = nil return .none + + case .toggleTimerButtonTapped: + state.isTimerRunning.toggle() + if state.isTimerRunning { + return .run { send in + while true { + try await Task.sleep(for: .seconds(1)) + await send(.timerTick) + } + } + .cancellable(id: CancelID.timer) + } else { + return .cancel(id: CancelID.timer) + } } } } @@ -55,6 +102,31 @@ struct CounterView: View { .background(Color.black.opacity(0.1)) .cornerRadius(10) } + + Button(store.isTimerRunning ? "Stop timer" : "Start timer") { + store.send(.toggleTimerButtonTapped) + } + .font(.largeTitle) + .padding() + .background(Color.black.opacity(0.1)) + .cornerRadius(10) + + Button("Fact") { + store.send(.factButtonTapped) + } + .font(.largeTitle) + .padding() + .background(Color.black.opacity(0.1)) + .cornerRadius(10) + + if store.isLoading { + ProgressView() + } else if let fact = store.fact { + Text(fact) + .font(.largeTitle) + .multilineTextAlignment(.center) + .padding() + } } } } From 22c8702ef32666677c159bd8da4c0fe2c9958ab6 Mon Sep 17 00:00:00 2001 From: Mick F Date: Thu, 19 Dec 2024 18:00:00 +0100 Subject: [PATCH 4/7] feat(tca): start testing counter --- .../HoodsApp.xcodeproj/project.pbxproj | 4 ++++ .../{Counter.swift => CounterFeature.swift} | 3 ++- .../HoodsAppTests/CounterFeatureTests.swift | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) rename Examples/HoodsApp/HoodsApp/TCATutorial/{Counter.swift => CounterFeature.swift} (97%) create mode 100644 Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift diff --git a/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj b/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj index f1bc956..a854fa3 100644 --- a/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj +++ b/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 0004E7C42B86A2F4008EBCF9 /* CopyTextDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0004E7C32B86A2F4008EBCF9 /* CopyTextDemoView.swift */; }; 0004E7C62B86A313008EBCF9 /* CopyTextDemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */; }; 004F27742AE6DF3A00D118BA /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 004F27732AE6DF3A00D118BA /* ComposableArchitecture */; }; + 0068F7132D1486A000295745 /* CounterFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0068F7122D1486A000295745 /* CounterFeatureTests.swift */; }; 00BC0F6D2B632FE2000FC408 /* HoodsTestsTools in Frameworks */ = {isa = PBXBuildFile; productRef = 00BC0F6C2B632FE2000FC408 /* HoodsTestsTools */; }; 00D120752C88E997001CDA65 /* ImagePickerFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D120742C88E997001CDA65 /* ImagePickerFeature.swift */; }; 00D120772C88E99C001CDA65 /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D120762C88E99C001CDA65 /* ImagePickerView.swift */; }; @@ -43,6 +44,7 @@ 0004E7C12B86A2E8008EBCF9 /* CopyTextDemoFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoFeature.swift; sourceTree = ""; }; 0004E7C32B86A2F4008EBCF9 /* CopyTextDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoView.swift; sourceTree = ""; }; 0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoTests.swift; sourceTree = ""; }; + 0068F7122D1486A000295745 /* CounterFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeatureTests.swift; sourceTree = ""; }; 00CC6CBA2A68425F0093300A /* DefaultTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DefaultTestPlan.xctestplan; sourceTree = ""; }; 00D120742C88E997001CDA65 /* ImagePickerFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerFeature.swift; sourceTree = ""; }; 00D120762C88E99C001CDA65 /* ImagePickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = ""; }; @@ -167,6 +169,7 @@ 00CC6CBA2A68425F0093300A /* DefaultTestPlan.xctestplan */, 00DCF4432B62CFF800082C13 /* MailButtonDemoTests.swift */, 0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */, + 0068F7122D1486A000295745 /* CounterFeatureTests.swift */, ); path = HoodsAppTests; sourceTree = ""; @@ -311,6 +314,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0068F7132D1486A000295745 /* CounterFeatureTests.swift in Sources */, 0004E7C62B86A313008EBCF9 /* CopyTextDemoTests.swift in Sources */, 00F6F5692A66CBE50088B530 /* HoodsAppTests.swift in Sources */, 00DCF4442B62CFF800082C13 /* MailButtonDemoTests.swift in Sources */, diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift similarity index 97% rename from Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift rename to Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift index 612431c..74704a6 100644 --- a/Examples/HoodsApp/HoodsApp/TCATutorial/Counter.swift +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift @@ -3,8 +3,9 @@ import SwiftUI @Reducer struct CounterFeature { + // An equatable state is required by the TestStore @ObservableState - struct State { + struct State: Equatable { var count = 0 var fact: String? var isLoading = false diff --git a/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift b/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift new file mode 100644 index 0000000..dccee3c --- /dev/null +++ b/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift @@ -0,0 +1,21 @@ +import ComposableArchitecture +import Testing + +@testable import HoodsApp + +@MainActor +struct CounterFeatureTests { + @Test + func basics() async { + let store = TestStore(initialState: CounterFeature.State()) { + CounterFeature() + } + + await store.send(.incrementButtonTapped) { + $0.count = 1 + } + await store.send(.decrementButtonTapped) { + $0.count = 0 + } + } +} From 6990c10dfcf66323ab80bc5d30b9a2d192965fe7 Mon Sep 17 00:00:00 2001 From: Mick F Date: Thu, 19 Dec 2024 18:08:18 +0100 Subject: [PATCH 5/7] feat(tca): test timers --- .../HoodsApp/TCATutorial/CounterFeature.swift | 5 +++-- .../HoodsAppTests/CounterFeatureTests.swift | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift index 74704a6..676f343 100644 --- a/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift @@ -25,6 +25,8 @@ struct CounterFeature { case timer } + @Dependency(\.continuousClock) var clock + var body: some ReducerOf { Reduce { state, action in switch action { @@ -62,8 +64,7 @@ struct CounterFeature { state.isTimerRunning.toggle() if state.isTimerRunning { return .run { send in - while true { - try await Task.sleep(for: .seconds(1)) + for await _ in clock.timer(interval: .seconds(1)) { await send(.timerTick) } } diff --git a/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift b/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift index dccee3c..907db19 100644 --- a/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift +++ b/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift @@ -18,4 +18,26 @@ struct CounterFeatureTests { $0.count = 0 } } + + @Test + func timer() async { + let clock = TestClock() + + let store = TestStore(initialState: CounterFeature.State()) { + CounterFeature() + } withDependencies: { + $0.continuousClock = clock + } + + await store.send(.toggleTimerButtonTapped) { + $0.isTimerRunning = true + } + await clock.advance(by: .seconds(1)) + await store.receive(\.timerTick) { + $0.count = 1 + } + await store.send(.toggleTimerButtonTapped) { + $0.isTimerRunning = false + } + } } From 8434fdf8ae744d42b1bee0576f45944acca6403c Mon Sep 17 00:00:00 2001 From: Mick F Date: Thu, 19 Dec 2024 18:15:07 +0100 Subject: [PATCH 6/7] feat(tca): add test for network request --- .../HoodsApp/TCATutorial/CounterFeature.swift | 6 ++--- .../TCATutorial/NumberFactClient.swift | 23 +++++++++++++++++++ .../HoodsAppTests/CounterFeatureTests.swift | 17 ++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift index 676f343..1f89b2e 100644 --- a/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift @@ -26,6 +26,7 @@ struct CounterFeature { } @Dependency(\.continuousClock) var clock + @Dependency(\.numberFact) var numberFact var body: some ReducerOf { Reduce { state, action in @@ -39,10 +40,7 @@ struct CounterFeature { state.fact = nil state.isLoading = true return .run { [count = state.count] send in - let (data, _) = try await URLSession.shared - .data(from: URL(string: "https://www.statium.app/newsletter/api/latest?query=\(count)")!) - let fact = String(decoding: data, as: UTF8.self) - await send(.factResponse(String(fact.prefix(10)))) + try await send(.factResponse(numberFact.fetch(count))) } case let .factResponse(fact): diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift new file mode 100644 index 0000000..d42a500 --- /dev/null +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift @@ -0,0 +1,23 @@ +import ComposableArchitecture +import Foundation + +struct NumberFactClient { + var fetch: (Int) async throws -> String +} + +extension NumberFactClient: DependencyKey { + static let liveValue = Self( + fetch: { number in + let (data, _) = try await URLSession.shared + .data(from: URL(string: "https://www.statium.app/newsletter/api/latest?query=\(number)")!) + return String(decoding: data, as: UTF8.self) + } + ) +} + +extension DependencyValues { + var numberFact: NumberFactClient { + get { self[NumberFactClient.self] } + set { self[NumberFactClient.self] = newValue } + } +} diff --git a/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift b/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift index 907db19..b0526be 100644 --- a/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift +++ b/Examples/HoodsApp/HoodsAppTests/CounterFeatureTests.swift @@ -40,4 +40,21 @@ struct CounterFeatureTests { $0.isTimerRunning = false } } + + @Test + func numberFact() async { + let store = TestStore(initialState: CounterFeature.State()) { + CounterFeature() + } withDependencies: { + $0.numberFact.fetch = { "\($0) is a good number." } + } + + await store.send(.factButtonTapped) { + $0.isLoading = true + } + await store.receive(\.factResponse) { + $0.isLoading = false + $0.fact = "0 is a good number." + } + } } From d778efccd9a2b2fc93dc19e8f3afb71410255ba1 Mon Sep 17 00:00:00 2001 From: Mick F Date: Fri, 20 Dec 2024 11:38:22 +0100 Subject: [PATCH 7/7] feat(tca): wrap up Counter feature --- .gitignore | 2 +- .../HoodsApp.xcodeproj/project.pbxproj | 12 +++-- Examples/HoodsApp/HoodsApp/RootView.swift | 6 +-- .../TCATutorial/CounterTabFeature.swift | 52 +++++++++++++++++++ .../TCATutorial/NumberFactClient.swift | 2 +- .../CounterTabFeatureTests.swift | 18 +++++++ 6 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 Examples/HoodsApp/HoodsApp/TCATutorial/CounterTabFeature.swift create mode 100644 Examples/HoodsApp/HoodsAppTests/CounterTabFeatureTests.swift diff --git a/.gitignore b/.gitignore index e284f64..5d087d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store -/.build +.build/ /Packages /*.xcodeproj xcuserdata/ diff --git a/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj b/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj index a854fa3..2b8a840 100644 --- a/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj +++ b/Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 0004E7C62B86A313008EBCF9 /* CopyTextDemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */; }; 004F27742AE6DF3A00D118BA /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 004F27732AE6DF3A00D118BA /* ComposableArchitecture */; }; 0068F7132D1486A000295745 /* CounterFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0068F7122D1486A000295745 /* CounterFeatureTests.swift */; }; + 0068F71B2D1574A600295745 /* CounterTabFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0068F71A2D1574A600295745 /* CounterTabFeatureTests.swift */; }; 00BC0F6D2B632FE2000FC408 /* HoodsTestsTools in Frameworks */ = {isa = PBXBuildFile; productRef = 00BC0F6C2B632FE2000FC408 /* HoodsTestsTools */; }; 00D120752C88E997001CDA65 /* ImagePickerFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D120742C88E997001CDA65 /* ImagePickerFeature.swift */; }; 00D120772C88E99C001CDA65 /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D120762C88E99C001CDA65 /* ImagePickerView.swift */; }; @@ -45,6 +46,7 @@ 0004E7C32B86A2F4008EBCF9 /* CopyTextDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoView.swift; sourceTree = ""; }; 0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoTests.swift; sourceTree = ""; }; 0068F7122D1486A000295745 /* CounterFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeatureTests.swift; sourceTree = ""; }; + 0068F71A2D1574A600295745 /* CounterTabFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterTabFeatureTests.swift; sourceTree = ""; }; 00CC6CBA2A68425F0093300A /* DefaultTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DefaultTestPlan.xctestplan; sourceTree = ""; }; 00D120742C88E997001CDA65 /* ImagePickerFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerFeature.swift; sourceTree = ""; }; 00D120762C88E99C001CDA65 /* ImagePickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = ""; }; @@ -170,6 +172,7 @@ 00DCF4432B62CFF800082C13 /* MailButtonDemoTests.swift */, 0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */, 0068F7122D1486A000295745 /* CounterFeatureTests.swift */, + 0068F71A2D1574A600295745 /* CounterTabFeatureTests.swift */, ); path = HoodsAppTests; sourceTree = ""; @@ -317,6 +320,7 @@ 0068F7132D1486A000295745 /* CounterFeatureTests.swift in Sources */, 0004E7C62B86A313008EBCF9 /* CopyTextDemoTests.swift in Sources */, 00F6F5692A66CBE50088B530 /* HoodsAppTests.swift in Sources */, + 0068F71B2D1574A600295745 /* CounterTabFeatureTests.swift in Sources */, 00DCF4442B62CFF800082C13 /* MailButtonDemoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -467,7 +471,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -501,7 +505,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -525,7 +529,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = N77R86QZUG; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.mickf.HoodsAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -545,7 +549,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = N77R86QZUG; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.mickf.HoodsAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Examples/HoodsApp/HoodsApp/RootView.swift b/Examples/HoodsApp/HoodsApp/RootView.swift index 6a81fc6..45d3c53 100644 --- a/Examples/HoodsApp/HoodsApp/RootView.swift +++ b/Examples/HoodsApp/HoodsApp/RootView.swift @@ -6,8 +6,8 @@ struct RootView: View { @State var currentPath: Path? @State private var preferredColumn = NavigationSplitViewColumn.detail - static let counterStore = Store(initialState: CounterFeature.State()) { - CounterFeature() + static let counterTabStore = Store(initialState: CounterTabFeature.State()) { + CounterTabFeature() ._printChanges() } @@ -60,7 +60,7 @@ struct RootView: View { } ) case .tcaCounter: - CounterView(store: Self.counterStore) + CounterTabView(store: Self.counterTabStore) case nil: VStack { Text("🏘️ Welcome to the ’hoods!") diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/CounterTabFeature.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterTabFeature.swift new file mode 100644 index 0000000..1fe5d2a --- /dev/null +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/CounterTabFeature.swift @@ -0,0 +1,52 @@ +import ComposableArchitecture +import SwiftUI + +@Reducer +struct CounterTabFeature { + struct State: Equatable { + var tab1 = CounterFeature.State() + var tab2 = CounterFeature.State() + } + + enum Action { + case tab1(CounterFeature.Action) + case tab2(CounterFeature.Action) + } + + var body: some ReducerOf { + Scope(state: \.tab1, action: \.tab1) { + CounterFeature() + } + Scope(state: \.tab2, action: \.tab2) { + CounterFeature() + } + Reduce { _, _ in + // Core logic of the app feature + .none + } + } +} + +struct CounterTabView: View { + let store: StoreOf + + var body: some View { + TabView { + Tab("Counter 1", systemImage: "number.square") { + CounterView(store: store.scope(state: \.tab1, action: \.tab1)) + } + + Tab("Counter 2", systemImage: "number.circle.fill") { + CounterView(store: store.scope(state: \.tab2, action: \.tab2)) + } + } + } +} + +#Preview { + CounterTabView( + store: Store(initialState: CounterTabFeature.State()) { + CounterTabFeature() + } + ) +} diff --git a/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift b/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift index d42a500..6b99f2c 100644 --- a/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift +++ b/Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift @@ -9,7 +9,7 @@ extension NumberFactClient: DependencyKey { static let liveValue = Self( fetch: { number in let (data, _) = try await URLSession.shared - .data(from: URL(string: "https://www.statium.app/newsletter/api/latest?query=\(number)")!) + .data(from: URL(string: "https://statium-monorepo.vercel.app/newsletter/latest?query=\(number)")!) return String(decoding: data, as: UTF8.self) } ) diff --git a/Examples/HoodsApp/HoodsAppTests/CounterTabFeatureTests.swift b/Examples/HoodsApp/HoodsAppTests/CounterTabFeatureTests.swift new file mode 100644 index 0000000..c28611c --- /dev/null +++ b/Examples/HoodsApp/HoodsAppTests/CounterTabFeatureTests.swift @@ -0,0 +1,18 @@ +import ComposableArchitecture +import Testing + +@testable import HoodsApp + +@MainActor +struct CounterTabFeatureTests { + @Test + func incrementInFirstTab() async { + let store = TestStore(initialState: CounterTabFeature.State()) { + CounterTabFeature() + } + + await store.send(\.tab1.incrementButtonTapped) { + $0.tab1.count = 1 + } + } +}