Skip to content

Commit ef205ae

Browse files
committed
Update event logging to parseable NavigationEvents
1 parent 9d0b320 commit ef205ae

File tree

9 files changed

+163
-50
lines changed

9 files changed

+163
-50
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Navigator Changelog
22

3+
### 0.9.26
4+
5+
* Update event logging to parseable NavigationEvents
6+
37
### 0.9.25
48

59
* Confusion between dismissAny and dismissAll, dismissAll is now dismissAnyChildren

Sources/Navigator/Navigator/Core/NavigationCheckpoint.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ extension NavigationState {
134134

135135
internal func returnToCheckpoint<T>(_ checkpoint: NavigationCheckpoint<T>) {
136136
guard let (navigator, found) = find(checkpoint) else {
137-
log(type:.warning, "Navigator checkpoint not found in current navigation tree: \(checkpoint.name)")
137+
log(.warning("checkpoint not found in current navigation tree: \(checkpoint.name)"))
138138
return
139139
}
140-
log("Navigator returning to checkpoint: \(checkpoint.name)")
140+
log(.checkpoint(.returning(checkpoint.name)))
141141
_ = navigator.dismissAnyChildren()
142142
_ = navigator.pop(to: found.index)
143143
// send trigger to specific action handler
@@ -149,10 +149,10 @@ extension NavigationState {
149149

150150
internal func returnToCheckpoint<T: Hashable>(_ checkpoint: NavigationCheckpoint<T>, value: T) {
151151
guard let (navigator, found) = find(checkpoint) else {
152-
log(type:.warning, "Navigator checkpoint value handler not found: \(checkpoint.name)")
152+
log(.warning("checkpoint value handler not found: \(checkpoint.name)"))
153153
return
154154
}
155-
log("Navigator returning to checkpoint: \(checkpoint.name) value: \(value)")
155+
log(.checkpoint(.returningWithValue(checkpoint.name, value)))
156156
// return to sender
157157
_ = navigator.dismissAnyChildren()
158158
_ = navigator.pop(to: found.index)
@@ -176,13 +176,15 @@ extension NavigationState {
176176
return
177177
}
178178
checkpoints[entry.key] = entry
179-
log("Navigator \(id) adding checkpoint: \(entry.key)")
179+
log(.checkpoint(.adding(checkpoint.name)))
180+
// log("Navigator \(id) adding checkpoint: \(entry.key)")
180181
}
181182

182183
internal func cleanCheckpoints() {
183184
checkpoints = checkpoints.filter {
184185
guard $1.index <= path.count else {
185-
log("Navigator \(id) removing checkpoint: \($1.key)")
186+
log(.checkpoint(.removing($1.key)))
187+
// log("Navigator \(id) removing checkpoint: \($1.key)")
186188
return false
187189
}
188190
return true
@@ -279,7 +281,7 @@ private struct NavigationCheckpointActionModifier<T>: ViewModifier {
279281
content
280282
.onReceive(navigator.state.publisher) { values in
281283
if let _: CheckpointAction = values.consume(checkpoint.identifier) {
282-
navigator.log("Navigator processing checkpoint action: \(checkpoint.name)")
284+
navigator.log(.checkpoint(.returning(checkpoint.name)))
283285
action()
284286
values.resume(.auto)
285287
}
@@ -303,7 +305,7 @@ private struct NavigationCheckpointValueModifier<T: Hashable>: ViewModifier {
303305
content
304306
.onReceive(navigator.state.publisher) { values in
305307
if let value: T = values.consume(checkpoint.identifier) {
306-
navigator.log("Navigator processing checkpoint: \(checkpoint.name) value: \(value)")
308+
navigator.log(.checkpoint(.returningWithValue(checkpoint.name, value)))
307309
completion(value)
308310
values.resume(.auto)
309311
}

Sources/Navigator/Navigator/Core/NavigationConfiguration.swift

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,20 @@ public struct NavigationConfiguration {
4343
/// Allows the developer to log navigation messages to the console or to their own logging system.
4444
///
4545
/// If logger is nil then nothing is logged.
46-
public let logger: ((_ message: String) -> Void)?
46+
public let logger: ((_ event: NavigationEvent) -> Void)?
4747

4848
/// Logging verbosity
49-
public let verbosity: Verbosity
49+
public let verbosity: NavigationEvent.Verbosity
5050

5151
public init(
5252
restorationKey: String? = nil,
53-
logger: ((String) -> Void)? = { print($0) },
53+
logger: ((NavigationEvent) -> Void)? = {
54+
#if DEBUG
55+
print($0)
56+
#endif
57+
},
5458
executionDelay: TimeInterval = 0.3,
55-
verbosity: Verbosity = .warning
59+
verbosity: NavigationEvent.Verbosity = .warning
5660
) {
5761
if #available(iOS 18, *) {
5862
self.executionDelay = min(max(0.3, executionDelay), 5.0)
@@ -64,11 +68,4 @@ public struct NavigationConfiguration {
6468
self.verbosity = verbosity
6569
}
6670

67-
public enum Verbosity: Int {
68-
case info
69-
case warning
70-
case error
71-
case none
72-
}
73-
7471
}

Sources/Navigator/Navigator/Core/NavigationDismiss.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ extension NavigationState {
114114
internal func dismiss() -> Bool {
115115
if isPresented {
116116
triggerDismiss = true
117-
log("Navigator dimsissing: \(id)")
117+
log(.navigation(.dismissed))
118118
return true
119119
}
120120
return false
@@ -123,7 +123,7 @@ extension NavigationState {
123123
/// Returns to the root Navigator and dismisses *any* presented ManagedNavigationStack.
124124
internal func dismissAny() throws -> Bool {
125125
guard !isNavigationLocked else {
126-
log(type: .warning, "Navigator \(id) error navigation locked")
126+
log(.warning("Navigator \(id) error navigation locked"))
127127
throw NavigationError.navigationLocked
128128
}
129129
return root.dismissAnyChildren()
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// NavigationEventLogging.swift
3+
// Navigator
4+
//
5+
// Created by Michael Long on 3/17/25.
6+
//
7+
8+
import Foundation
9+
10+
extension Navigator {
11+
internal func log(_ event: NavigationEvent.Event) {
12+
state.log(event)
13+
}
14+
}
15+
16+
extension NavigationState {
17+
internal func log(_ event: NavigationEvent.Event) {
18+
guard let configuration, let logger = configuration.logger else {
19+
return
20+
}
21+
let verbosity: NavigationEvent.Verbosity
22+
switch event {
23+
case .warning:
24+
verbosity = .warning
25+
case .error:
26+
verbosity = .error
27+
default:
28+
verbosity = .info
29+
}
30+
guard verbosity.rawValue >= configuration.verbosity.rawValue else {
31+
return
32+
}
33+
logger(.init(verbosity: .info, navigator: id, event: event, timestamp: Date()))
34+
}
35+
}
36+
37+
public struct NavigationEvent: CustomStringConvertible {
38+
39+
let verbosity: Verbosity
40+
let navigator: UUID
41+
let event: Event
42+
let timestamp: Date
43+
44+
public var description: String {
45+
"Navigator \(navigator) \(event)"
46+
}
47+
48+
}
49+
50+
extension NavigationEvent {
51+
52+
public enum Verbosity: Int {
53+
case info
54+
case warning
55+
case error
56+
case none
57+
}
58+
59+
}
60+
61+
extension NavigationEvent {
62+
63+
public enum Event: CustomStringConvertible {
64+
65+
case lifecycle(LifecycleEvent)
66+
case navigation(NavigationEvent)
67+
case checkpoint(CheckpointEvent)
68+
case send(SendEvent)
69+
70+
case message(String)
71+
case warning(String)
72+
case error(String)
73+
74+
public var description: String {
75+
switch self {
76+
case .lifecycle(let event):
77+
return "\(event)"
78+
case .navigation(let event):
79+
return "\(event)"
80+
case .checkpoint(let event):
81+
return "checkpoint \(event)"
82+
case .send(let event):
83+
return "\(event)"
84+
case .message(let message):
85+
return message
86+
case .warning(let message):
87+
return message
88+
case .error(let message):
89+
return message
90+
}
91+
}
92+
93+
public enum LifecycleEvent {
94+
case configured
95+
case intialized
96+
case adding(UUID)
97+
case removing(UUID)
98+
case `deinit`
99+
}
100+
101+
public enum NavigationEvent {
102+
case presenting(any NavigationDestination)
103+
case pushing(any Hashable)
104+
case popping
105+
case dismissed
106+
}
107+
108+
public enum SendEvent {
109+
case performing(any Hashable)
110+
case sending(any Hashable)
111+
case receiving(any Hashable)
112+
}
113+
114+
public enum CheckpointEvent {
115+
case adding(String)
116+
case removing(String)
117+
case returning(String)
118+
case returningWithValue(String, Any)
119+
}
120+
121+
}
122+
123+
}

Sources/Navigator/Navigator/Core/NavigationOperations.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ extension Navigator {
3030
/// ```
3131
@MainActor
3232
public func navigate<D: NavigationDestination>(to destination: D, method: NavigationMethod) {
33-
log("Navigator \(id) navigating to: \(String(reflecting: destination)), via: \(method)")
3433
switch method {
3534
case .push:
3635
push(destination)
@@ -40,10 +39,12 @@ extension Navigator {
4039

4140
case .sheet, .managedSheet:
4241
guard state.sheet?.id != destination.id else { return }
42+
log(.navigation(.presenting(destination)))
4343
state.sheet = AnyNavigationDestination(wrapped: destination, method: method)
4444

4545
case .cover, .managedCover:
4646
guard state.cover?.id != destination.id else { return }
47+
log(.navigation(.presenting(destination)))
4748
#if os(iOS)
4849
state.cover = AnyNavigationDestination(wrapped: destination, method: method)
4950
#else
@@ -66,6 +67,7 @@ extension Navigator {
6667
/// Also supports plain Hashable values for better integration with existing code bases.
6768
@MainActor
6869
public func push<D: Hashable>(_ destination: D) {
70+
log(.navigation(.pushing(destination)))
6971
if let destination = destination as? any Hashable & Codable {
7072
state.path.append(destination) // ensures NavigationPath knows type is Codable
7173
} else {
@@ -77,7 +79,8 @@ extension Navigator {
7779
@MainActor
7880
@discardableResult
7981
public func pop(to position: Int) -> Bool {
80-
state.pop(to: position)
82+
log(.navigation(.popping))
83+
return state.pop(to: position)
8184
}
8285

8386
/// Pops the specified number of the items from the end of a stack's navigation path.
@@ -92,6 +95,7 @@ extension Navigator {
9295
@discardableResult
9396
public func pop(last k: Int = 1) -> Bool {
9497
if state.path.count >= k {
98+
log(.navigation(.popping))
9599
state.path.removeLast(k)
96100
return true
97101
}
@@ -186,7 +190,7 @@ extension NavigationState {
186190

187191
internal func popAny() throws -> Bool {
188192
guard !isNavigationLocked else {
189-
log(type: .warning, "Navigator \(id) error navigation locked")
193+
log(.warning("Navigator \(id) error navigation locked"))
190194
throw NavigationError.navigationLocked
191195
}
192196
return root.recursivePopAny()

Sources/Navigator/Navigator/Core/NavigationSend.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ extension Navigator {
7474
}
7575
let remainingValues = Array(values.dropFirst())
7676
if let action = value as? NavigationAction {
77-
log("Navigator \(id) executing action \(action.name)")
77+
log(.send(.performing(action)))
7878
resume(action(self), values: remainingValues)
7979
} else {
80-
log("Navigator \(id) sending \(value)")
80+
log(.send(.sending(value)))
8181
state.publisher.send(NavigationSendValues(navigator: root, value: value, values: remainingValues))
8282
}
8383
}
@@ -88,11 +88,9 @@ extension Navigator {
8888
case .auto:
8989
let delay: TimeInterval = delay ?? state.executionDelay
9090
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
91-
// log("Navigator \(id) delay: \(delay)")
9291
self.send(values: values)
9392
}
9493
case .immediately:
95-
// log("Navigator \(id) immediate send")
9694
send(values: values)
9795
case .after(let interval):
9896
resume(.auto, values: values, delay: interval)
@@ -290,7 +288,7 @@ private struct OnNavigationReceiveModifier<T: Hashable>: ViewModifier {
290288
content
291289
.onReceive(navigator.state.publisher) { values in
292290
if let value: T = values.consume() {
293-
navigator.log("Navigator \(navigator.id) receiving \(value)")
291+
navigator.log(.send(.receiving(value)))
294292
let type = handler(value, navigator)
295293
if case .auto = type, let destination = value as? any NavigationDestination {
296294
values.resume(destination.receiveResumeType)
@@ -330,15 +328,15 @@ internal final class NavigationSendValues {
330328

331329
deinit {
332330
if consumed == false {
333-
navigator.log("Navigator missing receive handler for type: \(type(of: value))!!!")
331+
navigator.log(.error("missing receive handler for type: \(type(of: value))!!!"))
334332
}
335333
}
336334

337335
@MainActor
338336
internal func consume<T>(_ identifier: String? = nil) -> T? {
339337
if let value = value as? T, self.identifier == identifier {
340338
if consumed {
341-
navigator.log("Navigator additional receive handlers ignored for type: \(type(of: value))!!!")
339+
navigator.log(.error("additional receive handlers ignored for type: \(type(of: value))!!!"))
342340
return nil
343341
}
344342
consumed.toggle()

0 commit comments

Comments
 (0)