-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from Arman-Morshed/feat/feature_prelude
feat: introduce feature reducer and breaking down action and reducer concept
- Loading branch information
Showing
14 changed files
with
503 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 0 additions & 22 deletions
22
{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift
This file was deleted.
Oops, something went wrong.
123 changes: 123 additions & 0 deletions
123
{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
// | ||
// FeatureReducer.swift | ||
// Common | ||
// | ||
// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. | ||
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. | ||
// | ||
|
||
import ComposableArchitecture | ||
import SwiftUI | ||
|
||
// MARK: FeatureReducer | ||
public protocol FeatureReducer: Reducer where State: Sendable & Hashable, Action == FeatureAction<Self> { | ||
associatedtype ViewAction: Sendable & Equatable = Never | ||
associatedtype InternalAction: Sendable & Equatable = Never | ||
associatedtype ChildAction: Sendable & Equatable = Never | ||
associatedtype DelegateAction: Sendable & Equatable = Never | ||
|
||
func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> | ||
func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> | ||
func reduce(into state: inout State, childAction: ChildAction) -> Effect<Action> | ||
func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> | ||
func reduceDismissDestination(into state: inout State) -> Effect<Action> | ||
|
||
associatedtype Destination: DestinationReducer = EmptyDestination | ||
associatedtype ViewState: Equatable = Never | ||
} | ||
|
||
extension Reducer where Self: FeatureReducer { | ||
public typealias Action = FeatureAction<Self> | ||
|
||
public var body: some ReducerOf<Self> { | ||
Reduce(core) | ||
} | ||
|
||
public func core(into state: inout State, action: Action) -> Effect<Action> { | ||
switch action { | ||
case .destination(.dismiss): | ||
reduceDismissDestination(into: &state) | ||
case let .destination(.presented(presentedAction)): | ||
reduce(into: &state, presentedAction: presentedAction) | ||
case let .view(viewAction): | ||
reduce(into: &state, viewAction: viewAction) | ||
case let .internal(internalAction): | ||
reduce(into: &state, internalAction: internalAction) | ||
case let .child(childAction): | ||
reduce(into: &state, childAction: childAction) | ||
case .delegate: | ||
.none | ||
} | ||
} | ||
|
||
public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> { | ||
.none | ||
} | ||
|
||
public func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> { | ||
.none | ||
} | ||
|
||
public func reduce(into state: inout State, childAction: ChildAction) -> Effect<Action> { | ||
.none | ||
} | ||
|
||
public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> { | ||
.none | ||
} | ||
|
||
public func reduceDismissDestination(into state: inout State) -> Effect<Action> { | ||
.none | ||
} | ||
|
||
} | ||
|
||
public typealias PresentationStoreOf<R: Reducer> = Store<PresentationState<R.State>, PresentationAction<R.Action>> | ||
|
||
// MARK: FeatureAction | ||
@CasePathable | ||
public enum FeatureAction<Feature: FeatureReducer>: Sendable, Equatable { | ||
case destination(PresentationAction<Feature.Destination.Action>) | ||
case view(Feature.ViewAction) | ||
case `internal`(Feature.InternalAction) | ||
case child(Feature.ChildAction) | ||
case delegate(Feature.DelegateAction) | ||
} | ||
|
||
// MARK: DestinationReducer | ||
public protocol DestinationReducer: Reducer where State: Sendable & Hashable, Action: Sendable & Equatable & CasePathable { } | ||
|
||
// MARK: EmptyDestination | ||
|
||
public enum EmptyDestination: DestinationReducer { | ||
public struct State: Sendable, Hashable {} | ||
public typealias Action = Never | ||
public func reduce(into state: inout State, action: Never) -> Effect<Action> {} | ||
public func reduceDismissDestination(into state: inout State) -> Effect<Action> { .none } | ||
} | ||
|
||
//MARK: FeatureAction + Hashable | ||
extension FeatureAction: Hashable where Feature.Destination.Action: Hashable, | ||
Feature.ViewAction: Hashable, | ||
Feature.ChildAction: Hashable, | ||
Feature.InternalAction: Hashable, | ||
Feature.DelegateAction: Hashable { | ||
public func hash(into hasher: inout Hasher) { | ||
switch self { | ||
case let .destination(action): | ||
hasher.combine(action) | ||
case let .view(action): | ||
hasher.combine(action) | ||
case let .internal(action): | ||
hasher.combine(action) | ||
case let .child(action): | ||
hasher.combine(action) | ||
case let .delegate(action): | ||
hasher.combine(action) | ||
} | ||
} | ||
} | ||
|
||
/// For scoping to an actionless childstore | ||
public func actionless<T>(never: Never) -> T {} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// | ||
// AppFeature.swift | ||
// Features | ||
// | ||
// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. | ||
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. | ||
// | ||
|
||
import Common | ||
import Counter | ||
import ComposableArchitecture | ||
|
||
public struct AppFeature: FeatureReducer { | ||
public init() { } | ||
|
||
public struct State: Equatable, Hashable { | ||
public init() { } | ||
|
||
@PresentationState var destination: Destination.State? | ||
} | ||
|
||
public enum ViewAction: Equatable { | ||
case showSheet | ||
case showFullScreenCover | ||
} | ||
|
||
public enum InternalAction: Equatable { | ||
case dismissDestination | ||
} | ||
|
||
public var body: some ReducerOf<Self> { | ||
Reduce(core) | ||
.ifLet(\.$destination, action: \.destination) { | ||
Destination() | ||
} | ||
} | ||
|
||
public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> { | ||
switch viewAction { | ||
case .showSheet: | ||
state.destination = .sheet(.init()) | ||
return .none | ||
|
||
case .showFullScreenCover: | ||
state.destination = .fullScreenCover(.init()) | ||
return .none | ||
} | ||
} | ||
|
||
public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> { | ||
switch presentedAction { | ||
case .sheet(.delegate(.close)): | ||
return .send(.internal(.dismissDestination)) | ||
|
||
case .fullScreenCover(.delegate(.close)): | ||
return .send(.internal(.dismissDestination)) | ||
|
||
default: | ||
return .none | ||
} | ||
} | ||
|
||
public func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> { | ||
switch internalAction { | ||
case .dismissDestination: | ||
state.destination = nil | ||
return .none | ||
} | ||
} | ||
|
||
public struct Destination: DestinationReducer { | ||
|
||
public init() { } | ||
|
||
@CasePathable | ||
public enum State: Hashable { | ||
case sheet(Counter.State) | ||
case fullScreenCover(Counter.State) | ||
} | ||
|
||
@CasePathable | ||
public enum Action: Equatable { | ||
case sheet(Counter.Action) | ||
case fullScreenCover(Counter.Action) | ||
} | ||
|
||
public var body: some ReducerOf<Self> { | ||
Scope(state: \.sheet, action: \.sheet) { | ||
Counter() | ||
} | ||
Scope(state: \.fullScreenCover, action: \.fullScreenCover) { | ||
Counter() | ||
} | ||
} | ||
} | ||
} | ||
|
86 changes: 86 additions & 0 deletions
86
{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// | ||
// AppView.swift | ||
// Features | ||
// | ||
// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. | ||
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. | ||
// | ||
|
||
import Common | ||
import Counter | ||
import ComposableArchitecture | ||
import SwiftUI | ||
|
||
@MainActor | ||
public struct AppView: View { | ||
let store: StoreOf<AppFeature> | ||
|
||
public init(store: StoreOf<AppFeature>) { | ||
self.store = store | ||
} | ||
|
||
public var body: some View { | ||
WithViewStore(self.store, observe: { $0 }) { viewstore in | ||
Form { | ||
Button { | ||
viewstore.send(.view(.showSheet)) | ||
} label: { | ||
Text("Sheet") | ||
} | ||
|
||
Button { | ||
viewstore.send(.view(.showFullScreenCover)) | ||
} label: { | ||
Text("Full Screen Cover") | ||
} | ||
} | ||
.onAppear() | ||
.destinations(with: store) | ||
} | ||
} | ||
} | ||
|
||
private extension StoreOf<AppFeature> { | ||
var destination: PresentationStoreOf<AppFeature.Destination> { | ||
scope(state: \.$destination, action: \.destination) | ||
} | ||
} | ||
|
||
@MainActor | ||
private extension View { | ||
func destinations(with store: StoreOf<AppFeature>) -> some View { | ||
let destinationStore = store.destination | ||
return showSheet(with: destinationStore) | ||
.showFulllScreenCover(with: destinationStore) | ||
} | ||
|
||
private func showSheet(with destinationStore: PresentationStoreOf<AppFeature.Destination>) -> some View { | ||
sheet(store: | ||
destinationStore.scope( | ||
state: \.sheet, | ||
action: \.sheet) | ||
) { store in | ||
CounterView(store: store) | ||
} | ||
} | ||
|
||
private func showFulllScreenCover(with destinationStore: PresentationStoreOf<AppFeature.Destination>) -> some View { | ||
fullScreenCover(store: | ||
destinationStore.scope( | ||
state: \.fullScreenCover, | ||
action: \.fullScreenCover) | ||
) { store in | ||
CounterView(store: store) | ||
} | ||
} | ||
} | ||
|
||
|
||
#Preview { | ||
AppView(store: | ||
.init( | ||
initialState: AppFeature.State(), | ||
reducer: { AppFeature() } | ||
) | ||
) | ||
} |
Oops, something went wrong.