Skip to content

Commit 844dd5e

Browse files
committed
feat: API & Performance Improvements
- Optimizations: - Destinations now use strong references to controllers and explicitly removes those references when controllers are popped from the navigation stack, no more just-in-case cleanups in StackDestination on each wrappedValue access - API Improvements - With new navigationStack/Destination methods controllers can be implicitly instantiated from Destination wrappers, all users have to do is provide mappings from Route to a corresponding destination
1 parent 9576f50 commit 844dd5e

15 files changed

+391
-93
lines changed

Example/Sources/CombineNavigationExample/FeedTabFeature/FeedTabController.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ public final class FeedTabController: ComposableViewControllerOf<FeedTabFeature>
5858
publisher.map(\.path).removeDuplicates(by: { $0.ids == $1.ids }),
5959
ids: \.ids,
6060
route: { $0[id: $1] },
61-
switch: destinations { destination, route, id in
61+
switch: destinations { destinations, route in
6262
switch route {
6363
case .feed:
64-
destination.$feedControllers[id]
64+
destinations.$feedControllers
6565
case .profile:
66-
destination.$profileControllers[id]
66+
destinations.$profileControllers
6767
}
6868
},
6969
onPop: capture { _self, ids in

Example/Sources/CombineNavigationExample/TweetsFeedFeature/TweetsFeedController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public final class TweetsFeedController: ComposableViewControllerOf<TweetsFeedFe
4545
navigationDestination(
4646
"reply_detail",
4747
isPresented: publisher.detail.isNotNil,
48-
controller: _detailController.callAsFunction,
48+
destination: $detailController,
4949
onPop: captureSend(.detail(.dismiss))
5050
)
5151
.store(in: &cancellables)

README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ final class MyViewController: UIViewController {
5252
switch: destinations { destinations, route in
5353
switch route {
5454
case .details:
55-
destinations.$detailsController()
55+
destinations.$detailsController
5656
}
5757
},
5858
onPop: capture { _self in
@@ -79,7 +79,7 @@ final class MyViewController: UIViewController {
7979
navigationDestination(
8080
"my_feature_details"
8181
isPresented: viewModel.publisher(for: \.state.detais.isNotNil),
82-
controller: destinations { $0.$detailsController() },
82+
destination: $detailsController,
8383
onPop: capture { $0.viewModel.send(.dismiss) }
8484
).store(in: &cancellables)
8585
}
@@ -110,12 +110,12 @@ final class MyViewController: UIViewController {
110110
func bindViewModel() {
111111
navigationStack(
112112
viewModel.publisher(for: \.state.path),
113-
switch: destinations { destinations, route, index in
113+
switch: destinations { destinations, route in
114114
switch route {
115115
case .featureA:
116-
destinations.$featureAControllers[index]
116+
destinations.$featureAControllers
117117
case .featureB:
118-
destinations.$featureBControllers[index]
118+
destinations.$featureBControllers
119119
}
120120
},
121121
onPop: capture { _self, indices in

Sources/CombineNavigation/CocoaViewController+API.swift

+84
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@ import FoundationExtensions
99
// MARK: navigationStack
1010

1111
extension CocoaViewController {
12+
/// Subscribes on publisher of navigation stack state
13+
public func navigationStack<
14+
P: Publisher,
15+
C: Collection & Equatable,
16+
Route: Hashable
17+
>(
18+
_ publisher: P,
19+
switch destination: @escaping (Route) -> any GrouppedDestinationProtocol<C.Index>,
20+
onPop: @escaping ([C.Index]) -> Void
21+
) -> Cancellable where
22+
P.Output == C,
23+
P.Failure == Never,
24+
C.Element == Route,
25+
C.Index: Hashable,
26+
C.Indices: Equatable
27+
{
28+
combineNavigationRouter.navigationStack(
29+
publisher,
30+
switch: destination,
31+
onPop: onPop
32+
)
33+
}
34+
1235
/// Subscribes on publisher of navigation stack state
1336
public func navigationStack<
1437
P: Publisher,
@@ -32,6 +55,32 @@ extension CocoaViewController {
3255
)
3356
}
3457

58+
/// Subscribes on publisher of navigation stack state
59+
public func navigationStack<
60+
P: Publisher,
61+
Stack,
62+
IDs: Collection & Equatable,
63+
Route
64+
>(
65+
_ publisher: P,
66+
ids: @escaping (Stack) -> IDs,
67+
route: @escaping (Stack, IDs.Element) -> Route?,
68+
switch destination: @escaping (Route) -> any GrouppedDestinationProtocol<IDs.Element>,
69+
onPop: @escaping ([IDs.Element]) -> Void
70+
) -> Cancellable where
71+
P.Output == Stack,
72+
P.Failure == Never,
73+
IDs.Element: Hashable
74+
{
75+
combineNavigationRouter.navigationStack(
76+
publisher,
77+
ids: ids,
78+
route: route,
79+
switch: destination,
80+
onPop: onPop
81+
)
82+
}
83+
3584
/// Subscribes on publisher of navigation stack state
3685
public func navigationStack<
3786
P: Publisher,
@@ -62,6 +111,24 @@ extension CocoaViewController {
62111
// MARK: navigationDestination
63112

64113
extension CocoaViewController {
114+
/// Subscribes on publisher of navigation destination state
115+
public func navigationDestination<P: Publisher>(
116+
_ id: AnyHashable,
117+
isPresented publisher: P,
118+
destination: SingleDestinationProtocol,
119+
onPop: @escaping () -> Void
120+
) -> AnyCancellable where
121+
P.Output == Bool,
122+
P.Failure == Never
123+
{
124+
combineNavigationRouter.navigationDestination(
125+
id,
126+
isPresented: publisher,
127+
destination: destination,
128+
onPop: onPop
129+
)
130+
}
131+
65132
/// Subscribes on publisher of navigation destination state
66133
public func navigationDestination<P: Publisher>(
67134
_ id: AnyHashable,
@@ -80,6 +147,23 @@ extension CocoaViewController {
80147
)
81148
}
82149

150+
/// Subscribes on publisher of navigation destination state
151+
public func navigationDestination<P: Publisher, Route>(
152+
_ publisher: P,
153+
switch destination: @escaping (Route) -> SingleDestinationProtocol,
154+
onPop: @escaping () -> Void
155+
) -> AnyCancellable where
156+
Route: Hashable,
157+
P.Output == Route?,
158+
P.Failure == Never
159+
{
160+
combineNavigationRouter.navigationDestination(
161+
publisher,
162+
switch: destination,
163+
onPop: onPop
164+
)
165+
}
166+
83167
/// Subscribes on publisher of navigation destination state
84168
public func navigationDestination<P: Publisher, Route>(
85169
_ publisher: P,

Sources/CombineNavigation/Destinations/StackDestination.swift

+59-19
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,54 @@ import CocoaAliases
44
import Combine
55
import FoundationExtensions
66

7+
public protocol GrouppedDestinationProtocol<DestinationID> {
8+
associatedtype DestinationID: Hashable
9+
10+
@_spi(Internals)
11+
func _initControllerIfNeeded(for id: DestinationID) -> CocoaViewController
12+
13+
@_spi(Internals)
14+
func _invalidateDestination(for id: DestinationID)
15+
}
16+
717
/// Wrapper for creating and accessing managed navigation stack controllers
818
@propertyWrapper
919
open class StackDestination<
10-
StackElementID: Hashable,
20+
DestinationID: Hashable,
1121
Controller: CocoaViewController
12-
>: Weakifiable {
13-
private var _controllers: [StackElementID: Weak<Controller>] = [:]
22+
>: Weakifiable, GrouppedDestinationProtocol {
23+
@_spi(Internals)
24+
open var _controllers: [DestinationID: Controller] = [:]
1425

15-
open var wrappedValue: [StackElementID: Controller] {
16-
let controllers = _controllers.compactMapValues(\.wrappedValue)
17-
_controllers = controllers.mapValues(Weak.init(wrappedValue:))
18-
return controllers
26+
open var wrappedValue: [DestinationID: Controller] {
27+
_controllers
1928
}
2029

21-
open var projectedValue: StackDestination<StackElementID, Controller> { self }
30+
@inlinable
31+
open var projectedValue: StackDestination<DestinationID, Controller> { self }
2232

23-
private var _initControllerOverride: ((StackElementID) -> Controller)?
33+
@usableFromInline
34+
internal var _initControllerOverride: ((DestinationID) -> Controller)?
2435

25-
private var _configuration: ((Controller, StackElementID) -> Void)?
36+
@usableFromInline
37+
internal var _configuration: ((Controller, DestinationID) -> Void)?
2638

2739
/// Sets instance-specific override for creating a new controller
2840
///
2941
/// This override has the highest priority when creating a new controller
3042
///
3143
/// To disable isntance-specific override pass `nil` to this method
44+
@inlinable
3245
public func overrideInitController(
33-
with closure: ((StackElementID) -> Controller)?
46+
with closure: ((DestinationID) -> Controller)?
3447
) {
3548
_initControllerOverride = closure
3649
}
3750

3851
/// Sets instance-specific configuration for controllers
52+
@inlinable
3953
public func setConfiguration(
40-
_ closure: ((Controller, StackElementID) -> Void)?
54+
_ closure: ((Controller, DestinationID) -> Void)?
4155
) {
4256
_configuration = closure
4357
closure.map { configure in
@@ -47,15 +61,19 @@ open class StackDestination<
4761
}
4862
}
4963

50-
@_spi(Internals) open class func initController(
51-
for id: StackElementID
64+
@_spi(Internals)
65+
@inlinable
66+
open class func initController(
67+
for id: DestinationID
5268
) -> Controller {
5369
return Controller()
5470
}
5571

56-
@_spi(Internals) open func configureController(
72+
@_spi(Internals)
73+
@inlinable
74+
open func configureController(
5775
_ controller: Controller,
58-
for id: StackElementID
76+
for id: DestinationID
5977
) {}
6078

6179
/// Creates a new instance
@@ -70,16 +88,31 @@ open class StackDestination<
7088
/// doesn't have a custom init you'll have to use this method or if you have a base controller that
7189
/// requires custom init it'll be beneficial for you to create a custom subclass of StackDestination
7290
/// and override it's `initController` class method, you can find an example in tests.
73-
public convenience init(_ initControllerOverride: @escaping (StackElementID) -> Controller) {
91+
@inlinable
92+
public convenience init(_ initControllerOverride: @escaping (DestinationID) -> Controller) {
7493
self.init()
7594
self.overrideInitController(with: initControllerOverride)
7695
}
7796

97+
@_spi(Internals)
98+
@inlinable
99+
public func _initControllerIfNeeded(
100+
for id: DestinationID
101+
) -> CocoaViewController {
102+
return self[id]
103+
}
104+
105+
@_spi(Internals)
106+
@inlinable
107+
open func _invalidateDestination(for id: DestinationID) {
108+
self._controllers.removeValue(forKey: id)
109+
}
110+
78111
/// Returns `wrappedValue[id]` if present, intializes and configures a new instance otherwise
79-
public subscript(_ id: StackElementID) -> Controller {
112+
public subscript(_ id: DestinationID) -> Controller {
80113
let controller = wrappedValue[id] ?? {
81114
let controller = _initControllerOverride?(id) ?? Self.initController(for: id)
82-
_controllers[id] = Weak(controller)
115+
_controllers[id] = controller
83116
configureController(controller, for: id)
84117
_configuration?(controller, id)
85118
return controller
@@ -89,3 +122,10 @@ open class StackDestination<
89122
}
90123
}
91124
#endif
125+
126+
/*
127+
- Add erased protocols for Tree/StackDestination with some cleanup function
128+
- Make RoutingController.destinations method return an instance of the protocol
129+
- In CocoaViewController+API.swift add custom navigationStack/Destination methods
130+
- Those methods will inject cleanup function call into onPop handler
131+
*/

0 commit comments

Comments
 (0)