Skip to content

Commit

Permalink
Merge pull request #22 from dirtyhenry/update-tca
Browse files Browse the repository at this point in the history
Bring TCA tutorial content
  • Loading branch information
dirtyhenry authored Dec 30, 2024
2 parents 7fbcce1 + d778efc commit 3f37be8
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DS_Store
/.build
.build/
/Packages
/*.xcodeproj
xcuserdata/
Expand Down
26 changes: 21 additions & 5 deletions Examples/HoodsApp/HoodsApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 70;
objects = {

/* Begin PBXBuildFile section */
0004E7C22B86A2E8008EBCF9 /* CopyTextDemoFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0004E7C12B86A2E8008EBCF9 /* CopyTextDemoFeature.swift */; };
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 */; };
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 */; };
Expand Down Expand Up @@ -43,6 +45,8 @@
0004E7C12B86A2E8008EBCF9 /* CopyTextDemoFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoFeature.swift; sourceTree = "<group>"; };
0004E7C32B86A2F4008EBCF9 /* CopyTextDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoView.swift; sourceTree = "<group>"; };
0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextDemoTests.swift; sourceTree = "<group>"; };
0068F7122D1486A000295745 /* CounterFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeatureTests.swift; sourceTree = "<group>"; };
0068F71A2D1574A600295745 /* CounterTabFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterTabFeatureTests.swift; sourceTree = "<group>"; };
00CC6CBA2A68425F0093300A /* DefaultTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DefaultTestPlan.xctestplan; sourceTree = "<group>"; };
00D120742C88E997001CDA65 /* ImagePickerFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerFeature.swift; sourceTree = "<group>"; };
00D120762C88E99C001CDA65 /* ImagePickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
Expand All @@ -61,6 +65,10 @@
00F84B012A66DE0B00A7EC95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
0068F70F2D146A1800295745 /* TCATutorial */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TCATutorial; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
00F6F5512A66CBE40088B530 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
Expand Down Expand Up @@ -135,6 +143,7 @@
00F6F5562A66CBE40088B530 /* HoodsApp */ = {
isa = PBXGroup;
children = (
0068F70F2D146A1800295745 /* TCATutorial */,
0004E7C02B86A2B7008EBCF9 /* CopyTextDemo */,
00D120732C88E987001CDA65 /* ImagePickerDemo */,
00DCF43E2B62CFBE00082C13 /* MailButtonDemo */,
Expand Down Expand Up @@ -162,6 +171,8 @@
00CC6CBA2A68425F0093300A /* DefaultTestPlan.xctestplan */,
00DCF4432B62CFF800082C13 /* MailButtonDemoTests.swift */,
0004E7C52B86A313008EBCF9 /* CopyTextDemoTests.swift */,
0068F7122D1486A000295745 /* CounterFeatureTests.swift */,
0068F71A2D1574A600295745 /* CounterTabFeatureTests.swift */,
);
path = HoodsAppTests;
sourceTree = "<group>";
Expand All @@ -188,6 +199,9 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
0068F70F2D146A1800295745 /* TCATutorial */,
);
name = HoodsApp;
packageProductDependencies = (
004F27732AE6DF3A00D118BA /* ComposableArchitecture */,
Expand Down Expand Up @@ -303,8 +317,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
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;
Expand Down Expand Up @@ -455,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",
Expand Down Expand Up @@ -489,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",
Expand All @@ -513,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)";
Expand All @@ -533,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)";
Expand Down
9 changes: 9 additions & 0 deletions Examples/HoodsApp/HoodsApp/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ struct RootView: View {
@State var currentPath: Path?
@State private var preferredColumn = NavigationSplitViewColumn.detail

static let counterTabStore = Store(initialState: CounterTabFeature.State()) {
CounterTabFeature()
._printChanges()
}

func navigate(to path: Path) {
currentPath = path
preferredColumn = .detail
Expand All @@ -16,6 +21,7 @@ struct RootView: View {
case mailer
case copyText
case imagePicker
case tcaCounter
}

var body: some View {
Expand All @@ -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 {
Expand Down Expand Up @@ -52,6 +59,8 @@ struct RootView: View {
ImagePickerDemoFeature()
}
)
case .tcaCounter:
CounterTabView(store: Self.counterTabStore)
case nil:
VStack {
Text("🏘️ Welcome to the ’hoods!")
Expand Down
140 changes: 140 additions & 0 deletions Examples/HoodsApp/HoodsApp/TCATutorial/CounterFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import ComposableArchitecture
import SwiftUI

@Reducer
struct CounterFeature {
// An equatable state is required by the TestStore
@ObservableState
struct State: Equatable {
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
}

@Dependency(\.continuousClock) var clock
@Dependency(\.numberFact) var numberFact

var body: some ReducerOf<Self> {
Reduce { state, action in
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
try await send(.factResponse(numberFact.fetch(count)))
}

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
for await _ in clock.timer(interval: .seconds(1)) {
await send(.timerTick)
}
}
.cancellable(id: CancelID.timer)
} else {
return .cancel(id: CancelID.timer)
}
}
}
}
}

struct CounterView: View {
let store: StoreOf<CounterFeature>

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)
}

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()
}
}
}
}

#Preview {
CounterView(
store: Store(initialState: CounterFeature.State()) {
CounterFeature()
}
)
}
52 changes: 52 additions & 0 deletions Examples/HoodsApp/HoodsApp/TCATutorial/CounterTabFeature.swift
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<CounterTabFeature>

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()
}
)
}
23 changes: 23 additions & 0 deletions Examples/HoodsApp/HoodsApp/TCATutorial/NumberFactClient.swift
Original file line number Diff line number Diff line change
@@ -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://statium-monorepo.vercel.app/newsletter/latest?query=\(number)")!)
return String(decoding: data, as: UTF8.self)
}
)
}

extension DependencyValues {
var numberFact: NumberFactClient {
get { self[NumberFactClient.self] }
set { self[NumberFactClient.self] = newValue }
}
}
Loading

0 comments on commit 3f37be8

Please sign in to comment.