Skip to content

Commit 642c30c

Browse files
author
Mohammed Rokon Uddin
authored
Merge pull request #16 from Arman-Morshed/feat/feature_prelude
feat: introduce feature reducer and breaking down action and reducer concept
2 parents 0347cd4 + 983808d commit 642c30c

File tree

14 files changed

+503
-57
lines changed

14 files changed

+503
-57
lines changed

{{cookiecutter.app_name}}/Common/Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.8
1+
// swift-tools-version: 5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -14,7 +14,7 @@ let package = Package(
1414
dependencies: [
1515
.package(
1616
url: "https://github.com/pointfreeco/swift-composable-architecture",
17-
exact: "1.2.0"
17+
exact: "1.5.1"
1818
),
1919
],
2020
targets: [

{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// FeatureReducer.swift
3+
// Common
4+
//
5+
// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}.
6+
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved.
7+
//
8+
9+
import ComposableArchitecture
10+
import SwiftUI
11+
12+
// MARK: FeatureReducer
13+
public protocol FeatureReducer: Reducer where State: Sendable & Hashable, Action == FeatureAction<Self> {
14+
associatedtype ViewAction: Sendable & Equatable = Never
15+
associatedtype InternalAction: Sendable & Equatable = Never
16+
associatedtype ChildAction: Sendable & Equatable = Never
17+
associatedtype DelegateAction: Sendable & Equatable = Never
18+
19+
func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action>
20+
func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action>
21+
func reduce(into state: inout State, childAction: ChildAction) -> Effect<Action>
22+
func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action>
23+
func reduceDismissDestination(into state: inout State) -> Effect<Action>
24+
25+
associatedtype Destination: DestinationReducer = EmptyDestination
26+
associatedtype ViewState: Equatable = Never
27+
}
28+
29+
extension Reducer where Self: FeatureReducer {
30+
public typealias Action = FeatureAction<Self>
31+
32+
public var body: some ReducerOf<Self> {
33+
Reduce(core)
34+
}
35+
36+
public func core(into state: inout State, action: Action) -> Effect<Action> {
37+
switch action {
38+
case .destination(.dismiss):
39+
reduceDismissDestination(into: &state)
40+
case let .destination(.presented(presentedAction)):
41+
reduce(into: &state, presentedAction: presentedAction)
42+
case let .view(viewAction):
43+
reduce(into: &state, viewAction: viewAction)
44+
case let .internal(internalAction):
45+
reduce(into: &state, internalAction: internalAction)
46+
case let .child(childAction):
47+
reduce(into: &state, childAction: childAction)
48+
case .delegate:
49+
.none
50+
}
51+
}
52+
53+
public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
54+
.none
55+
}
56+
57+
public func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> {
58+
.none
59+
}
60+
61+
public func reduce(into state: inout State, childAction: ChildAction) -> Effect<Action> {
62+
.none
63+
}
64+
65+
public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> {
66+
.none
67+
}
68+
69+
public func reduceDismissDestination(into state: inout State) -> Effect<Action> {
70+
.none
71+
}
72+
73+
}
74+
75+
public typealias PresentationStoreOf<R: Reducer> = Store<PresentationState<R.State>, PresentationAction<R.Action>>
76+
77+
// MARK: FeatureAction
78+
@CasePathable
79+
public enum FeatureAction<Feature: FeatureReducer>: Sendable, Equatable {
80+
case destination(PresentationAction<Feature.Destination.Action>)
81+
case view(Feature.ViewAction)
82+
case `internal`(Feature.InternalAction)
83+
case child(Feature.ChildAction)
84+
case delegate(Feature.DelegateAction)
85+
}
86+
87+
// MARK: DestinationReducer
88+
public protocol DestinationReducer: Reducer where State: Sendable & Hashable, Action: Sendable & Equatable & CasePathable { }
89+
90+
// MARK: EmptyDestination
91+
92+
public enum EmptyDestination: DestinationReducer {
93+
public struct State: Sendable, Hashable {}
94+
public typealias Action = Never
95+
public func reduce(into state: inout State, action: Never) -> Effect<Action> {}
96+
public func reduceDismissDestination(into state: inout State) -> Effect<Action> { .none }
97+
}
98+
99+
//MARK: FeatureAction + Hashable
100+
extension FeatureAction: Hashable where Feature.Destination.Action: Hashable,
101+
Feature.ViewAction: Hashable,
102+
Feature.ChildAction: Hashable,
103+
Feature.InternalAction: Hashable,
104+
Feature.DelegateAction: Hashable {
105+
public func hash(into hasher: inout Hasher) {
106+
switch self {
107+
case let .destination(action):
108+
hasher.combine(action)
109+
case let .view(action):
110+
hasher.combine(action)
111+
case let .internal(action):
112+
hasher.combine(action)
113+
case let .child(action):
114+
hasher.combine(action)
115+
case let .delegate(action):
116+
hasher.combine(action)
117+
}
118+
}
119+
}
120+
121+
/// For scoping to an actionless childstore
122+
public func actionless<T>(never: Never) -> T {}
123+
Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.8
1+
// swift-tools-version: 5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -8,21 +8,38 @@ let package = Package(
88
platforms: [.macOS(.v12), .iOS(.v15)],
99
products: [
1010
.library(
11-
name: "Features",
12-
targets: ["Features"]),
11+
name: "App",
12+
targets: ["App"]
13+
),
14+
15+
.library(
16+
name: "Counter",
17+
targets: ["Counter"]
18+
)
1319
],
1420
dependencies: [
21+
.package(path: "../Common"),
1522
.package(
1623
url: "https://github.com/pointfreeco/swift-composable-architecture",
17-
exact: "1.2.0"
24+
exact: "1.5.1"
1825
),
1926
],
2027
targets: [
2128
.target(
22-
name: "Features",
23-
dependencies: []),
24-
.testTarget(
25-
name: "FeaturesTests",
26-
dependencies: ["Features"]),
29+
name: "App",
30+
dependencies: [
31+
"Counter",
32+
.product(name: "Common", package: "Common"),
33+
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
34+
]
35+
),
36+
37+
.target(
38+
name: "Counter",
39+
dependencies: [
40+
.product(name: "Common", package: "Common"),
41+
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
42+
]
43+
)
2744
]
2845
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// AppFeature.swift
3+
// Features
4+
//
5+
// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}.
6+
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved.
7+
//
8+
9+
import Common
10+
import Counter
11+
import ComposableArchitecture
12+
13+
public struct AppFeature: FeatureReducer {
14+
public init() { }
15+
16+
public struct State: Equatable, Hashable {
17+
public init() { }
18+
19+
@PresentationState var destination: Destination.State?
20+
}
21+
22+
public enum ViewAction: Equatable {
23+
case showSheet
24+
case showFullScreenCover
25+
}
26+
27+
public enum InternalAction: Equatable {
28+
case dismissDestination
29+
}
30+
31+
public var body: some ReducerOf<Self> {
32+
Reduce(core)
33+
.ifLet(\.$destination, action: \.destination) {
34+
Destination()
35+
}
36+
}
37+
38+
public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
39+
switch viewAction {
40+
case .showSheet:
41+
state.destination = .sheet(.init())
42+
return .none
43+
44+
case .showFullScreenCover:
45+
state.destination = .fullScreenCover(.init())
46+
return .none
47+
}
48+
}
49+
50+
public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> {
51+
switch presentedAction {
52+
case .sheet(.delegate(.close)):
53+
return .send(.internal(.dismissDestination))
54+
55+
case .fullScreenCover(.delegate(.close)):
56+
return .send(.internal(.dismissDestination))
57+
58+
default:
59+
return .none
60+
}
61+
}
62+
63+
public func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> {
64+
switch internalAction {
65+
case .dismissDestination:
66+
state.destination = nil
67+
return .none
68+
}
69+
}
70+
71+
public struct Destination: DestinationReducer {
72+
73+
public init() { }
74+
75+
@CasePathable
76+
public enum State: Hashable {
77+
case sheet(Counter.State)
78+
case fullScreenCover(Counter.State)
79+
}
80+
81+
@CasePathable
82+
public enum Action: Equatable {
83+
case sheet(Counter.Action)
84+
case fullScreenCover(Counter.Action)
85+
}
86+
87+
public var body: some ReducerOf<Self> {
88+
Scope(state: \.sheet, action: \.sheet) {
89+
Counter()
90+
}
91+
Scope(state: \.fullScreenCover, action: \.fullScreenCover) {
92+
Counter()
93+
}
94+
}
95+
}
96+
}
97+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// AppView.swift
3+
// Features
4+
//
5+
// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}.
6+
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved.
7+
//
8+
9+
import Common
10+
import Counter
11+
import ComposableArchitecture
12+
import SwiftUI
13+
14+
@MainActor
15+
public struct AppView: View {
16+
let store: StoreOf<AppFeature>
17+
18+
public init(store: StoreOf<AppFeature>) {
19+
self.store = store
20+
}
21+
22+
public var body: some View {
23+
WithViewStore(self.store, observe: { $0 }) { viewstore in
24+
Form {
25+
Button {
26+
viewstore.send(.view(.showSheet))
27+
} label: {
28+
Text("Sheet")
29+
}
30+
31+
Button {
32+
viewstore.send(.view(.showFullScreenCover))
33+
} label: {
34+
Text("Full Screen Cover")
35+
}
36+
}
37+
.onAppear()
38+
.destinations(with: store)
39+
}
40+
}
41+
}
42+
43+
private extension StoreOf<AppFeature> {
44+
var destination: PresentationStoreOf<AppFeature.Destination> {
45+
scope(state: \.$destination, action: \.destination)
46+
}
47+
}
48+
49+
@MainActor
50+
private extension View {
51+
func destinations(with store: StoreOf<AppFeature>) -> some View {
52+
let destinationStore = store.destination
53+
return showSheet(with: destinationStore)
54+
.showFulllScreenCover(with: destinationStore)
55+
}
56+
57+
private func showSheet(with destinationStore: PresentationStoreOf<AppFeature.Destination>) -> some View {
58+
sheet(store:
59+
destinationStore.scope(
60+
state: \.sheet,
61+
action: \.sheet)
62+
) { store in
63+
CounterView(store: store)
64+
}
65+
}
66+
67+
private func showFulllScreenCover(with destinationStore: PresentationStoreOf<AppFeature.Destination>) -> some View {
68+
fullScreenCover(store:
69+
destinationStore.scope(
70+
state: \.fullScreenCover,
71+
action: \.fullScreenCover)
72+
) { store in
73+
CounterView(store: store)
74+
}
75+
}
76+
}
77+
78+
79+
#Preview {
80+
AppView(store:
81+
.init(
82+
initialState: AppFeature.State(),
83+
reducer: { AppFeature() }
84+
)
85+
)
86+
}

0 commit comments

Comments
 (0)