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)
|
.swiftLanguageMode(.v6)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
let tcaDependency = Target.Dependency.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Meow",
|
name: "Meow",
|
||||||
platforms: [.iOS(.v18), .macOS(.v15)],
|
platforms: [.iOS(.v18), .macOS(.v15)],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
.library(name: "CatFactsKit", targets: ["CatFactsKit"]),
|
||||||
.library(
|
.library(name: "Core", targets: ["Core"]),
|
||||||
name: "CatFactsKit",
|
.library(name: "Root", targets: ["Root"]),
|
||||||
targets: ["CatFactsKit"]
|
.library(name: "Facts", targets: ["Facts"])
|
||||||
),
|
|
||||||
.library(
|
|
||||||
name: "Root",
|
|
||||||
targets: ["Root"]
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.2"),
|
.package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.2"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.23.1"),
|
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.23.1"),
|
||||||
],
|
],
|
||||||
targets: [
|
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(
|
.target(
|
||||||
name: "CatFactsKit",
|
name: "CatFactsKit",
|
||||||
swiftSettings: commonSwiftSettings
|
swiftSettings: commonSwiftSettings
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "Core",
|
||||||
|
dependencies: ["CatFactsKit", tcaDependency],
|
||||||
|
swiftSettings: commonSwiftSettings
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Root",
|
name: "Root",
|
||||||
dependencies: [
|
dependencies: ["CatFactsKit", "Core", "Facts", tcaDependency],
|
||||||
"CatFactsKit",
|
swiftSettings: commonSwiftSettings
|
||||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
|
),
|
||||||
],
|
.target(
|
||||||
|
name: "Facts",
|
||||||
|
dependencies: ["CatFactsKit", "Core", tcaDependency],
|
||||||
swiftSettings: commonSwiftSettings
|
swiftSettings: commonSwiftSettings
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "CatFactsKitTests",
|
name: "CatFactsKitTests",
|
||||||
dependencies: [
|
dependencies: ["CatFactsKit", "Mocker"],
|
||||||
"CatFactsKit",
|
resources: [.process("Fixtures")],
|
||||||
"Mocker"
|
|
||||||
],
|
|
||||||
resources: [
|
|
||||||
.process("Fixtures")
|
|
||||||
],
|
|
||||||
swiftSettings: commonSwiftSettings
|
swiftSettings: commonSwiftSettings
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct CatFacts {
|
public struct CatFacts: Sendable {
|
||||||
let baseURL: URL
|
public let baseURL: URL
|
||||||
let urlSession: URLSession
|
let urlSession: URLSession
|
||||||
|
|
||||||
public init(baseURL: URL, urlSession: URLSession = .shared) {
|
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 Foundation
|
||||||
|
import Facts
|
||||||
|
import CatFactsKit
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
|
||||||
@Reducer
|
@Reducer
|
||||||
public struct RootFeature {
|
public struct RootFeature {
|
||||||
@ObservableState
|
@ObservableState
|
||||||
public struct State {
|
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 init() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Action {
|
public enum Action {
|
||||||
// Add your actions here
|
case viewAppeared
|
||||||
|
case facts(FactsFeature.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init() { }
|
public init() { }
|
||||||
|
|
||||||
public var body: some ReducerOf<Self> {
|
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 SwiftUI
|
||||||
|
import Facts
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
|
||||||
public struct RootView: View {
|
public struct RootView: View {
|
||||||
let store: StoreOf<RootFeature>
|
@Bindable var store: StoreOf<RootFeature>
|
||||||
|
|
||||||
public init(store: StoreOf<RootFeature>) {
|
public init(store: StoreOf<RootFeature>) {
|
||||||
self.store = store
|
self.store = store
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack {
|
NavigationStack {
|
||||||
Image(systemName: "globe")
|
FactsView(store: store.scope(state: \.facts, action: \.facts))
|
||||||
.imageScale(.large)
|
}.onAppear {
|
||||||
.foregroundStyle(.tint)
|
store.send(.viewAppeared)
|
||||||
Text("Hello, world!")
|
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user