├── .github
└── workflows
│ └── Deploy_DocC.yml
├── .gitignore
├── Package.swift
├── README.md
├── Sources
└── CombineFlow
│ ├── Flow
│ ├── Flow.swift
│ └── Flows.swift
│ ├── FlowContributor
│ ├── FlowContributor.swift
│ └── FlowContributors.swift
│ ├── FlowCoordinator.swift
│ ├── Presentable.swift
│ └── Step
│ ├── Step.swift
│ └── Stepper.swift
└── Tests
└── CombineFlowTests
└── CombineFlowTests.swift
/.github/workflows/Deploy_DocC.yml:
--------------------------------------------------------------------------------
1 | name: Deploy DocC to Github Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | hosting:
10 | runs-on: macos-12
11 | steps:
12 | - uses: actions/checkout@v2
13 | with:
14 | fetch-depth: 0
15 | - uses: maxim-lobanov/setup-xcode@v1
16 | with:
17 | xcode-version: '14.0'
18 | - name: config git
19 | run: |
20 | git config --global user.name 'baekteun'
21 | git config --global user.email 'baegteun@gmail.com'
22 | git config pull.rebase false
23 | git checkout -t origin/Deploy_DocC
24 | git pull origin Deploy_DocC
25 | git merge master
26 | - name: Archive Docc
27 | run: |
28 | xcodebuild clean docbuild -scheme CombineFlow \
29 | -destination generic/platform=iOS \
30 | OTHER_DOCC_FLAGS="--transform-for-static-hosting --hosting-base-path CombineFlow --output-path docs"
31 |
32 | - name: git commit & push
33 | run: |
34 | git add -A
35 | git commit -m "📝 :: deploy docc"
36 | git push
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /build
3 | /.build
4 | /.swiftpm
5 | /*.xcodeproj
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "CombineFlow",
8 | platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)],
9 | products: [
10 | .library(
11 | name: "CombineFlow",
12 | targets: ["CombineFlow"])
13 | ],
14 | dependencies: [],
15 | targets: [
16 | .target(
17 | name: "CombineFlow",
18 | dependencies: []
19 | ),
20 | .testTarget(
21 | name: "CombineFlowTests",
22 | dependencies: ["CombineFlow"]
23 | )
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CombineFlow
2 |
3 | Navigation framework for iOS applications based on a Coordinator pattern.
4 |
5 | [Document](https://baekteun.github.io/CombineFlow/documentation/combineflow/)
6 |
7 |
8 |
9 | ## Constents
10 | - [CombineFlow](#combineflow)
11 | - [Constents](#constents)
12 | - [Requirements](#requirements)
13 | - [Overview](#overview)
14 | - [Communication](#communication)
15 | - [Installation](#installation)
16 | - [Swift Package Manager](#swift-package-manager)
17 | - [Manually](#manually)
18 | - [Usage](#usage)
19 | - [Quick Start](#quick-start)
20 |
21 |
22 | ## Requirements
23 | - iOS 13.0+
24 | - Swift 5+
25 |
26 |
27 |
28 | ## Overview
29 | Navigation framework for iOS applications based on a Coordinator pattern
30 |
31 | CombineFlow is inspired [RxFlow](https://github.com/RxSwiftCommunity/RxFlow)
32 |
33 |
34 |
35 | ## Communication
36 | - If you found a bug, open an issue.
37 | - If you have a feature request, open an issue.
38 | - If you want to contribute, submit a pull request.
39 |
40 |
41 |
42 | ## Installation
43 |
44 | ### Swift Package Manager
45 | [Swift Package Manager](https://www.swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
46 |
47 | To integrate `CombineFlow` into your Xcode project using Swift Package Manager, add it to the dependencies value of your Package.swift:
48 |
49 | ```swift
50 | dependencies: [
51 | .package(url: "https://github.com/baekteun/CombineFlow.git", .upToNextMajor(from: "1.0.0"))
52 | ]
53 | ```
54 |
55 | ### Manually
56 | If you prefer not to use either of the aforementioned dependency managers, you can integrate CombineFlow into your project manually.
57 |
58 |
59 |
60 | ## Usage
61 |
62 | ### Quick Start
63 |
64 | ```swift
65 | // create a path
66 | import CombineFlow
67 |
68 | enum ExStep: Step {
69 | case main
70 | }
71 | ```
72 |
73 | ```swift
74 | // create a flow
75 | import CombineFlow
76 | import Combine
77 | import UIKit
78 |
79 | final class MainFlow: Flow {
80 | private let rootVC = UINavigationController()
81 |
82 | var root: Presentable {
83 | rootVC
84 | }
85 |
86 | // navigation
87 | func navigate(to step: any Step) -> FlowContributors {
88 | guard let step = step as? ExStep else { return .none }
89 | switch step {
90 | case .main:
91 | let vc = StepperViewController()
92 | rootVC.setViewControllers([vc], animated: true)
93 | return .one(.contribute(withNextPresentable: vc, withNextStepper: vc))
94 | }
95 | return .none
96 | }
97 | }
98 | ```
99 |
100 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/Flow/Flow.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol Flow: Presentable {
4 | var root: Presentable { get }
5 |
6 | func navigate(to step: any Step) -> FlowContributors
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/Flow/Flows.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public enum Flows {
4 | public static func use(
5 | _ flow: any Flow,
6 | block: @escaping (_ root: Root) -> Void
7 | ) where Root: UIViewController {
8 | guard let root = flow.root as? Root else {
9 | fatalError("The Root type does not match Flow's root")
10 | }
11 | block(root)
12 | }
13 |
14 | public static func use(
15 | _ Flow1: any Flow,
16 | _ Flow2: any Flow,
17 | block: @escaping (_ root1: Root1, _ root2: Root2) -> Void
18 | ) where Root1: UIViewController, Root2: UIViewController {
19 | guard
20 | let root1 = Flow1.root as? Root1,
21 | let root2 = Flow2.root as? Root2
22 | else {
23 | fatalError("The Root type does not match Flow's root")
24 | }
25 | block(root1, root2)
26 | }
27 |
28 | public static func use(
29 | _ Flow1: any Flow,
30 | _ Flow2: any Flow,
31 | _ Flow3: any Flow,
32 | block: @escaping (_ root1: Root1, _ root2: Root2, _ root3: Root3) -> Void
33 | ) where Root1: UIViewController, Root2: UIViewController, Root3: UIViewController {
34 | guard
35 | let root1 = Flow1.root as? Root1,
36 | let root2 = Flow2.root as? Root2,
37 | let root3 = Flow3.root as? Root3
38 | else {
39 | fatalError("The Root type does not match Flow's root")
40 | }
41 | block(root1, root2, root3)
42 | }
43 |
44 | public static func use(
45 | _ Flow1: any Flow,
46 | _ Flow2: any Flow,
47 | _ Flow3: any Flow,
48 | _ Flow4: any Flow,
49 | block: @escaping (_ root1: Root1, _ root2: Root2, _ root3: Root3, _ root4: Root4) -> Void
50 | )
51 | where
52 | Root1: UIViewController,
53 | Root2: UIViewController,
54 | Root3: UIViewController,
55 | Root4: UIViewController {
56 | guard
57 | let root1 = Flow1.root as? Root1,
58 | let root2 = Flow2.root as? Root2,
59 | let root3 = Flow3.root as? Root3,
60 | let root4 = Flow4.root as? Root4
61 | else {
62 | fatalError("The Root type does not match Flow's root")
63 | }
64 | block(root1, root2, root3, root4)
65 | }
66 |
67 | public static func use(
68 | _ Flow1: any Flow,
69 | _ Flow2: any Flow,
70 | _ Flow3: any Flow,
71 | _ Flow4: any Flow,
72 | _ Flow5: any Flow,
73 | block: @escaping (_ root1: Root1, _ root2: Root2, _ root3: Root3, _ root4: Root4, _ root5: Root5) -> Void
74 | ) where Root1: UIViewController,
75 | Root2: UIViewController,
76 | Root3: UIViewController,
77 | Root4: UIViewController,
78 | Root5: UIViewController {
79 | guard
80 | let root1 = Flow1.root as? Root1,
81 | let root2 = Flow2.root as? Root2,
82 | let root3 = Flow3.root as? Root3,
83 | let root4 = Flow4.root as? Root4,
84 | let root5 = Flow5.root as? Root5
85 | else {
86 | fatalError("The Root type does not match Flow's root")
87 | }
88 | block(root1, root2, root3, root4, root5)
89 | }
90 |
91 | public static func use(
92 | _ Flows: [some Flow],
93 | block: @escaping (_ roots: [UIViewController]) -> Void
94 | ) {
95 | let roots = Flows
96 | .compactMap { $0.root as? UIViewController }
97 | block(roots)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/FlowContributor/FlowContributor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum FlowContributor {
4 | case contribute(
5 | withNextPresentable: any Presentable,
6 | withNextRouter: any Stepper = DefaultStepper()
7 | )
8 | case forwardToCurrentFlow(withStep: any Step)
9 | case forwardToParentFlow(withStep: any Step)
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/FlowContributor/FlowContributors.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public enum FlowContributors {
4 | case one(flowContributor: FlowContributor)
5 | case multiple(flowContributors: [FlowContributor])
6 | case end(forwardToParentFlowWithStep: any Step)
7 | case none
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/FlowCoordinator.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public final class FlowCoordinator {
5 | private let id = UUID().uuidString
6 | private var bag = Set()
7 | private weak var parentFlowCoordinator: FlowCoordinator?
8 | private var childFlowCoordinators: [String: FlowCoordinator] = [:]
9 | private let stepsSubject = PassthroughSubject()
10 |
11 | public func coordinate(
12 | flow: any Flow,
13 | with stepper: any Stepper = DefaultStepper()
14 | ) {
15 | stepsSubject
16 | .append(Publishers.Merge(stepper.steps, stepsSubject))
17 | .receive(on: RunLoop.main)
18 | .map { flow.navigate(to: $0) }
19 | .handleEvents(receiveOutput: { [weak self] contributors in
20 | switch contributors {
21 | case .none:
22 | return
23 |
24 | case let .one(contributor):
25 | self?.performSideEffect(contributor: contributor)
26 |
27 | case let .multiple(contributors):
28 | contributors.forEach { self?.performSideEffect(contributor: $0) }
29 |
30 | case let .end(path):
31 | self?.parentFlowCoordinator?.stepsSubject.send(path)
32 | self?.childFlowCoordinators.removeAll()
33 | self?.parentFlowCoordinator?
34 | .childFlowCoordinators
35 | .removeValue(forKey: self?.id ?? "")
36 | }
37 | })
38 | .map { [weak self] in self?.nextSteppers(from: $0) ?? [] }
39 | .flatMap { $0.publisher.eraseToAnyPublisher() }
40 | .flatMap { [weak self] in self?.toSteps(from: $0) ?? Empty().eraseToAnyPublisher() }
41 | .sink { [weak self] step in
42 | self?.stepsSubject.send(step)
43 | }
44 | .store(in: &bag)
45 |
46 | stepper.steps
47 | .sink { [weak self] step in
48 | self?.stepsSubject.send(step)
49 | }
50 | .store(in: &bag)
51 |
52 | Just(stepper.initialStep)
53 | .sink { [weak self] step in
54 | self?.stepsSubject.send(step)
55 | }
56 | .store(in: &bag)
57 | }
58 | }
59 |
60 | private extension FlowCoordinator {
61 | private func performSideEffect(contributor: FlowContributor) {
62 | switch contributor {
63 | case let .contribute(presentable, router):
64 | guard let childMoordinator = presentable as? Flow else { return }
65 | let flowCoordinator = FlowCoordinator()
66 | flowCoordinator.parentFlowCoordinator = self
67 | self.childFlowCoordinators[flowCoordinator.id] = flowCoordinator
68 | flowCoordinator.coordinate(flow: childMoordinator, with: router)
69 |
70 | case let .forwardToCurrentFlow(step):
71 | self.stepsSubject.send(step)
72 |
73 | case let .forwardToParentFlow(step):
74 | self.parentFlowCoordinator?.stepsSubject.send(step)
75 | }
76 | }
77 |
78 | private func nextSteppers(from contributors: FlowContributors) -> [any Stepper] {
79 | switch contributors {
80 | case let .one(.contribute(_, stepper)):
81 | return [stepper]
82 |
83 | case let .multiple(flowContributors):
84 | return flowContributors.compactMap {
85 | if case let .contribute(_, stepper) = $0 {
86 | return stepper
87 | }
88 | return nil
89 | }
90 |
91 | default:
92 | return []
93 | }
94 | }
95 |
96 | private func toSteps(from stepper: any Stepper) -> AnyPublisher {
97 | stepper.steps
98 | .filter { !($0 is NoneStep) }
99 | .eraseToAnyPublisher()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/Presentable.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol Presentable {}
4 |
5 | extension UIViewController: Presentable {}
6 | extension UIWindow: Presentable {}
7 |
8 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/Step/Step.swift:
--------------------------------------------------------------------------------
1 | public protocol Step {}
2 |
3 | public struct NoneStep: Step {}
4 |
--------------------------------------------------------------------------------
/Sources/CombineFlow/Step/Stepper.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public protocol Stepper {
5 | var steps: PassthroughSubject { get }
6 | var initialStep: any Step { get }
7 | }
8 |
9 | public extension Stepper {
10 | var initialStep: any Step {
11 | NoneStep()
12 | }
13 | }
14 |
15 | public final class OneStepper: Stepper {
16 | public let steps: PassthroughSubject = .init()
17 | private let singleStep: any Step
18 |
19 | init(singleStep: any Step) {
20 | self.singleStep = singleStep
21 | }
22 |
23 | public var initialStep: Step {
24 | singleStep
25 | }
26 | }
27 |
28 | public final class DefaultStepper: Stepper {
29 | public let steps: PassthroughSubject = .init()
30 |
31 | public init() {}
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/CombineFlowTests/CombineFlowTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import CombineFlow
3 |
4 | final class CombineFlowTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | }
10 | }
11 |
--------------------------------------------------------------------------------