@@ -33,80 +33,167 @@ private extension AnyReactor {
3333
3434 // MARK: - Public
3535
36- public func route< R: Reactor > ( _ reactor: R . Type = R . self, _ destination: R . Destination ) {
37- route ( type: reactor, destination: destination)
36+ /// Routes to a specified destination within a reactor. The reactor must be present in the navigation hierarchy.
37+ ///
38+ /// This function searches for the specified reactor type within the navigation hierarchy and attempts to route to
39+ /// the given destination if the reactor is found.
40+ ///
41+ /// - Parameters:
42+ /// - reactor: The type of reactor to search for in the navigation hierarchy
43+ /// - destination: The destination associated with the given reactor
44+ /// - Returns: `true` if routing succeeded (the reactor was found in the hierarchy), `false` otherwise
45+ @discardableResult
46+ public func route< R: Reactor > ( _ reactor: R . Type = R . self, _ destination: R . Destination ) -> Bool {
47+ let lastFoundReactor = navigationPath. root. depthFirstSearch ( NavigationStep ( ) , predicate: { lhs, rhs in
48+ guard let rReactor = rhs. reactor else { return false }
49+ return rReactor. is ( ofType: reactor)
50+ } )
51+
52+ guard let lastFoundReactor else { return false }
53+ lastFoundReactor. value. mutator ? ( ( destination as! AnyDestination ) )
54+
55+ return true
3856 }
39-
40- public func route< each R : Reactor > ( type: repeat ( each R ) . Type, destination: repeat ( each R ) . Destination) {
57+
58+ /// Routes through a path of destinations across multiple reactors.
59+ ///
60+ /// Routing starts with the first reactor, which must be present in the navigation hierarchy.
61+ /// For subsequent destinations, if their reactors are not in the navigation hierarchy, this function yields
62+ /// to the main run loop, allowing the UI to update its state. This process enables SwiftUI to create
63+ /// destination reactors from previous steps.
64+ ///
65+ /// - Parameters:
66+ /// - type: A pack representing the reactors to route through
67+ /// - destination: A pack representing the destinations associated with each reactor type
68+ public func route< each R : Reactor > ( type: repeat ( each R ) . Type, destination: repeat ( each R ) . Destination) async {
4169 for (type, destination) in repeat ( each type, each destination) {
42- let lastSuchReactor = navigationPath. root. depthFirstSearch ( NavigationStep ( ) , predicate: { lhs, rhs in
43- guard let rReactor = rhs. reactor else { return false }
44- return rReactor. is ( ofType: type)
45- } )
46-
47- guard let lastSuchReactor else { continue }
48- lastSuchReactor. value. mutator ? ( ( destination as! AnyDestination ) )
70+ var attempts = 0
71+ var success = false
72+ repeat {
73+ attempts += 1
74+ success = route ( type, destination)
75+ if !success {
76+ await Task . yield ( )
77+ }
78+ } while !success && attempts < 10
4979 }
5080 }
5181
52- public func pop( last count: Int = 1 ) {
82+ /// Pops the current destination from the navigation hierarchy.
83+ ///
84+ /// Performs a single back navigation step. If the current screen is presenting a destination,
85+ /// the presentation is cleared; otherwise the screen itself is dismissed by its parent.
86+ /// Does nothing when already at the root level.
87+ public func pop( ) {
88+ let currentDepth = navigationPath. lastActiveNode. depth
89+ guard currentDepth > 1 else { return }
90+ pop ( node: navigationPath. lastActiveNode)
91+ }
92+
93+ /// Pops multiple destinations from the navigation hierarchy.
94+ ///
95+ /// Pops up to `count` steps back. Between each step, this function yields to the main
96+ /// run loop, allowing the UI to update its state. If `count` exceeds the available depth,
97+ /// this operation pops to the root.
98+ ///
99+ /// - Parameters:
100+ /// - count: The number of steps to pop. Defaults to 1.
101+ public func pop( last count: Int = 1 ) async {
53102 let currentDepth = navigationPath. lastActiveNode. depth
54103 guard currentDepth > 1 else { return }
55- guard count < currentDepth else { return pop ( last: currentDepth - 1 ) }
104+ guard count < currentDepth else { return await pop ( last: currentDepth - 1 ) }
56105
57106 for _ in 0 ..< count {
58- let currentNode = navigationPath. lastActiveNode
59- let parentNode = currentNode. parent
60-
61- if let parentNode {
62- if parentNode. value. isTabs {
63- // parent is tabs and screen may be presenting something directly
64- if currentNode. value. currentDestination != nil && !currentNode. value. isTabs {
65- currentNode. value. mutator ? ( nil )
66- } else {
67- popTabAttemptDetected ( )
68- return
69- }
70- } else {
71- // current destination is a reactor, pop from parent
72- parentNode. value. mutator ? ( nil )
73- }
107+ guard pop ( node: navigationPath. lastActiveNode) else { return }
108+ await Task . yield ( )
109+ }
110+ }
111+
112+ /// Pops back to the nearest destination whose reactor matches the given type.
113+ ///
114+ /// If a match is found, screens are popped one by one, yielding to the main run loop between steps.
115+ /// If no matching destination exists, no action is taken.
116+ ///
117+ /// - Parameters:
118+ /// - reactor: The reactor type to pop to
119+ public func popTo< R: Reactor > ( _ reactor: R . Type ) async {
120+ var nodesToPop : [ TreeNode < NavigationStep > ] = [ ]
121+ var currentNode = navigationPath. lastActiveNode
122+ var foundMatch = false
123+
124+ // iterate upwards to find matching reactor
125+ while let parentNode = currentNode. parent {
126+ nodesToPop. append ( currentNode)
127+
128+ if currentNode. value. reactor? . is ( ofType: reactor) ?? false {
129+ foundMatch = true
130+ break
74131 }
132+
133+ currentNode = parentNode
134+ }
135+
136+ // check if there is a match
137+ guard foundMatch else { return }
138+
139+ // pop screens only if possible
140+ for node in nodesToPop {
141+ guard pop ( node: node) else { return }
142+ await Task . yield ( )
75143 }
76144 }
77145
146+ /// Drops inactive navigation branches (including tabs) and preserves only the active path.
147+ ///
148+ /// Use this to reset previous tab and screen state once a new path becomes active. It is
149+ /// safe to call after switching tabs or routing across tabs to forget the previous tab’s
150+ /// state. This is also appropriate after major context changes (for example, transitioning
151+ /// from a logged-out flow to a logged-in flow, or resetting an onboarding).
152+ ///
153+ /// Notes:
154+ /// - Internally, the navigation tree is pruned to keep only the path from root to the last
155+ /// active node, effectively resetting tabs.
156+ /// - When context tabs are switched, sibling tabs are marked inactive. Calling
157+ /// `cleanup()` prunes those inactive siblings and any inactive branches, effectively
158+ /// removing old branches and inactive tabs while keeping the current tab/screen intact.
159+ /// - The currently active screen and tab remain unchanged.
160+ /// - The function is idempotent, calling it when there are no inactive branches is a no-op.
78161 public func cleanup( ) {
79162 navigationPath. cleanup ( )
80163 }
81164
82- public func popTo< R: Reactor > ( _ reactor: R . Type ) {
83- var currentNode = navigationPath. lastActiveNode
84- while var parentNode = currentNode. parent {
85- if parentNode. value. isTabs {
86- // parent is tabs, something is presented and presenting node has specified type
87- if currentNode. value. currentDestination != nil && !currentNode. value. isTabs {
88- if currentNode. value. reactor? . is ( ofType: reactor) ?? false {
89- currentNode. value. mutator ? ( nil )
90- return
91- } else {
92- currentNode = parentNode
93- continue
94- }
95- } else {
96- popTabAttemptDetected ( )
97- return
98- }
165+ }
166+
167+ // MARK: - Private
168+
169+ private extension Router {
170+
171+ @discardableResult
172+ func pop( node currentNode: TreeNode < NavigationStep > ) -> Bool {
173+ guard let parentNode = currentNode. parent else {
174+ // cannot pop root
175+ return false
176+ }
177+
178+ if parentNode. value. isTabs {
179+ if currentNode. value. currentDestination != nil && !currentNode. value. isTabs {
180+ // even when parent is a tab, it may be presenting something directly
181+ currentNode. value. mutator ? ( nil )
182+ return true
99183 } else {
100- // current destination is a reactor, pop from parent if this is the expected destination
101- // or continue searching up the hierarchy
102- let parentReactor = parentNode. value. reactor
103- if parentReactor? . is ( ofType: reactor) ?? false {
104- parentNode. value. mutator ? ( nil )
105- return
106- } else {
107- currentNode = parentNode
108- continue
109- }
184+ // tab is not presenting anything
185+ popTabAttemptDetected ( )
186+ return false
187+ }
188+ } else {
189+ if currentNode. value. currentDestination != nil && !currentNode. value. isTabs {
190+ // current node is a screen and is presenting something directly
191+ currentNode. value. mutator ? ( nil )
192+ return true
193+ } else {
194+ // current node is a screen and is not presenting, popped from parent
195+ parentNode. value. mutator ? ( nil )
196+ return true
110197 }
111198 }
112199 }
0 commit comments