Skip to content

Commit 5fa1eb3

Browse files
committed
Initial commit.
0 parents  commit 5fa1eb3

10 files changed

+458
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Ethan Wong
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// swift-tools-version:5.5
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "RoutableNavigation",
7+
platforms: [
8+
.iOS(.v13)
9+
],
10+
products: [
11+
.library(
12+
name: "RoutableNavigation",
13+
targets: ["RoutableNavigation"]
14+
),
15+
],
16+
dependencies: [
17+
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.0.0")),
18+
.package(url: "https://github.com/BoltDocs/ObjectAssociationHelper.git", .exact("1.0.0")),
19+
],
20+
targets: [
21+
.target(
22+
name: "RoutableNavigation",
23+
dependencies: [
24+
.product(name: "RxSwift", package: "RxSwift"),
25+
.product(name: "RxCocoa", package: "RxSwift"),
26+
"ObjectAssociationHelper",
27+
],
28+
path: "./RoutableNavigation"
29+
),
30+
.testTarget(
31+
name: "RoutableNavigationTests",
32+
dependencies: ["RoutableNavigation"],
33+
path: "./RoutableNavigationTests"
34+
),
35+
]
36+
)

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# RoutableNavigation
2+
3+
Two-way binding between UINavigationController and route objects, based on [RxSwift](https://github.com/ReactiveX/RxSwift).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2022 Ethan Wong
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import RxCocoa
26+
import RxSwift
27+
28+
public final class NavigationRouteCoordinator<Element: RouteElement> {
29+
30+
public var currentRoute: Driver<Route<Element>> {
31+
return _currentRoute
32+
.asDriver()
33+
.map { $0.0 }
34+
}
35+
36+
var routingActions: Signal<[RoutingAction<Element>]> {
37+
return _routingActions.asSignal()
38+
}
39+
40+
private lazy var _currentRoute = BehaviorRelay<(Route<Element>, Bool)>(value: (Route([]), false))
41+
42+
private lazy var _routingActions = PublishRelay<[RoutingAction<Element>]>()
43+
44+
private var disposeBag = DisposeBag()
45+
46+
public init() {
47+
_currentRoute
48+
.withPrevious(startWith: nil)
49+
.map { previous, current in
50+
if current.1 == true {
51+
return Self.routingActionsForTransition(
52+
from: previous?.0.elements ?? [],
53+
to: current.0.elements
54+
)
55+
}
56+
return []
57+
}
58+
.bind(to: _routingActions)
59+
.disposed(by: disposeBag)
60+
}
61+
62+
public func changeRoute(_ route: [Element], performsSideEffect: Bool = true) {
63+
_currentRoute.accept((Route(route), performsSideEffect))
64+
}
65+
66+
public func push(_ element: Element, performsSideEffect: Bool = true) {
67+
var oldRoute = _currentRoute.value.0.elements
68+
oldRoute.append(element)
69+
changeRoute(oldRoute, performsSideEffect: performsSideEffect)
70+
}
71+
72+
public func pop(performsSideEffect: Bool = true) {
73+
var oldRoute = _currentRoute.value.0.elements
74+
oldRoute.removeLast()
75+
changeRoute(oldRoute, performsSideEffect: performsSideEffect)
76+
}
77+
78+
private static func routingActionsForTransition(
79+
from oldRoute: [Element],
80+
to newRoute: [Element]
81+
) -> [RoutingAction<Element>] {
82+
func calcCommonSubrouteCount(oldRoute: [Element], newRoute: [Element]) -> Int {
83+
var commonSubrouteCount = 0
84+
while
85+
commonSubrouteCount < newRoute.count &&
86+
commonSubrouteCount < oldRoute.count &&
87+
newRoute[commonSubrouteCount] == oldRoute[commonSubrouteCount]
88+
{
89+
commonSubrouteCount += 1
90+
}
91+
return commonSubrouteCount
92+
}
93+
94+
let commonSubrouteCount = calcCommonSubrouteCount(oldRoute: oldRoute, newRoute: newRoute)
95+
96+
var routingActions = [RoutingAction<Element>]()
97+
98+
if commonSubrouteCount == 0 && newRoute.isEmpty {
99+
routingActions.append(.replaceRoot(element: newRoute.first!))
100+
routingActions.append(
101+
contentsOf: newRoute[1...].map { RoutingAction.push(element: $0) }
102+
)
103+
} else {
104+
routingActions.append(
105+
contentsOf: [RoutingAction<Element>](
106+
repeating: .pop,
107+
count: oldRoute.count - commonSubrouteCount
108+
)
109+
)
110+
routingActions.append(
111+
contentsOf: newRoute[(commonSubrouteCount)...].map { RoutingAction.push(element: $0) }
112+
)
113+
}
114+
return routingActions
115+
}
116+
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2022 Ethan Wong
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import RxSwift
26+
27+
extension ObservableType {
28+
29+
func withPrevious(startWith first: Element? = nil) -> Observable<(previous: Element?, current: Element)> {
30+
return scan((nil, first)) { previous, current in
31+
return (previous.1, current)
32+
}
33+
.map { return ($0.0, $0.1!) }
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2022 Ethan Wong
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import UIKit
26+
27+
import RxSwift
28+
29+
public enum RoutingAction<Element: RouteElement> {
30+
case push(element: Element)
31+
case pop
32+
case replaceRoot(element: Element)
33+
}
34+
35+
open class RoutableNavigationController<Element: RouteElement>: UINavigationController {
36+
37+
private var disposeBag = DisposeBag()
38+
39+
public let coordinator: NavigationRouteCoordinator<Element>
40+
41+
public init(coordinator: NavigationRouteCoordinator<Element>) {
42+
self.coordinator = coordinator
43+
super.init(nibName: nil, bundle: nil)
44+
}
45+
46+
@available(*, unavailable)
47+
public required init?(coder aDecoder: NSCoder) {
48+
fatalError("\(#function) has not been implemented")
49+
}
50+
51+
// swiftlint:disable:next unavailable_function
52+
open func viewController(forRouteElement element: Element) -> UIViewController? {
53+
fatalError("viewController(forRouteElement:) is meant be implemented by subclass")
54+
}
55+
56+
open override func viewDidLoad() {
57+
super.viewDidLoad()
58+
59+
rx.willShow.map { $0.viewController }
60+
.withLatestFrom(coordinator.currentRoute) {
61+
// (topViewController, currentHashes)
62+
return ($0, $1.elements.map { $0.routeHash })
63+
}
64+
.subscribe(with: self) { owner, val in
65+
let willShowController = val.0
66+
let currentHashes = val.1
67+
68+
let controllerHashes = owner.viewControllers.map { $0.routeHash }
69+
print("[RoutableNav]: Did received controller change, State hashes: \(currentHashes), Controller hashes: \(controllerHashes)")
70+
71+
if !(controllerHashes == currentHashes) {
72+
print("[RoutableNav]: Route not match, perfoming adjustment")
73+
if
74+
Array(currentHashes[0..<(currentHashes.endIndex - 1)]) == controllerHashes,
75+
willShowController.routeHash == controllerHashes.last
76+
{
77+
// 第一类校正: 用户 pop 了最上方的 controller
78+
print("[RoutableNav]: Topmost controller poped by user, syncing state change")
79+
owner.coordinator.pop(performsSideEffect: false)
80+
} else {
81+
// 其他无法处理的场景
82+
assertionFailure("Cannot perform route adjustment, route state can be corruped")
83+
}
84+
}
85+
}
86+
.disposed(by: disposeBag)
87+
88+
coordinator.routingActions
89+
.emit(with: self) { owner, actions in
90+
let destViewControllers = actions.reduce(owner.viewControllers) { viewControllers, action in
91+
var viewControllers = viewControllers
92+
switch action {
93+
case .pop:
94+
viewControllers.removeLast()
95+
case .push(let element):
96+
if let destViewController = owner.viewController(forRouteElement: element) {
97+
assert(destViewController.routeHash != nil)
98+
viewControllers.append(destViewController)
99+
}
100+
case .replaceRoot(let element):
101+
if let destViewController = owner.viewController(forRouteElement: element) {
102+
assert(destViewController.routeHash != nil)
103+
viewControllers = [destViewController]
104+
}
105+
}
106+
return viewControllers
107+
}
108+
// Disable animation to prevent random ordered controllers on delegate calls
109+
owner.setViewControllers(destViewControllers, animated: false)
110+
}
111+
.disposed(by: disposeBag)
112+
}
113+
114+
}

RoutableNavigation/RouteElement.swift

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2022 Ethan Wong
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
public protocol RouteElement: Hashable {
26+
27+
var routeHash: String { get }
28+
29+
}
30+
31+
public extension RouteElement {
32+
33+
static func == (lhs: Self, rhs: Self) -> Bool {
34+
return lhs.routeHash == rhs.routeHash
35+
}
36+
37+
func hash(into hasher: inout Hasher) {
38+
hasher.combine(routeHash)
39+
}
40+
41+
}
42+
43+
public struct Route<Element: RouteElement>: Equatable {
44+
45+
public let elements: [Element]
46+
47+
public init(_ elements: [Element]) {
48+
self.elements = elements
49+
}
50+
51+
}

0 commit comments

Comments
 (0)