-
Notifications
You must be signed in to change notification settings - Fork 4
/
TraceUI.swift
202 lines (169 loc) · 8 KB
/
TraceUI.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//
// TraceUI.swift
// Tracer
//
// Created by Rob Phillips on 5/8/18.
// Copyright © 2018 Keepsafe Inc. All rights reserved.
//
import UIKit
/// Coordinator for the Trace UI
public final class TraceUI: Listening {
// MARK: - Instantiation
/// Starts the logger and listens for signals
///
/// Note: UI will be configured the first time `show` is called
public init() {
logger = ItemLogger()
start()
}
// MARK: - API
/// Shows the TraceUI tool as a standalone window that floats over top of the application window
public func show() {
_ = setupView
TraceUISignals.UI.show.fire(data: nil)
}
/// Hides the TraceUI tool from the view
public func hide() {
TraceUISignals.UI.hide.fire(data: nil)
}
/// Adds the traces to the list of executable traces
///
/// Traces should only be started using the UI
/// (or manually if using them in other ways outside of this UI tool)
public func add(traces: [Traceable]) {
TraceUISignals.Traces.added.fire(data: traces)
}
/// Optionally add extra settings to the settings action sheet
///
/// - Parameter settings: An array of settings to add
///
/// Note: this is a no-op if a setting with that title already exists
public func add(settings: [UIAlertAction]) {
TraceUISignals.UI.settingsAdded.fire(data: settings)
}
/// Logs the given `TraceItem` to a trace, if running, and also logs it to the generic log
///
/// - Parameters:
/// - traceItem: The `TraceItem` to log
/// - verboseLog: An optional item to log verbosely along with the trace item (not used for comparison, just for display)
/// - emojiToPrepend: An optional emoji to prepend to the generic log entry, defaulting to ⚡️
/// (i.e. the emoji does not affect trace item values)
///
/// Note: If no trace is running, this will just log to the generic log and be a no-op
public func log(traceItem: TraceItem, verboseLog: AnyTraceEquatable? = nil, emojiToPrepend: String? = "⚡️") {
TraceUISignals.Traces.itemLogged.fire(data: traceItem)
var properties = ["isTraceItem": AnyTraceEquatable(true),
"type": AnyTraceEquatable(traceItem.type)]
if let verboseLog = verboseLog {
properties["verbose"] = verboseLog
}
if let uxFlowHint = traceItem.uxFlowHint {
properties["uxFlowHint"] = AnyTraceEquatable(uxFlowHint)
}
log(genericItem: traceItem.itemToMatch, properties: properties, emojiToPrepend: emojiToPrepend)
}
/// Logs a verbose `TraceItem` to a trace, if running, and also logs it to the generic log as a
/// properties type display with a smaller font.
///
/// This is a convenience helper for logging items like dictionaries that may be very verbose
/// and be more readable when displayed as a properties type.
///
/// - Parameters:
/// - traceItem: The `TraceItem` to log
/// - emojiToPrepend: An optional emoji to prepend to the generic log entry, defaulting to ⚡️
/// (i.e. the emoji does not affect trace item values)
///
/// Note: If no trace is running, this will just log to the generic log and be a no-op
public func logVerbose(traceItem: TraceItem, emojiToPrepend: String? = "⚡️") {
TraceUISignals.Traces.itemLogged.fire(data: traceItem)
var properties = ["isTraceItem": AnyTraceEquatable(true),
"traceItem": traceItem.itemToMatch]
if let uxFlowHint = traceItem.uxFlowHint {
properties["uxFlowHint"] = AnyTraceEquatable(uxFlowHint)
}
log(genericItem: AnyTraceEquatable(traceItem.type), properties: properties, emojiToPrepend: emojiToPrepend)
}
/// Logs an item to the in-memory tailing log
///
/// i.e. this is a generic logger used throughout the entire app session
/// regardless of whether or not a trace is running
///
/// - Parameters:
/// - genericItem: The `AnyTraceEquatable` item to log
/// - properties: An optional dictionary of properties (i.e. `LoggedItemProperties`) to log along with this item
/// - emojiToPrepend: An optional emoji to prepend to the generic log entry, defaulting to ⚡️
///
/// Note: this isn't used for traces because trace items require a `type` to differentiate
/// the items from each other (e.g. ["type": "userId"] with a value of "1" is
/// different than ["type": "age"] with a value of "1")
public func log(genericItem: AnyTraceEquatable, properties: LoggedItemProperties? = nil, emojiToPrepend: String? = "⚡️") {
let loggedItem: LoggedItem
if let emojiToPrepend = emojiToPrepend {
loggedItem = LoggedItem(item: AnyTraceEquatable("\(emojiToPrepend) \(genericItem)"), properties: properties)
} else {
loggedItem = LoggedItem(item: genericItem, properties: properties)
}
logger.log(item: loggedItem)
TraceUISignals.Logger.itemLogged.fire(data: loggedItem)
TraceUISignals.Toasts.queue.fire(data: loggedItem)
}
/// Clears the tailing log
public func clearLog() {
logger.clear()
}
// MARK: - Properties
/// Whether or not the UI tool should assert during traces; this defaults to `false` since it often
/// doesn't make sense to assert and abort the app during normal usage, whereas it makes sense for
/// unit tests that don't use the UI tool to throw assertions
public static var canThrowAssertions = false
/// The items logged to the generic tailing log
public var loggedItems: [LoggedItem] {
return logger.loggedItems
}
/// The root view controller; this will be non-nil once `show()` is called for the first time
public var rootViewController: UIViewController? {
return container?.rootViewController
}
// MARK: - Private Properties
private let logger: ItemLogger
private var container: TraceUIContainer?
private var containerPresenter: TraceUIContainerPresenter?
private lazy var setupView: Void = { [unowned self] in
let traceUIContainer = TraceUIContainer()
container = traceUIContainer
containerPresenter = TraceUIContainerPresenter(view: traceUIContainer)
}()
}
// MARK: - Listeners
private extension TraceUI {
func start() {
logger.start()
listenForChanges()
}
func listenForChanges() {
TraceUISignals.UI.clearLog.listen { _ in
self.clearLog()
}
TraceUISignals.UI.traceReportExported.listen { report in
guard let (summaryURL, rawLogURL) = report.exportedAsTextFiles(),
let genericLogURL = ItemLoggerReport(loggedItems: self.loggedItems).exportedAsCSVFile(),
let rootVC = self.container?.rootViewController
else { return }
TraceShareSheet.present(with: [summaryURL, rawLogURL, genericLogURL], in: rootVC, success: {
self.log(genericItem: AnyTraceEquatable("Trace results exported successfully!"))
}, failure: { error in
self.log(genericItem: AnyTraceEquatable("Error while exporting trace results: \(error.localizedDescription)"))
})
}
TraceUISignals.UI.exportLog.listen { _ in
guard let rootVC = self.container?.rootViewController,
let genericLogURL = ItemLoggerReport(loggedItems: self.loggedItems).exportedAsCSVFile()
else { return }
TraceShareSheet.present(with: [genericLogURL], in: rootVC, success: {
self.log(genericItem: AnyTraceEquatable("Logged items exported successfully!"))
}, failure: { error in
self.log(genericItem: AnyTraceEquatable("Error while exporting logged items: \(error.localizedDescription)"))
})
}
}
}