-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathindex.html
638 lines (538 loc) · 33.1 KB
/
index.html
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
<!DOCTYPE html>
<html lang="en">
<head>
<title>Clue Reference</title>
<link rel="stylesheet" type="text/css" href="css/jazzy.css" />
<link rel="stylesheet" type="text/css" href="css/highlight.css" />
<meta charset='utf-8'>
<script src="js/jquery.min.js" defer></script>
<script src="js/jazzy.js" defer></script>
</head>
<body>
<a title="Clue Reference"></a>
<header>
<div class="content-wrapper">
<p><a href="index.html">Clue Docs</a> (100% documented)</p>
</div>
</header>
<div class="content-wrapper">
<p id="breadcrumbs">
<a href="index.html">Clue Reference</a>
<img id="carat" src="img/carat.png" />
Clue Reference
</p>
</div>
<div class="content-wrapper">
<nav class="sidebar">
<ul class="nav-groups">
<li class="nav-group-name">
<a href="Categories.html">Categories</a>
<ul class="nav-group-tasks">
<li class="nav-group-task">
<a href="Categories/NSError(CLUNetworkAdditions).html">NSError(CLUNetworkAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories/NSException(CLUExceptionAdditions).html">NSException(CLUExceptionAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories/NSHTTPURLResponse(CLUNetworkAdditions).html">NSHTTPURLResponse(CLUNetworkAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories/NSURLRequest(CLUNetworkAdditions).html">NSURLRequest(CLUNetworkAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories/NSURLResponse(CLUNetworkAdditions).html">NSURLResponse(CLUNetworkAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories.html#/c:objc(cy)UIImageView@CLUViewRecordableAdditions">UIImageView(CLUViewRecordableAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories.html#/c:objc(cy)UILabel@CLUViewRecordableAdditions">UILabel(CLUViewRecordableAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories.html#/c:objc(cy)UITextField@CLUViewRecordableAdditions">UITextField(CLUViewRecordableAdditions)</a>
</li>
<li class="nav-group-task">
<a href="Categories/UIView(CLUViewRecordableAdditions).html">UIView(CLUViewRecordableAdditions)</a>
</li>
</ul>
</li>
<li class="nav-group-name">
<a href="Classes.html">Classes</a>
<ul class="nav-group-tasks">
<li class="nav-group-task">
<a href="Classes/CLUDataWriter.html">CLUDataWriter</a>
</li>
<li class="nav-group-task">
<a href="Classes.html#/c:objc(cs)CLUDeviceInfoModule">CLUDeviceInfoModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUExceptionInfoModule.html">CLUExceptionInfoModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUGeneralGestureRecognizer.html">CLUGeneralGestureRecognizer</a>
</li>
<li class="nav-group-task">
<a href="Classes.html#/c:objc(cs)CLUMailDelegate">CLUMailDelegate</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUMailHelper.html">CLUMailHelper</a>
</li>
<li class="nav-group-task">
<a href="Classes.html#/c:objc(cs)CLUNetworkModule">CLUNetworkModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUObserveModule.html">CLUObserveModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUOptions.html">CLUOptions</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLURecordIndicatorView.html">CLURecordIndicatorView</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLURecordIndicatorViewManager.html">CLURecordIndicatorViewManager</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUReportComposer.html">CLUReportComposer</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUReportFileManager.html">CLUReportFileManager</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUTouch.html">CLUTouch</a>
</li>
<li class="nav-group-task">
<a href="Classes.html#/c:objc(cs)CLUURLProtocol">CLUURLProtocol</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUURLProtocolConfiguration.html">CLUURLProtocolConfiguration</a>
</li>
<li class="nav-group-task">
<a href="Classes.html#/c:objc(cs)CLUUserInteractionModule">CLUUserInteractionModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUVideoModule.html">CLUVideoModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/CLUVideoWriter.html">CLUVideoWriter</a>
</li>
<li class="nav-group-task">
<a href="Classes.html#/c:objc(cs)CLUViewStructureModule">CLUViewStructureModule</a>
</li>
<li class="nav-group-task">
<a href="Classes/ClueController.html">ClueController</a>
</li>
</ul>
</li>
<li class="nav-group-name">
<a href="Protocols.html">Protocols</a>
<ul class="nav-group-tasks">
<li class="nav-group-task">
<a href="Protocols/CLUInfoModule.html">CLUInfoModule</a>
</li>
<li class="nav-group-task">
<a href="Protocols/CLUInteractionObserverDelegate.html">CLUInteractionObserverDelegate</a>
</li>
<li class="nav-group-task">
<a href="Protocols/CLUNetworkObserverDelegate.html">CLUNetworkObserverDelegate</a>
</li>
<li class="nav-group-task">
<a href="Protocols/CLURecordableModule.html">CLURecordableModule</a>
</li>
<li class="nav-group-task">
<a href="Protocols/CLUViewRecordableProperties.html">CLUViewRecordableProperties</a>
</li>
<li class="nav-group-task">
<a href="Protocols/CLUWritable.html">CLUWritable</a>
</li>
</ul>
</li>
</ul>
</nav>
<article class="main-content">
<section>
<section class="section">
<p align="center">
<img src="https://github.com/Geek-1001/Clue/raw/master/Images/clue-logo-with-text.png" width="300" max-width="50%" alt="Clue" />
</p>
<p align="center">
<a href="https://travis-ci.org/Geek-1001/Clue/branches">
<img src="https://img.shields.io/travis/Geek-1001/Clue/master.svg" alt="Travis status" />
</a>
<a href="https://cocoapods.org/pods/Clue">
<img src="https://img.shields.io/cocoapods/p/Clue.svg" alt="CocoaPods platforms"/>
</a>
<a href="https://cocoapods.org/pods/Clue">
<img src="https://img.shields.io/cocoapods/v/Clue.svg" alt="CocoaPods version" />
</a>
<a href="https://twitter.com/ahmed_sulajman">
<img src="https://img.shields.io/badge/contact-%40ahmed__sulajman-orange.svg?style=flat" alt="Twitter: @ahmed__sulajman" />
</a>
</p>
<p><strong>Clue</strong> is a simple smart-bug report framework for iOS, which allow your users to record full bug/crash report and send it to you as a single .clue file via email. </p>
<p>(which includes full video of the screen, views structure, all network operations and user interactions during recording)
<br></p>
<a href='#why' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='why'>🕵️ Why</h2>
<p>As soon as you get new bug report from your users - you’re not software engineer anymore but a true detective. Trying to get a clue why something went wrong in your app. Especially it’s true if you’re working in company where you need to talk to users directly.</p>
<p>I believe it’s a problem. What if you can fix bug/crash without trying to reproduce it. What if you can see all required information and exact cause of the problem right away without wasting your time figuring it out.
<br></p>
<a href='#description' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='description'>ℹ️ Description</h2>
<p align="center">
<img src="https://github.com/Geek-1001/Clue/raw/master/Images/clue-framework-demo.gif" alt="Clue demo" />
</p>
<p>Clue.framework records all required information so you’ll be able to fix bug or crash really fast. Just import and setup Clue.framework in your iOS application (Xcode project) and you’d be able to shake the device (or simulator) to start bug report recording. During that recording you can do whatever you want to reproduce the bug.</p>
<p>After you’re done you need to shake the device (or simulator) once again (or tap on recording indicator view) and Clue will save the .clue file with the following information:
* Device Information
* All network operations during recording
* All views structure changes (including view’s properties and subviews)
* All user touches and interactions
* Screen record video</p>
<p>Next Clue will open system mail window with your email (unfortunately on device only, simulator doesn’t support system mail client) — so the user can send .clue report file right to your inbox.<br>
<br></p>
<a href='#how-to-install' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='how-to-install'>📲 How to install</h2>
<a href='#manually' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='manually'>Manually</h3>
<p>If you prefer not to use dependency managers, you can integrate Clue into your project manually.
1. Clone the repo <code>git clone git@github.com:Geek-1001/Clue.git</code>
2. Open <strong>Clue.xcodeproj</strong> with Xcode
3. Choose <code>Clue</code> build schema and build it with Xcode
4. Drag and Drop <strong>Clue.framework</strong> file from Product folder right into your Xcode project
5. Make sure to select <q>Copy item if needed</q> in the dialog
6. Go to Project settings > Build Phases
7. Expand <q>Copy Files</q> section, choose <q>Frameworks</q> in the Destination dropdown
8. Click on the plus icon and add Clue.framework</p>
<a href='#using-cocoapods' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='using-cocoapods'>Using CocoaPods</h3>
<ol>
<li>To integrate Clue into your Xcode project using CocoaPods, add following line to your Podfile : <code>pod 'Clue', '~> 0.1.0'</code></li>
<li>Then, run install command: <code>$ pod install</code>
<br></li>
</ol>
<a href='#how-to-use' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='how-to-use'>💻 How to use</h2>
<a href='#basic-usage' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='basic-usage'>Basic usage</h3>
<ul>
<li>Import Clue framework wherever you need it</li>
</ul>
<p>Objective-C :
<code>objective-c
@import Clue
</code>
Swift :
<code>swift
import Clue
</code></p>
<ul>
<li>To setup Clue you need to enable it with launch configuration in your AppDelegate.</li>
</ul>
<p>Objective-C :
<code>objective-c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[ClueController sharedInstance] enableWithOptions:[CLUOptions optionsWithEmail:@"ahmed.sulajman@gmail.com"]];
return YES;
}
</code>
Swift :
<code>swift
optional func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
ClueController.sharedInstance().enable(with: CLUOptions(email: "ahmed.sulajman@gmail.com"))
return true
}
</code></p>
<ul>
<li>Then you should handle shake gesture in <code>AppDelegate</code> (if you want to record bug reports from anywhere in the app on iOS 8, 9, 10) or in specific <code>UIViewController</code> (since <code>motionBegan</code> method doesn’t work in <code>AppDelegate</code> starting from iOS 10).</li>
</ul>
<p>Objective-C :
<code>objective-c
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
[[ClueController sharedInstance] handleShake:motion];
}
</code>
Swift :
<code>swift
func motionBegan(_ motion: UIEventSubtype, with event: UIEvent?) {
ClueController.sharedInstance().handleShake(motion)
}
</code></p>
<p>After that you can shake your device and start recording. Shake it again to stop it.</p>
<a href='#additional-abilities' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='additional-abilities'>Additional abilities</h3>
<ul>
<li>If you want to use your own custom UI element to enable report recording you can call <code>startRecording()</code> and <code>stopRecording()</code> methods from <code>ClueController</code> directly like in the example below.</li>
</ul>
<p>Objective-C :
<code>objective-c
[[ClueController sharedInstance] startRecording];
...
[[ClueController sharedInstance] stopRecording];
</code>
Swift :
<code>swift
ClueController.sharedInstance().startRecording()
...
ClueController.sharedInstance().stopRecording()
</code></p>
<ul>
<li>To disable ability to record bug reports just call <code>disable()</code> method from <code>ClueController</code>.</li>
</ul>
<p>Objective-C :
<code>objective-c
[[ClueController sharedInstance] disable];
</code>
Swift :
<code>swift
ClueController.sharedInstance().disable()
</code>
<br></p>
<a href='#platforms-support' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='platforms-support'>✅ Platforms Support</h2>
<p>Clue supports iOS 9+ devices.
The current version of Clue (and the <code>master</code> branch) is compatible with Xcode 8
<br></p>
<a href='#hackable' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='hackable'>🛠️ Hackable</h2>
<p>Love this part. Clue designed in a way that you can tweak whatever you want and add more functionalities to report recording process (and I encourage you to do so) so it will fit into your custom needs. For example you want to track your custom logs during recording. Just create a separate module for that and plug it to recording process (I’ll explain this in details below)</p>
<p>Unfortunately Clue built with Objective-C (at least for the first iteration. I’m considering to rewrite some parts to Swift, for sure) so all internals wasn’t design to be Swift compatible. (If you can fix it — contributions are welcome!)</p>
<a href='#basic-architecture-and-structures' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='basic-architecture-and-structures'>Basic architecture and structures:</h3>
<p align="center">
<img src="https://github.com/Geek-1001/Clue/raw/master/Images/clue_structure.png" alt="Clue structure" />
</p>
<p>There are two main concepts in Clue which you need to understand in order to build it by yourself:
* Modules
* Recordable modules
* Info modules
* Writers</p>
<p><strong>Modules</strong> are responsible for actual data handling. Those classes observe some specific data during recording or collect this data just once during record launch. There are two module types: <code>CLURecordableModule</code> and <code>CLUInfoModule</code>.</p>
<p><code>CLURecordableModule</code> — is a protocol. It describes recordable module (like Video, View Structure, Network modules etc) which needs to track or inspect some specific information over time (like view structure for example) and record this information with specific timestamp using <strong>Writers</strong></p>
<blockquote>
<p>Note : Every recordable modules have to implement this protocol to be able to work normally inside the system </p>
</blockquote>
<p>If your Recordable Module needs to observe new data instead of writing new data with every new frame you should use (subclass your new module from) <code>CLUObserveModule</code> which provide common interface to write new data as soon as data become available.</p>
<hr>
<p><code>CLUInfoModule</code> — is a protocol. It describes info modules (like Device Info module), static one-time modules which needs to write their data only once during whole recording also using <strong>Writers</strong></p>
<blockquote>
<p>Note : Every info modules have to implement this protocol to be able to work normally inside the system </p>
</blockquote>
<p><strong>Writers</strong> are responsible for writing some specific content (video, text etc) into the file.</p>
<p><code>CLUWritable</code> — is a protocol. It describes writers (like <code>CLUDataWriter</code> or <code>CLUVideoWriter</code>) which needs to actually write new data to specific file (could be text file, video file etc.)</p>
<a href='#new-recordable-module-example' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='new-recordable-module-example'>New Recordable Module Example</h3>
<p>Let’s assume you want to add module which will intercept logs and write them into json file with specific timestamp inside .clue report file.
First of you need to create new module class and subclass it from <code>CLUObserveModule</code> (we’re assuming here that we need to observe new logs and they are not available right away)</p>
<pre class="highlight plaintext"><code>@interface CLULogsModule : CLUObserveModule
@end
</code></pre>
<p>Next we need to implement some methods from <code>CLURecordableModule</code> protocol:</p>
<pre class="highlight plaintext"><code>@implementation CLULogsModule
- (instancetype)initWithWriter:(id <CLUWritable>)writer {
// If you will write just text data into json file you can use CLUDataWriter as an argument for this method. CLUDataWriter implements CLUWritable protocol
}
- (void)startRecording {
if (!self.isRecording) {
[super startRecording];
// Start logs interception
}
}
- (void)stopRecording {
if (self.isRecording) {
[super stopRecording];
// Stop logs interception
}
}
// This method will be called every time new frame with new timestamp is available.
// Since you subclass your module from CLUObserveModule you don't need to implement this method. CLUObserveModule takes care about it with data buffer.
// - (void)addNewFrameWithTimestamp:(CFTimeInterval)timestamp { }
@end
</code></pre>
<p>Next let’s assume we have some kind of log delegate method which will be called every time new log record is available. In this delegate method you should compose <code>NSDictionary</code> (if you want to write json) with all properties of this log data entity you want to record into final .clue report.</p>
<pre class="highlight plaintext"><code>- (void)newLog:(NSString *)logText isAvailableWithDate:(NSDate *)date {
// Let's build NSDictionary with data we need
// You always should include timestamp property for every new data entry
NSDictionary *logDictionary =
@{@"text" : logText,
@"date" : date,
TIMESTAMP_KEY : self.currentTimeStamp}; // TIMESTAMP_KEY - is a #define with default name for timetamp declared in CLUObserveModule. self.currentTimeStamp – is a property declared in CLUObserveModule which is updates every [CLUObserveModule addNewFrameWithTimestamp:] method call. So you can use it to indicate current timestamp
// Now we need to check validity of NSDictionary so we can convert it in to NSData as a json
if ([NSJSONSerialization isValidJSONObject:touchDictionary]) {
NSError *error;
// Convert NSDictionary to NSData
NSData *logData = [NSJSONSerialization dataWithJSONObject:logDictionary options:0 error:&error];
// Add log data to data buffer from CLUObserveModule with [CLUObserveModule addData:]
[self addData:logData];
}
}
</code></pre>
<p>To make it work you also need to plug your new module into the system of modules. The <code>CLUReportComposer</code> is exactly for this purposes.</p>
<blockquote>
<p><code>CLUReportComposer</code> is a class responsible for composing final Clue report from many modules. This class initialize all recordable and info modules and actually start recording. Also this class is calling <code>addNewFrameWithTimestamp:</code> method for every recordable module during recording and <code>recordInfoData</code> method from <code>CLURecordableModule</code> protocol for every info module only once at record launch. </p>
</blockquote>
<p>So in <code>ClueController</code> you need to add your new module in this method</p>
<pre class="highlight plaintext"><code>- (NSMutableArray *)configureRecordableModules {
CLUVideoModule *videoModul = [self configureVideoModule];
CLUViewStructureModule *viewStructureModule = [self configureViewStructureModule];
CLUUserInteractionModule *userInteractionModule = [self configureUserInteractionModule];
CLUNetworkModule *networkModule = [self configureNetworkModule];
// Initialize your module just like other modules above
CLULogsModule *logModule = [self configureLogsModule]
NSMutableArray *modulesArray = [[NSMutableArray alloc] initWithObjects:videoModul, viewStructureModule, userInteractionModule, networkModule, /* HERE goes your module, */ nil];
return modulesArray;
}
</code></pre>
<p>Here is configuration method example:</p>
<pre class="highlight plaintext"><code>- (void)configureLogsModule {
// Get directory URL for all recordable modules inside .clue file
NSURL *recordableModulesDirectory = [[CLUReportFileManager sharedManager] recordableModulesDirectoryURL];
// Specify URL for output json file
NSURL *outputURL = [recordableModulesDirectory URLByAppendingPathComponent:@"module_logs.json"];
// Initialize CLUDataWriter with specific output file URL
CLUDataWriter *dataWriter = [[CLUDataWriter alloc] initWithOutputURL:outputURL];
// Initialize CLULogsModule with specific Writer
CLULogsModule *logsModule = [[CLULogsModule alloc] initWithWriter:dataWriter];
return logsModule;
}
</code></pre>
<p>That’s it. Now you have logs inside your .clue report file as well as other useful data for fast bug fixing.
You can literally build any module you want which can record any possible data inside your .clue bug report file.</p>
<a href='#new-info-module-example' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='new-info-module-example'>New Info Module Example</h3>
<p>Now let’s assume you want to add info module which will collect some initial data at the beginning of report recording for current users’s location and write in into json file inside .clue report file.
First of you need to create new info module class and declare <code>CLUInfoModule</code> protocol in the header file.</p>
<blockquote>
<p><code>CLUInfoModule</code> protocol describe info modules (like Device Info module or Exception module), static one-time modules which needs to write their data only once during recording. Every info modules have to implement this protocol to be able to work normally inside the system</p>
</blockquote>
<pre class="highlight plaintext"><code>@interface CLULocationInfoModule : NSObject <CLUInfoModule>
@end
</code></pre>
<p>Next we need to implement some methods from <code>CLUInfoModule</code> protocol:</p>
<pre class="highlight plaintext"><code>@interface CLULocationInfoModule()
@property (nonatomic) CLUDataWriter *writer; // Just keep reference to Writer object from initialization
@end
@implementation CLULocationInfoModule
// If you will write just text data into json file you can use CLUDataWriter as an argument for this method. CLUDataWriter implements CLUWritable protocol so you can write any NSData object into output file
- (instancetype)initWithWriter:(id <CLUWritable>)writer {
// Basic initialization stuff
_writer = writer;
return self;
}
// This method will be called once at recording startup. Here you can add all your required data (in our case user's location) into report file
- (void)recordInfoData {
NSData *locationData = [self retrieveLocationData];
// Add location data to the output file via Writer object
[_writer startWriting];
[_writer addData:locationData];
[_writer finishWriting];
}
@end
</code></pre>
<p>To make it work you also need to plug your new info module into the system of modules (just like recordable module) using <code>CLUReportComposer</code>.</p>
<blockquote>
<p><code>CLUReportComposer</code> is a class responsible for composing final Clue report from many modules. This class initialize all recordable and info modules and actually start recording. Also this class is calling <code>addNewFrameWithTimestamp:</code> method for every recordable module during recording and <code>recordInfoData</code> method from <code>CLURecordableModule</code> protocol for every info module only once at record launch. </p>
</blockquote>
<p>So in <code>ClueController</code> you need to add your new info module in this method</p>
<pre class="highlight plaintext"><code>- (NSMutableArray *)configureInfoModules {
CLUDeviceInfoModule *deviceModule = [self configureDeviceInfoModule];
// Initialize your info module just like other info modules above
CLULocationInfoModule *locationModule = [self configureLocationModule];
NSMutableArray *modulesArray = [[NSMutableArray alloc] initWithObjects:deviceModule, /* HERE goes your info module ,*/ nil];
return modulesArray;
}
</code></pre>
<p>Here is configuration method example:</p>
<pre class="highlight plaintext"><code>- (void)configureLocationModule {
// Get directory URL for all info modules inside .clue file
NSURL *infoModulesDirectory = [[CLUReportFileManager sharedManager] infoModulesDirectoryURL];
// Specify URL for output json file
NSURL *outputURL = [infoModulesDirectory URLByAppendingPathComponent:@"info_location.json"];
// Initialize CLUDataWriter with specific output file URL
CLUDataWriter *dataWriter = [[CLUDataWriter alloc] initWithOutputURL:outputURL];
// Initialize CLULocationInfoModule with specific Writer
CLULocationInfoModule *locationModule = [[CLULogsModule alloc] initWithWriter:dataWriter];
return locationModule;
}
</code></pre>
<p>That’s it. Now you have location information inside your .clue report file.</p>
<a href='#how-to-add-custom-view-object-parsing' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='how-to-add-custom-view-object-parsing'>How to add custom View object parsing</h3>
<p>Clue records views’ structure. This means that Clue needs to parse all properties and subviews for every visible View (and invisible as well) on the screen to be able to represent whole views’ structure at the current time.</p>
<p>UIView categories are used for this purposes. There are following categories for UIView related classes:
* <code>UIView (CLUViewRecordableAdditions)</code>
* <code>UILabel (CLUViewRecordableAdditions)</code>
* <code>UIImageView (CLUViewRecordableAdditions)</code>
* <code>UITextField (CLUViewRecordableAdditions)</code></p>
<p><strong>UIView (CLUViewRecordableAdditions)</strong> category has methods to parse basic view properties (this is general for every view object) and recursively parse subviews.</p>
<p>Other categories made to parse View specific properties (like <code>text</code> property for <code>UITextField</code>, for example).
If you want to parse some specific view properties and add them to final report you have to create separate category like this</p>
<pre class="highlight plaintext"><code>@interface AHCustomView (CLUViewRecordableAdditions) <CLUViewRecordableProperties>
@end
</code></pre>
<blockquote>
<p>Every new View category always have to implement method from <code>CLUViewRecordableProperties</code> protocol to be able to use root properties from base view class (<code>UIView</code>, for example) so all properties would be in place for your custom view object as well. </p>
</blockquote>
<p>Now we need to implement actual property parsing</p>
<pre class="highlight plaintext"><code>// Method from CLUViewRecordableProperties protocol
- (NSMutableDictionary *)clue_viewPropertiesDictionary {
// First of you need to get property dictionary from view's superclass
NSMutableDictionary *rootDictionary = [super clue_viewPropertiesDictionary];
// Change class name, so it would be real instead of superclass' class name
[rootDictionary setObject:NSStringFromClass([self class]) forKey:@"class"];
// Next you want to add some view specific properties into root dictionary.
NSMutableDictionary *propertiesDictionary = [rootDictionary objectForKey:@"properties"];
// Let's add text property just for example
[propertiesDictionary setObject:self.text ?: @""
forKey:@"text"];
…
// Add new properties dictionary to root dictionary
[rootDictionary setObject:propertiesDictionary
forKey:@"properties"];
// Return root dictionary as a result
return rootDictionary;
}
</code></pre>
<p>That’s it, now even your custom view classes will show specific properties in final .clue report.</p>
<a href='#what-is-clue-file' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h3 id='what-is-clue-file'>What is .clue file</h3>
<p>It’s basically just a package file with json data from network, view structure, user interactions modules and device info module and .mp4 video file from video module.</p>
<p>Here is tree representation of <code>Report.clue</code> file:
<code>
Report.clue
├── Info
│ └── info_device.json
└── Modules
├── module_interaction.json
├── module_network.json
├── module_video.mp4
└── module_view.json
</code>
<br></p>
<a href='#macos-companion-application' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='macos-companion-application'>🖥️ macOS Companion Application</h2>
<p align="center">
<img src="https://github.com/Geek-1001/Clue/raw/master/Images/clue-macOS-app.png" alt="Clue macOS app" />
</p>
<p>I also made <a href="https://github.com/Geek-1001/Clue-macOS-application">macOS companion app</a> (open source as well) which allow you to view .clue report files in a nice and simple way. It shows full timeline with all event so you can inspect dependencies between user actions and actual app’s responses pretty easily.</p>
<p>Obviously you can view .clue report files with whatever method you want since it’s just json files and mp4 video file combined inside single entity.</p>
<a href='#roadmap' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='roadmap'>🛣️ Roadmap</h2>
<ul>
<li>[x] Build basic modules for Network, View Structure, User Interactions and Video</li>
<li>[x] Send final report file via email</li>
<li>[ ] Skip recordable property if this property is invalid</li>
<li>[ ] Integrate Nonnull and Nullable annotations</li>
<li>[ ] Migrate some parts of the framework to Swift</li>
<li>[ ] Add more useful recordable/info modules</li>
<li>[ ] Slack integration</li>
<li>[ ] macOS version of the framework to use in macOS apps</li>
<li>[ ] Suggestions are welcome!
<br></li>
</ul>
<a href='#contribution' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='contribution'>👩💻👨💻 Contribution</h2>
<p>Contributions are welcome! That’s why Open Source is cool!
Also <strong>Contributions Guide</strong> will be ready soon, but you can create an issue with your improvements or suggestion right now and we’ll discuss it.
<br></p>
<a href='#contacts' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='contacts'>☎️ Contacts</h2>
<p>Feel free to reach me on twitter <a href="https://twitter.com/ahmed_sulajman">@ahmed_sulajman</a> or drop me a line on <a href="mailto:ahmed.sulajman@gmail.com">ahmed.sulajman@gmail.com</a>
I Hope Clue framework will help you with your bug reports!
<br></p>
<a href='#licence' class='anchor' aria-hidden=true><span class="header-anchor"></span></a><h2 id='licence'>📄 Licence</h2>
<p>Clue is released under the MIT License. See the LICENSE file.</p>
</section>
</section>
<section id="footer">
<p>© 2017 <a class="link" href="https://github.com/Geek-1001/Clue" target="_blank" rel="external">Ahmed Sulaiman</a>. All rights reserved. (Last updated: 2017-04-14)</p>
<p>Generated by <a class="link" href="https://github.com/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.7.4</a>, a <a class="link" href="http://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
</div>
</body>
</div>
</html>