-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathdispatchsource-detecting-changes-in-files-and-folders-in-swift.html
400 lines (340 loc) · 21.4 KB
/
dispatchsource-detecting-changes-in-files-and-folders-in-swift.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
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://use.fontawesome.com/afd448ce82.js"></script>
<!-- Meta Tag -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- SEO -->
<meta name="author" content="Bruno Rocha">
<meta name="keywords" content="Software, Engineering, Blog, Posts, iOS, Xcode, Swift, Articles, Tutorials, OBJ-C, Objective-C, Apple">
<meta name="description" content="The DispatchSource family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use DispatchSource to monitor a log file and create a debug-only view that shows the app's latest logs.">
<meta name="title" content="DispatchSource: Detecting changes in files and folders in Swift">
<meta name="url" content="https://swiftrocks.com/dispatchsource-detecting-changes-in-files-and-folders-in-swift">
<meta name="image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4">
<meta name="copyright" content="Bruno Rocha">
<meta name="robots" content="index,follow">
<meta property="og:title" content="DispatchSource: Detecting changes in files and folders in Swift"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta property="og:description" content="The DispatchSource family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use DispatchSource to monitor a log file and create a debug-only view that shows the app's latest logs."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/dispatchsource-detecting-changes-in-files-and-folders-in-swift"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta name="twitter:image:alt" content="Page Thumbnail"/>
<meta name="twitter:title" content="DispatchSource: Detecting changes in files and folders in Swift"/>
<meta name="twitter:description" content="The DispatchSource family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use DispatchSource to monitor a log file and create a debug-only view that shows the app's latest logs."/>
<meta name="twitter:site" content="@rockbruno_"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="images/favicon/iconsmall2.png" sizes="32x32" />
<link rel="apple-touch-icon" href="images/favicon/iconsmall2.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<!-- Bootstrap CSS Plugins -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css">
<!-- Prism CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/prism4.css">
<!-- Main CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/style48.css">
<link rel="stylesheet" type="text/css" href="css/sponsor4.css">
<!-- HTML5 shiv and Respond.js support IE8 or Older for HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://swiftrocks.com/dispatchsource-detecting-changes-in-files-and-folders-in-swift"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb.jpg"
],
"datePublished": "2020-11-24T14:00:00+02:00",
"dateModified": "2020-11-24T14:00:00+02:00",
"author": {
"@type": "Person",
"name": "Bruno Rocha"
},
"publisher": {
"@type": "Organization",
"name": "SwiftRocks",
"logo": {
"@type": "ImageObject",
"url": "https://swiftrocks.com/images/thumbs/thumb.jpg"
}
},
"headline": "DispatchSource: Detecting changes in files and folders in Swift",
"abstract": "The DispatchSource family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use DispatchSource to monitor a log file and create a debug-only view that shows the app's latest logs."
}
</script>
</head>
<body>
<div id="main">
<!-- Blog Header -->
<!-- Blog Post (Right Sidebar) Start -->
<div class="container">
<div class="col-xs-12">
<div class="page-body">
<div class="row">
<div><a href="https://swiftrocks.com">
<img id="logo" class="logo" alt="SwiftRocks" src="images/bg/logo2light.png">
</a>
<div class="menu-large">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-large">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
<div class="menu-small">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-1">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-2">
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
</div>
<div class="content-page" id="WRITEIT_DYNAMIC_CONTENT">
<!--WRITEIT_POST_NAME=DispatchSource: Detecting changes in files and folders in Swift-->
<!--WRITEIT_POST_HTML_NAME=dispatchsource-detecting-changes-in-files-and-folders-in-swift-->
<!--Add here the additional properties that you want each page to possess.-->
<!--These properties can be used to change content in the template page or in the page itself as shown here.-->
<!--Properties must start with 'WRITEIT_POST'.-->
<!--Writeit provides and injects WRITEIT_POST_NAME and WRITEIT_POST_HTML_NAME by default.-->
<!--WRITEIT_POST_SHORT_DESCRIPTION=The DispatchSource family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use DispatchSource to monitor a log file and create a debug-only view that shows the app's latest logs.-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-11-24T14:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2020-11-24T14:00:00+02:00-->
<title>DispatchSource: Detecting changes in files and folders in Swift</title>
<div class="blog-post">
<div class="post-title-index">
<h1>DispatchSource: Detecting changes in files and folders in Swift</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 24 Nov 2020
</div>
</div>
<div class="post-image">
<img src="https://i.imgur.com/OsWGXW5.png" alt="Debug Log View" style="height: 500px">
</div>
<p>The <code>DispatchSource</code> family of types from GCD contains a series of objects that are capable of monitoring OS-related events. In this article, we'll see how to use <code>DispatchSource</code> to monitor a log file and create a debug-only view that shows the app's latest logs.</p>
<h2>Context: File logging in Swift</h2>
<div class="sponsor-article-ad-auto hidden"></div>
<p>While every app will print debug logs to the developer console, it's good practice to save these logs somewhere. While <code>OSLog</code> automatically saves your logs to the system, I find that maintaining your own log file (like <code>MyApp-2020-11-24T14:23:42.log</code>) is an additional good practice. If your app receives a bug report from an external beta tester, you may find retrieving and inspecting your own log file easier than teaching that user how to extract and send their <code>OSLogs</code>. For example, if you have your own log files, you can add a debug gesture that automatically dumps these logs somewhere.</p>
<p>Regardless of how you generate these logs, you can save them in two main ways. The most common way to write a file is to write all of the contents at once using <code>String.write(to:)</code>:</p>
<pre><code>var logs = ["Logged in!", "Logged out!"]
logs.joined(separator: "\n").write(to: logsPath, atomically: false, encoding: .utf8)</code></pre>
<p>This is fine if you're writing all your logs at once when your app is going to close, but if you plan to continuously add content to a file, you should use <code>FileHandler</code>:</p>
<pre><code>let fileHandler = try FileHandle(forWritingTo: logsPath)
func addToFile(log: String) throws {
fileHandler.seekToEndOfFile() // Move the cursor to the end of the file
fileHandler.write(log.data(using: .utf8)!)
}</code></pre>
<p>In the end, the difference between these two methods is that the first one is overwriting the file, while the second one is more similar to a text editor in terms that you're <i>modifying</i> an existing file.</p>
<h2>Monitoring file changes</h2>
<p>Monitoring changes in the file system is done by attaching a <code>DispatchSource</code> object to the file/folder in question and registering which events we'd like to be notified of. Note though that a <code>DispatchSource</code> is not necessarily restricted to file system events -- they are capable of monitoring many types of OS-related events, which include timers, processes, UNIX signals and more things that are meant to be used in macOS instead of iOS itself.</p>
<p>In this article, however, we're only going to monitor file events. To show how the process works, we are going to detect changes in a log file and display these changes in the app's UI.</p>
<div class="post-image">
<img src="https://i.imgur.com/OsWGXW5.png" alt="Debug Log View" style="height: 500px">
</div>
<p>If you have something akin to an internal employee beta of your app, a feature like this can be very useful. If someone finds a bug, they can open this feature and potentially determine the cause of the issue on the fly without needing a developer to boot Xcode and run an actual debug build.</p>
<p>The first step to monitor file changes is to abstract all of it. Let's start with a new <code>FileMonitor</code> class:</p>
<pre><code>final class FileMonitor {
let url: URL
let fileHandle: FileHandle
let source: DispatchSourceFileSystemObject
init(url: URL) throws {
self.url = url
...
}
}</code></pre>
<p>To create a <code>DispatchSource</code> that monitors the file system, we'll call the <code>makeFileSystemObjectSource</code> factory to get a new <code>DispatchSourceFileSystemObject</code>:</p>
<pre><code>source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: ...,
eventMask: ...,
queue: ...
)</code></pre>
<p>To fill these arguments, let's describe what each of them represents.</p>
<p><code>fileDescriptor</code> is an <code>Int32</code> that represents a file descriptor pointing to the file/folder we want to monitor. Sounds crazy right? Don't worry! The same <code>FileHandle</code> type used to write the logs can provide this information.</p>
<p>For <code>eventMask</code>, we should pass the event types that we want to be notified of. The enum of possibilities includes many cases like <code>.rename</code>, <code>.delete</code>, <code>.write</code> and <code>.extend</code>, and for monitoring <b>changes</b> in files, the correct one to use depends on how you're writing to that file. If you're <i>overwriting</i> the file by calling <code>String.write(to:)</code>, you should use <code>.write</code>, but if you're <i>modifying</i> the file with <code>FileHandle</code>, you should use <code>.extend</code> instead. For this tutorial, we'll use the latter.</p>
<p>Finally, the <code>queue</code> argument is the dispatch queue in which the events should be dispatched. For simplicity, we'll use the main queue.</p>
<pre><code>self.fileHandle = try FileHandle(forReadingFrom: url)
source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileHandle.fileDescriptor,
eventMask: .extend,
queue: DispatchQueue.main
)</code></pre>
<p>In order to receive event notifications, we must pass an <code>eventHandler</code> to the dispatch source. This might seem weird since you'd normally use a delegate object for this, but the reason it works like this is probably that this is a very old Objective-C API.</p>
<pre><code>source.setEventHandler {
let event = self.source.data
self.process(event: event)
}</code></pre>
<p>When the event handler is triggered, the <code>data</code> property of the dispatch source will contain the set of events that were dispatched.</p>
<p>Additionally, we must provide a way to safely shutdown the dispatch source. We do this by assigning a <code>cancelHandler</code> that closes the <code>FileHandle</code> whenever the source is canceled, and by adding a <code>deinit</code> call to our class that cancels it.</p>
<pre><code>//init()...
source.setCancelHandler {
try? self.fileHandle.close()
}
}
deinit {
source.cancel()
}</code></pre>
<p>To process the events, we'll use the following method:</p>
<pre><code>func process(event: DispatchSource.FileSystemEvent) {
guard event.contains(.extend) else {
return
}
let newData = self.fileHandle.readDataToEndOfFile()
let string = String(data: newData, encoding: .utf8)!
print("Detected: \(string)")
}</code></pre>
<p>When <code>readDataToEndOfFile()</code> is called, the file handle will return everything between the column it's currently pointing at and the end of the file. This also makes it point to the end of the file, making it a great way of fetching the changes in the file. When another event is received, the file handle will already be positioned to read the newest changes.</p>
<p>If the concept of pointers here makes you confused, think of <code>FileHandle</code> like a cursor in a text editor. When we call <code>readDataToEndOfFile()</code>, we're copying everything that was added and moving the cursor to the end of it.</p>
<p>While the <code>guard</code> is going to be useless for this example, it's important to notice that <code>FileSystemEvent</code> is an <code>OptionSet</code>. As you can monitor and receive multiple event types to/from your dispatch source, the idea is that you should always check which events were received so you can call the correct logic for it.</p>
<p>To test all of this, we need to set up two final things. First, as we're not interested in reading what's <i>already</i> logged, we should move the file handler's pointer to the end of the file as soon as we create it. Finally, to wrap it up, we can start the dispatch source by calling <code>source.resume()</code>.</p>
<pre><code>fileHandle.seekToEndOfFile()
source.resume()</code></pre>
<p>Here's a simple <code>ViewController</code> that you can use to test this:</p>
<pre><code>class ViewController: UIViewController {
// Make sure to edit this path to your real Desktop.
static let logPath = URL(fileURLWithPath: "/Users/swiftrocks/Desktop/logTester.log")
override func viewDidLoad() {
// Create the file
try! "".write(to: Self.logPath, atomically: true, encoding: .utf8)
// Monitor the file
let monitor = try! FileMonitor(url: ViewController.logPath)
// Write something to the file
let fileHandle = try! FileHandle(forWritingTo: Self.logPath)
fileHandle.seekToEndOfFile() // Make sure we're writing at the end of the file!
fileHandle.write("Woo! SwiftRocks.".data(using: .utf8)!)
}
}</code></pre>
<p>After running this app, you should see <code>"Detected: Woo! SwiftRocks."</code> in the console, plus anything else you add to that file later on!</p>
<h2>Why doesn't it work when I edit the file in an editor?</h2>
<p>If you try to test this by opening a text editor, adding some text and saving the file, you'll see that it may not work. The reason is that editors like Xcode don't actually modify the file -- instead, they act on copies of it. When you save it, they <i>delete</i> the original file and replace it with the copy they were maintaining. You can confirm that this is the case by registering events like <code>.delete</code> and <code>.link</code> to your dispatch source and see how they get triggered when you save the file. If you're doing this for a macOS app, one way to support text editors would be to register these cases and cancel/reboot the dispatch source when a new file is linked.</p>
<h2>Wrapping up: Getting it ready for our debug feature</h2>
<p>Because making our monitor print what was just logged to a file makes no sense, we can modify our <code>FileMonitor</code> to work with a delegate. Here's the full <code>FileMonitor</code>:</p>
<pre><code>protocol FileMonitorDelegate: AnyObject {
func didReceive(changes: String)
}
final class FileMonitor {
let url: URL
let fileHandle: FileHandle
let source: DispatchSourceFileSystemObject
weak var delegate: FileMonitorDelegate?
init(url: URL) throws {
self.url = url
self.fileHandle = try FileHandle(forReadingFrom: url)
source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileHandle.fileDescriptor,
eventMask: .extend,
queue: DispatchQueue.main
)
source.setEventHandler {
let event = self.source.data
self.process(event: event)
}
source.setCancelHandler {
try? self.fileHandle.close()
}
fileHandle.seekToEndOfFile()
source.resume()
}
deinit {
source.cancel()
}
func process(event: DispatchSource.FileSystemEvent) {
guard event.contains(.extend) else {
return
}
let newData = self.fileHandle.readDataToEndOfFile()
let string = String(data: newData, encoding: .utf8)!
self.delegate?.didReceive(changes: string)
}
}</code></pre>
<div class="sponsor-article-ad-auto hidden"></div>
<p>From here, creating a view that displays the latest logs like in the example picture is just a matter of creating a new <code>FileMonitor</code> and setting the feature as the delegate.</p>
<p>You can make a feature like this without file logging/monitoring, but adding it to the mix would allow you to isolate the feature's logic from the actual logging mechanics. For something that's meant to be only used when debugging, that can be very nice in terms of architecture.</p>
</div></div>
<div class="blog-post footer-main">
<div class="footer-logos">
<a href="https://swiftrocks.com/rss.xml"><i class="fa fa-rss"></i></a>
<a href="https://twitter.com/rockbruno_"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/rockbruno"><i class="fa fa-github"></i></a>
</div>
<div class="footer-text">
© 2025 Bruno Rocha
</div>
<div class="footer-text">
<p><a href="https://swiftrocks.com">Home</a> / <a href="blog">See all posts</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Blog Post (Right Sidebar) End -->
</div>
</div>
</div>
<!-- All Javascript Plugins -->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script type="text/javascript" src="js/prism4.js"></script>
<!-- Main Javascript File -->
<script type="text/javascript" src="js/scripts30.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-H8KZTWSQ1R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-H8KZTWSQ1R');
</script>
</body>
</html>