Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce feature reducer and breaking down action and reducer concept #16

Merged
merged 4 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading