From c5a22f971b32e391db5e785c0545af167f6fb349 Mon Sep 17 00:00:00 2001 From: Arman Morshed Date: Wed, 6 Dec 2023 12:01:30 +0600 Subject: [PATCH 1/4] bump version and introduce feature reducer --- .../Common/Package.swift | 2 +- .../Common/Sources/Common/BaseAction.swift | 22 ---- .../Sources/Common/FeatureReducer.swift | 122 ++++++++++++++++++ .../Features/Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 29 +++-- 5 files changed, 143 insertions(+), 34 deletions(-) delete mode 100644 {{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift create mode 100644 {{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift diff --git a/{{cookiecutter.app_name}}/Common/Package.swift b/{{cookiecutter.app_name}}/Common/Package.swift index 2c4f46b..56a8f91 100644 --- a/{{cookiecutter.app_name}}/Common/Package.swift +++ b/{{cookiecutter.app_name}}/Common/Package.swift @@ -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: [ diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift deleted file mode 100644 index 1ea9089..0000000 --- a/{{cookiecutter.app_name}}/Common/Sources/Common/BaseAction.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// BaseAction.swift -// Common -// -// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. -// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. -// - -import Foundation - -// keep Actions organized and their intent explicit -// https://github.com/pointfreeco/swift-composable-architecture/discussions/1440 - -public protocol BaseAction { - associatedtype ViewAction - associatedtype DelegateAction - associatedtype InlyingAction - - static func view(_: ViewAction) -> Self - static func delegate(_: DelegateAction) -> Self - static func inlying(_: InlyingAction) -> Self - } diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift new file mode 100644 index 0000000..5e2bf96 --- /dev/null +++ b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift @@ -0,0 +1,122 @@ +// +// FeatureReducer.swift +// +// +// Created by Md. Arman Morshed on 6/12/23. +// + +import ComposableArchitecture +import SwiftUI + +// MARK: FeatureReducer +public protocol FeatureReducer: Reducer where State: Sendable & Hashable Action == FeatureAction { + 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 + func reduce(into state: inout State, internalAction: InternalAction) -> Effect + func reduce(into state: inout State, childAction: ChildAction) -> Effect + func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect + func reduceDismissDestination(into state: inout State) -> Effect + + associatedtype Destination: DestinationReducer = EmptyDestination + associatedtype ViewState: Equatable = Never +} + +extension Reducer where Self: FeatureReducer { + public typealias Action = FeatureAction + + public var body: some ReducerOf { + Reduce(core) + } + + public func core(into state: inout State, action: Action) -> Effect { + 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 { + .none + } + + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { + .none + } + + public func reduce(into state: inout State, childAction: ChildAction) -> Effect { + .none + } + + public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect { + .none + } + + public func reduceDismissDestination(into state: inout State) -> Effect { + .none + } + +} + +public typealias PresentationStoreOf = Store, PresentationAction> + +// MARK: FeatureAction +@CasePathable +public enum FeatureAction: Sendable, Equatable { + case destination(PresentationAction) + 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 {} + public func reduceDismissDestination(into state: inout State) -> Effect { .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(never: Never) -> T {} + diff --git a/{{cookiecutter.app_name}}/Features/Package.swift b/{{cookiecutter.app_name}}/Features/Package.swift index 53cd0bc..65109d1 100644 --- a/{{cookiecutter.app_name}}/Features/Package.swift +++ b/{{cookiecutter.app_name}}/Features/Package.swift @@ -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: [ diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 836f744..d114f95 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version" : "1.0.0" + "revision" : "a5521dde99570789d8cb7c43e51418d7cd1a87ca", + "version" : "1.1.2" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "a7c1f799b55ecb418f85094b142565834f7ee7c7", - "version" : "1.2.0" + "revision" : "dcde72151de8a60eecaa1673ed3c3d110549069a", + "version" : "1.5.1" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "ea631ce892687f5432a833312292b80db238186a", - "version" : "1.0.0" + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", - "version" : "1.0.0" + "revision" : "9783b58167f7618cb86011156e741cbc6f4cc864", + "version" : "1.1.2" } }, { @@ -117,13 +117,22 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" + } + }, { "identity" : "swiftui-navigation", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swiftui-navigation", "state" : { - "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", - "version" : "1.0.0" + "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", + "version" : "1.2.0" } }, { From 5f2e8c2bd0e2de4f0a21e004b9daa063d120c5d1 Mon Sep 17 00:00:00 2001 From: Arman Morshed Date: Wed, 6 Dec 2023 18:32:14 +0600 Subject: [PATCH 2/4] feat: add counter example to demostrate the feature logic --- .../Common/Package.swift | 2 +- .../Sources/Common/FeatureReducer.swift | 2 +- .../Features/Package.swift | 33 +++++-- .../Features/Sources/App/AppFeature.swift | 96 +++++++++++++++++++ .../Features/Sources/App/AppView.swift | 85 ++++++++++++++++ .../Sources/Counter/CounterFeature.swift | 42 ++++++++ .../Sources/Counter/CounterView.swift | 60 ++++++++++++ .../Features/Sources/Features/Features.swift | 6 -- .../NetworkPlatform/Package.swift | 2 +- .../Utilities/Package.swift | 2 +- .../project.pbxproj | 26 ++++- .../Application/MainApp.swift | 9 +- 12 files changed, 341 insertions(+), 24 deletions(-) create mode 100644 {{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift create mode 100644 {{cookiecutter.app_name}}/Features/Sources/App/AppView.swift create mode 100644 {{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift create mode 100644 {{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift delete mode 100644 {{cookiecutter.app_name}}/Features/Sources/Features/Features.swift diff --git a/{{cookiecutter.app_name}}/Common/Package.swift b/{{cookiecutter.app_name}}/Common/Package.swift index 56a8f91..6122973 100644 --- a/{{cookiecutter.app_name}}/Common/Package.swift +++ b/{{cookiecutter.app_name}}/Common/Package.swift @@ -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 diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift index 5e2bf96..4d37854 100644 --- a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift +++ b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift @@ -9,7 +9,7 @@ import ComposableArchitecture import SwiftUI // MARK: FeatureReducer -public protocol FeatureReducer: Reducer where State: Sendable & Hashable Action == FeatureAction { +public protocol FeatureReducer: Reducer where State: Sendable & Hashable, Action == FeatureAction { associatedtype ViewAction: Sendable & Equatable = Never associatedtype InternalAction: Sendable & Equatable = Never associatedtype ChildAction: Sendable & Equatable = Never diff --git a/{{cookiecutter.app_name}}/Features/Package.swift b/{{cookiecutter.app_name}}/Features/Package.swift index 65109d1..aa29676 100644 --- a/{{cookiecutter.app_name}}/Features/Package.swift +++ b/{{cookiecutter.app_name}}/Features/Package.swift @@ -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 @@ -8,10 +8,17 @@ 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.5.1" @@ -19,10 +26,20 @@ let package = Package( ], 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"), + ] + ) ] ) diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift new file mode 100644 index 0000000..32c6768 --- /dev/null +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift @@ -0,0 +1,96 @@ +// +// AppFeature.swift +// +// +// Created by Md. Arman Morshed on 6/12/23. +// + +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 { + Reduce(core) + .ifLet(\.$destination, action: \.destination) { + Destination() + } + } + + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { + 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 { + 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 { + 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 { + Scope(state: \.sheet, action: \.sheet) { + Counter() + } + Scope(state: \.fullScreenCover, action: \.fullScreenCover) { + Counter() + } + } + } +} + diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift new file mode 100644 index 0000000..c148ec6 --- /dev/null +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift @@ -0,0 +1,85 @@ +// +// AppView.swift +// +// +// Created by Md. Arman Morshed on 6/12/23. +// + +import Common +import Counter +import ComposableArchitecture +import SwiftUI + +@MainActor +public struct AppView: View { + let store: StoreOf + + public init(store: StoreOf) { + 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 { + var destination: PresentationStoreOf { + scope(state: \.$destination, action: \.destination) + } +} + +@MainActor +private extension View { + func destinations(with store: StoreOf) -> some View { + let destinationStore = store.destination + return showSheet(with: destinationStore) + .showFulllScreenCover(with: destinationStore) + } + + private func showSheet(with destinationStore: PresentationStoreOf) -> some View { + sheet(store: + destinationStore.scope( + state: \.sheet, + action: \.sheet) + ) { store in + CounterView(store: store) + } + } + + private func showFulllScreenCover(with destinationStore: PresentationStoreOf) -> some View { + fullScreenCover(store: + destinationStore.scope( + state: \.fullScreenCover, + action: \.fullScreenCover) + ) { store in + CounterView(store: store) + } + } +} + + +#Preview { + AppView(store: + .init( + initialState: AppFeature.State(), + reducer: { AppFeature() } + ) + ) +} diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift new file mode 100644 index 0000000..b53a9c0 --- /dev/null +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift @@ -0,0 +1,42 @@ +// +// CounterFeature.swift +// +// +// Created by Md. Arman Morshed on 6/12/23. +// + +import Foundation +import Common +import ComposableArchitecture + +public struct Counter: FeatureReducer { + + public init() { } + + public struct State: Equatable, Hashable { + public init() { } + + var count = 0 + } + + public enum ViewAction: Equatable { + case decrementButtonTapped + case incrementButtonTapped + } + + public enum DelegateAction: Equatable { + case close + } + + public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { + switch viewAction { + case .decrementButtonTapped: + state.count -= 1 + return .none + + case .incrementButtonTapped: + state.count += 1 + return .none + } + } +} diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift new file mode 100644 index 0000000..4abe0b7 --- /dev/null +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift @@ -0,0 +1,60 @@ +// +// CounterView.swift +// +// +// Created by Md. Arman Morshed on 6/12/23. +// + +import Common +import ComposableArchitecture +import SwiftUI + +@MainActor +public struct CounterView: View { + let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewstore in + VStack(spacing: 16) { + HStack { + Button { + viewstore.send(.view(.decrementButtonTapped)) + } label: { + Image(systemName: "minus") + } + + Text("\(viewstore.count)") + .monospacedDigit() + + Button { + viewstore.send(.view(.incrementButtonTapped)) + } label: { + Image(systemName: "plus") + } + } + + Button { + viewstore.send(.delegate(.close)) + } label: { + Text("Dismiss") + .foregroundStyle(.white) + .frame(width: 120, height: 40) + .background(.blue) + } + } + } + } +} + +#Preview { + CounterView(store: + .init( + initialState: Counter.State(), + reducer: { Counter() } + ) + ) +} diff --git a/{{cookiecutter.app_name}}/Features/Sources/Features/Features.swift b/{{cookiecutter.app_name}}/Features/Sources/Features/Features.swift deleted file mode 100644 index c91ff16..0000000 --- a/{{cookiecutter.app_name}}/Features/Sources/Features/Features.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct Features { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift b/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift index ac70ebe..f037386 100644 --- a/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift +++ b/{{cookiecutter.app_name}}/NetworkPlatform/Package.swift @@ -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 diff --git a/{{cookiecutter.app_name}}/Utilities/Package.swift b/{{cookiecutter.app_name}}/Utilities/Package.swift index a6dbdca..fcfc1a3 100644 --- a/{{cookiecutter.app_name}}/Utilities/Package.swift +++ b/{{cookiecutter.app_name}}/Utilities/Package.swift @@ -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 diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj index e2b14af..8a9fd74 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 67B2B5C529127D3A00B2E0AC /* Staging.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 67B2B5BD29127D3A00B2E0AC /* Staging.xcconfig */; }; 67B2B5C729127D3A00B2E0AC /* Development.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 67B2B5C029127D3A00B2E0AC /* Development.xcconfig */; }; 67B2B5CA29127D3A00B2E0AC /* Production.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 67B2B5C429127D3A00B2E0AC /* Production.xcconfig */; }; + B20307482B207C8600EBA314 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = B20307472B207C8600EBA314 /* App */; }; + B203074B2B207D4E00EBA314 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = B203074A2B207D4E00EBA314 /* ComposableArchitecture */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,6 +80,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B203074B2B207D4E00EBA314 /* ComposableArchitecture in Frameworks */, + B20307482B207C8600EBA314 /* App in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -248,6 +252,8 @@ ); name = "{{cookiecutter.app_name}}"; packageProductDependencies = ( + B20307472B207C8600EBA314 /* App */, + B203074A2B207D4E00EBA314 /* ComposableArchitecture */, ); productName = "{{cookiecutter.app_name}}"; productReference = 2F7B0E56273AD7AC00C68B1F /* {{cookiecutter.app_name}}.app */; @@ -323,7 +329,7 @@ ); mainGroup = 2F7B0E4D273AD7AC00C68B1F; packageReferences = ( - 6782D5B32912554D0015FCBB /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + B20307492B207D4E00EBA314 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, ); productRefGroup = 2F7B0E57273AD7AC00C68B1F /* Products */; projectDirPath = ""; @@ -1315,15 +1321,27 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6782D5B32912554D0015FCBB /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { + B20307492B207D4E00EBA314 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; + repositoryURL = "git@github.com:pointfreeco/swift-composable-architecture.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.0; + minimumVersion = 1.5.1; }; }; /* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + B20307472B207C8600EBA314 /* App */ = { + isa = XCSwiftPackageProductDependency; + productName = App; + }; + B203074A2B207D4E00EBA314 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + package = B20307492B207D4E00EBA314 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 2F7B0E4E273AD7AC00C68B1F /* Project object */; } diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift index 36e70cc..a5ef175 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/Application/MainApp.swift @@ -6,15 +6,20 @@ // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // +import App +import ComposableArchitecture import SwiftUI #warning("Please rename to your app name") @main struct MainApp: App { var body: some Scene { + let store = Store(initialState: AppFeature.State(), + reducer: { AppFeature() } + ) + WindowGroup { - Text("👋🏼 🌎") - .font(.largeTitle) + AppView(store: store) } } } From 0b986cda1e884e5312ff1429af99adacea301fcc Mon Sep 17 00:00:00 2001 From: Arman Morshed Date: Thu, 7 Dec 2023 11:33:57 +0600 Subject: [PATCH 3/4] chore: add copyright template --- .../Common/Sources/Common/FeatureReducer.swift | 5 +++-- .../Features/Sources/App/AppFeature.swift | 5 +++-- {{cookiecutter.app_name}}/Features/Sources/App/AppView.swift | 5 +++-- .../Features/Sources/Counter/CounterFeature.swift | 5 +++-- .../Features/Sources/Counter/CounterView.swift | 5 +++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift index 4d37854..67688a2 100644 --- a/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift +++ b/{{cookiecutter.app_name}}/Common/Sources/Common/FeatureReducer.swift @@ -1,8 +1,9 @@ // // FeatureReducer.swift +// Common // -// -// Created by Md. Arman Morshed on 6/12/23. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import ComposableArchitecture diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift index 32c6768..79b266f 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppFeature.swift @@ -1,8 +1,9 @@ // // AppFeature.swift +// Features // -// -// Created by Md. Arman Morshed on 6/12/23. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import Common diff --git a/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift b/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift index c148ec6..b35029e 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/App/AppView.swift @@ -1,8 +1,9 @@ // // AppView.swift +// Features // -// -// Created by Md. Arman Morshed on 6/12/23. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import Common diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift index b53a9c0..96c73f6 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift @@ -1,8 +1,9 @@ // // CounterFeature.swift +// Features // -// -// Created by Md. Arman Morshed on 6/12/23. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import Foundation diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift index 4abe0b7..c67a017 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift @@ -1,8 +1,9 @@ // // CounterView.swift +// Features // -// -// Created by Md. Arman Morshed on 6/12/23. +// Created by {{ cookiecutter.creator }} on {% now 'utc', '%d/%m/%Y' %}. +// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.company_name}}. All rights reserved. // import Common From 983808df92b16ad942b3039ace80d18119a1b17f Mon Sep 17 00:00:00 2001 From: Arman Morshed Date: Thu, 7 Dec 2023 12:33:33 +0600 Subject: [PATCH 4/4] feat: call delete action from internal action instead from view action --- .../Features/Sources/Counter/CounterFeature.swift | 15 +++++++++++++++ .../Features/Sources/Counter/CounterView.swift | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift index 96c73f6..a4a84f9 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterFeature.swift @@ -23,6 +23,11 @@ public struct Counter: FeatureReducer { public enum ViewAction: Equatable { case decrementButtonTapped case incrementButtonTapped + case closeButtonTapped + } + + public enum InternalAction: Equatable { + case close } public enum DelegateAction: Equatable { @@ -38,6 +43,16 @@ public struct Counter: FeatureReducer { case .incrementButtonTapped: state.count += 1 return .none + + case .closeButtonTapped: + return .send(.internal(.close)) + } + } + + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { + switch internalAction { + case .close: + return .send(.delegate(.close)) } } } diff --git a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift index c67a017..63fa1fd 100644 --- a/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift +++ b/{{cookiecutter.app_name}}/Features/Sources/Counter/CounterView.swift @@ -39,7 +39,7 @@ public struct CounterView: View { } Button { - viewstore.send(.delegate(.close)) + viewstore.send(.view(.closeButtonTapped)) } label: { Text("Dismiss") .foregroundStyle(.white)