Skip to content

Commit

Permalink
Merge pull request #16 from Arman-Morshed/feat/feature_prelude
Browse files Browse the repository at this point in the history
feat: introduce feature reducer and breaking down action and reducer concept
  • Loading branch information
Mohammed Rokon Uddin authored Dec 12, 2023
2 parents 0347cd4 + 983808d commit 642c30c
Show file tree
Hide file tree
Showing 14 changed files with 503 additions and 57 deletions.
4 changes: 2 additions & 2 deletions {{cookiecutter.app_name}}/Common/Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.8
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -14,7 +14,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture",
exact: "1.2.0"
exact: "1.5.1"
),
],
targets: [
Expand Down
22 changes: 0 additions & 22 deletions {{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift

This file was deleted.

123 changes: 123 additions & 0 deletions {{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift
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 {}

35 changes: 26 additions & 9 deletions {{cookiecutter.app_name}}/Features/Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.8
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -8,21 +8,38 @@ let package = Package(
platforms: [.macOS(.v12), .iOS(.v15)],
products: [
.library(
name: "Features",
targets: ["Features"]),
name: "App",
targets: ["App"]
),

.library(
name: "Counter",
targets: ["Counter"]
)
],
dependencies: [
.package(path: "../Common"),
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture",
exact: "1.2.0"
exact: "1.5.1"
),
],
targets: [
.target(
name: "Features",
dependencies: []),
.testTarget(
name: "FeaturesTests",
dependencies: ["Features"]),
name: "App",
dependencies: [
"Counter",
.product(name: "Common", package: "Common"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),

.target(
name: "Counter",
dependencies: [
.product(name: "Common", package: "Common"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
)
]
)
97 changes: 97 additions & 0 deletions {{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift
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 {{cookiecutter.app_name}}/Features/Sources/App/AppView.swift
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() }
)
)
}
Loading

0 comments on commit 642c30c

Please sign in to comment.