Add subfeature complete with dependency injection, more or less
This commit is contained in:
parent
498716fd67
commit
ac29eb40c6
@ -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
|
||||
),
|
||||
]
|
||||
|
||||
@ -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) {
|
||||
|
||||
25
Meow/Sources/Core/CatFacts+Meow.swift
Normal file
25
Meow/Sources/Core/CatFacts+Meow.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
31
Meow/Sources/Core/ClientFactory.swift
Normal file
31
Meow/Sources/Core/ClientFactory.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
64
Meow/Sources/Facts/FactsFeature.swift
Normal file
64
Meow/Sources/Facts/FactsFeature.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Meow/Sources/Facts/FactsView.swift
Normal file
42
Meow/Sources/Facts/FactsView.swift
Normal 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()
|
||||
})
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user