Add subfeature complete with dependency injection, more or less

This commit is contained in:
Ben Kreeger 2025-11-23 21:26:05 -06:00
parent 498716fd67
commit ac29eb40c6
Signed by: kreeger
GPG Key ID: D5CF8683D4BE4B50
8 changed files with 211 additions and 38 deletions

View File

@ -11,48 +11,45 @@ let commonSwiftSettings: [SwiftSetting] = [
.swiftLanguageMode(.v6)
]
let tcaDependency = Target.Dependency.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
let package = Package(
name: "Meow",
platforms: [.iOS(.v18), .macOS(.v15)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "CatFactsKit",
targets: ["CatFactsKit"]
),
.library(
name: "Root",
targets: ["Root"]
)
.library(name: "CatFactsKit", targets: ["CatFactsKit"]),
.library(name: "Core", targets: ["Core"]),
.library(name: "Root", targets: ["Root"]),
.library(name: "Facts", targets: ["Facts"])
],
dependencies: [
.package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.2"),
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.23.1"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "CatFactsKit",
swiftSettings: commonSwiftSettings
),
.target(
name: "Core",
dependencies: ["CatFactsKit", tcaDependency],
swiftSettings: commonSwiftSettings
),
.target(
name: "Root",
dependencies: [
"CatFactsKit",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
],
dependencies: ["CatFactsKit", "Core", "Facts", tcaDependency],
swiftSettings: commonSwiftSettings
),
.target(
name: "Facts",
dependencies: ["CatFactsKit", "Core", tcaDependency],
swiftSettings: commonSwiftSettings
),
.testTarget(
name: "CatFactsKitTests",
dependencies: [
"CatFactsKit",
"Mocker"
],
resources: [
.process("Fixtures")
],
dependencies: ["CatFactsKit", "Mocker"],
resources: [.process("Fixtures")],
swiftSettings: commonSwiftSettings
),
]

View File

@ -5,8 +5,8 @@
import Foundation
public struct CatFacts {
let baseURL: URL
public struct CatFacts: Sendable {
public let baseURL: URL
let urlSession: URLSession
public init(baseURL: URL, urlSession: URLSession = .shared) {

View File

@ -0,0 +1,25 @@
//
// CatFacts+Meow.swift
// Core
//
import Foundation
import CatFactsKit
import ComposableArchitecture
extension CatFacts: DependencyKey, TestDependencyKey {
public static var liveValue: CatFactsKit.CatFacts {
CatFacts(baseURL: URL(string: "https://barf.com")!)
}
public static var testValue: CatFactsKit.CatFacts {
CatFacts(baseURL: URL(string: "https://test.local")!)
}
}
public extension DependencyValues {
var catFacts: CatFacts {
get { self[CatFacts.self] }
set { self[CatFacts.self] = newValue }
}
}

View File

@ -0,0 +1,31 @@
//
// ClientFactory.swift
// Meow
//
import Foundation
import CatFactsKit
import ComposableArchitecture
public struct ClientFactory: Sendable {
public func createClient(baseURL: URL) -> CatFacts {
return CatFacts(baseURL: baseURL)
}
}
extension ClientFactory: DependencyKey, TestDependencyKey {
public static var liveValue: ClientFactory {
ClientFactory()
}
public static var testValue: ClientFactory {
ClientFactory()
}
}
public extension DependencyValues {
var clientFactory: ClientFactory {
get { self[ClientFactory.self] }
set { self[ClientFactory.self] = newValue }
}
}

View File

@ -0,0 +1,64 @@
//
// FactsFeature.swift
// Meow
//
import Foundation
import Core
import CatFactsKit
import ComposableArchitecture
@Reducer
public struct FactsFeature {
@ObservationIgnored
@Dependency(\.clientFactory) var clientFactory
@ObservableState
public struct State {
public var baseURL: URL
public var mode: Mode = .notLoaded
public init(baseURL: URL = URL(string: "https://invalid.barf")!) {
self.baseURL = baseURL
}
}
public enum Mode {
case notLoaded
case loading
case loaded(facts: [String])
case error(Error)
}
public enum Action {
case viewAppeared
case receivedFacts([String])
case receivedError(Error)
}
public init() { }
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .viewAppeared:
state.mode = .loading
let client = clientFactory.createClient(baseURL: state.baseURL)
return .run { send in
do {
let results = try await client.getFacts(count: 5)
await send(.receivedFacts(results))
} catch {
await send(.receivedError(error))
}
}
case let .receivedFacts(facts):
state.mode = .loaded(facts: facts)
return .none
case let .receivedError(error):
state.mode = .error(error)
return .none
}
}
}
}

View File

@ -0,0 +1,42 @@
//
// FactsView.swift
// Facts
//
import SwiftUI
import Foundation
import ComposableArchitecture
public struct FactsView: View {
let store: StoreOf<FactsFeature>
public init(store: StoreOf<FactsFeature>) {
self.store = store
}
public var body: some View {
VStack {
switch store.state.mode {
case .notLoaded:
EmptyView()
case .loading:
ProgressView()
case let .loaded(facts):
List(facts, id: \.self) { fact in
Text(fact)
}
case let .error(error):
Text(error.localizedDescription)
}
}
.onAppear {
store.send(.viewAppeared)
}
}
}
#Preview {
FactsView(store: .init(initialState: FactsFeature.State()) {
FactsFeature()
})
}

View File

@ -4,24 +4,39 @@
//
import Foundation
import Facts
import CatFactsKit
import ComposableArchitecture
@Reducer
public struct RootFeature {
@ObservableState
public struct State {
public var baseURL: String?
var facts: FactsFeature.State = .init(baseURL: URL(string: "https://invalid.barf")!)
public var baseURL: URL?
public init() { }
}
public enum Action {
// Add your actions here
case viewAppeared
case facts(FactsFeature.Action)
}
public init() { }
public var body: some ReducerOf<Self> {
EmptyReducer()
Reduce { state, action in
switch action {
case .viewAppeared:
state.facts = .init(baseURL: URL(string: "https://meowfacts.herokuapp.com/")!)
return .none
case .facts:
return .none
}
}
Scope(state: \.facts, action: \.facts) {
FactsFeature()
}
}
}

View File

@ -4,23 +4,22 @@
//
import SwiftUI
import Facts
import ComposableArchitecture
public struct RootView: View {
let store: StoreOf<RootFeature>
@Bindable var store: StoreOf<RootFeature>
public init(store: StoreOf<RootFeature>) {
self.store = store
}
public var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
NavigationStack {
FactsView(store: store.scope(state: \.facts, action: \.facts))
}.onAppear {
store.send(.viewAppeared)
}
.padding()
}
}