Add some docstrings and clean some things up

This commit is contained in:
Ben Kreeger 2025-11-24 16:43:12 -06:00
parent d02ffb9ab4
commit 2695aab46e
Signed by: kreeger
GPG Key ID: D5CF8683D4BE4B50
6 changed files with 42 additions and 15 deletions

View File

@ -85,7 +85,6 @@
C3BB763D2ED0C38800D56534 /* App */, C3BB763D2ED0C38800D56534 /* App */,
C3BB764B2ED0C38900D56534 /* AppTests */, C3BB764B2ED0C38900D56534 /* AppTests */,
C3BB76552ED0C38900D56534 /* AppUITests */, C3BB76552ED0C38900D56534 /* AppUITests */,
C3C75C592ED2C672000FD10A /* Frameworks */,
C3BB763C2ED0C38800D56534 /* Products */, C3BB763C2ED0C38800D56534 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@ -100,13 +99,6 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C3C75C592ED2C672000FD10A /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */

View File

@ -82,6 +82,13 @@
ReferencedContainer = "container:Meow.xcodeproj"> ReferencedContainer = "container:Meow.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -8,6 +8,7 @@ import Core
import CatFactsKit import CatFactsKit
import ComposableArchitecture import ComposableArchitecture
/// Handles the logic for fetching and displaying a `List` of cat facts from a remote API.
@Reducer @Reducer
public struct FactsFeature { public struct FactsFeature {
@ObservationIgnored @ObservationIgnored
@ -18,7 +19,7 @@ public struct FactsFeature {
public var baseURL: URL public var baseURL: URL
public var mode: Mode = .notLoaded public var mode: Mode = .notLoaded
public init(baseURL: URL = URL(string: "https://invalid.barf")!) { public init(baseURL: URL = URL(string: "https://invalid.barf")!, mode: Mode = .notLoaded) {
self.baseURL = baseURL self.baseURL = baseURL
} }
} }
@ -32,17 +33,30 @@ public struct FactsFeature {
public enum Action { public enum Action {
case viewAppeared case viewAppeared
case pulledToRefresh
case fetchNeeded
case receivedFacts([String]) case receivedFacts([String])
case receivedError(Error) case receivedError(Error)
} }
// MARK: - Public functionality
public init() { } public init() { }
public var body: some ReducerOf<Self> { public var body: some ReducerOf<Self> {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case .viewAppeared: case .viewAppeared:
state.mode = .loading switch state.mode {
case .loaded:
return .none
default:
state.mode = .loading
return .send(.fetchNeeded)
}
case .pulledToRefresh:
return .send(.fetchNeeded)
case .fetchNeeded:
let client = clientFactory.createClient(baseURL: state.baseURL) let client = clientFactory.createClient(baseURL: state.baseURL)
return .run { send in return .run { send in
do { do {

View File

@ -7,6 +7,8 @@ import SwiftUI
import Foundation import Foundation
import ComposableArchitecture import ComposableArchitecture
/// A view that displays either a progress spinner, a list of cat facts provided by an underlying feature reducer, or
/// an error message, depending on the state. Supports pull-to-refresh.
public struct FactsView: View { public struct FactsView: View {
let store: StoreOf<FactsFeature> let store: StoreOf<FactsFeature>
@ -29,6 +31,11 @@ public struct FactsView: View {
Text(error.localizedDescription) Text(error.localizedDescription)
} }
} }
.refreshable {
// Tie the lifecycle of the PTR to the effect.
// https://github.com/pointfreeco/swift-composable-architecture/discussions/2542
await Task { await store.send(.pulledToRefresh).finish() }.value
}
.onAppear { .onAppear {
store.send(.viewAppeared) store.send(.viewAppeared)
} }
@ -36,7 +43,7 @@ public struct FactsView: View {
} }
#Preview { #Preview {
FactsView(store: .init(initialState: FactsFeature.State()) { FactsView(store: .init(initialState: FactsFeature.State(mode: .loaded(facts: ["One", "Two", "Three"]))) {
FactsFeature() FactsFeature()
}) })
} }

View File

@ -8,6 +8,9 @@ import Facts
import CatFactsKit import CatFactsKit
import ComposableArchitecture import ComposableArchitecture
/// Handles interaction logic for the root of the application. Doesn't otherwise do much itself but it does respond to
/// a `viewAppeared` action so that it can show how one might change the state that drives _another_ feature (in this
/// example, `FactsFeature`).
@Reducer @Reducer
public struct RootFeature { public struct RootFeature {
@ObservableState @ObservableState
@ -26,6 +29,11 @@ public struct RootFeature {
public init() { } public init() { }
public var body: some ReducerOf<Self> { public var body: some ReducerOf<Self> {
// This fun DSL lets me provide a concrete Feature reducer when the Root feature is asked for a scoped store
Scope(state: \.facts, action: \.facts) {
FactsFeature()
}
// And then this is everything that's provided by the root.
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case .viewAppeared: case .viewAppeared:
@ -35,8 +43,5 @@ public struct RootFeature {
return .none return .none
} }
} }
Scope(state: \.facts, action: \.facts) {
FactsFeature()
}
} }
} }

View File

@ -7,6 +7,7 @@ import SwiftUI
import Facts import Facts
import ComposableArchitecture import ComposableArchitecture
/// The root view of the entire app.
public struct RootView: View { public struct RootView: View {
@Bindable var store: StoreOf<RootFeature> @Bindable var store: StoreOf<RootFeature>
@ -16,6 +17,7 @@ public struct RootView: View {
public var body: some View { public var body: some View {
NavigationStack { NavigationStack {
// Super basic here; I want to show a FactsView scoped to just that feature's `State` and `Action`s.
FactsView(store: store.scope(state: \.facts, action: \.facts)) FactsView(store: store.scope(state: \.facts, action: \.facts))
}.onAppear { }.onAppear {
store.send(.viewAppeared) store.send(.viewAppeared)