-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathimproving-observer-pattern-apis-in-swift-with-weak-collections.html
366 lines (304 loc) · 20.3 KB
/
improving-observer-pattern-apis-in-swift-with-weak-collections.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
<!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="In this article, I'll show you a memory management trick with some old-school Foundation types that can be used in any object that applies the Observer pattern to create better and safer APIs.">
<meta name="title" content="Improving Observer Pattern APIs in Swift With Weak Collections">
<meta name="url" content="https://swiftrocks.com/improving-observer-pattern-apis-in-swift-with-weak-collections">
<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="Improving Observer Pattern APIs in Swift With Weak Collections"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta property="og:description" content="In this article, I'll show you a memory management trick with some old-school Foundation types that can be used in any object that applies the Observer pattern to create better and safer APIs."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/improving-observer-pattern-apis-in-swift-with-weak-collections"/>
<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="Improving Observer Pattern APIs in Swift With Weak Collections"/>
<meta name="twitter:description" content="In this article, I'll show you a memory management trick with some old-school Foundation types that can be used in any object that applies the Observer pattern to create better and safer APIs."/>
<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">
<link rel="canonical" href="https://swiftrocks.com/improving-observer-pattern-apis-in-swift-with-weak-collections"/>
<!-- 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/improving-observer-pattern-apis-in-swift-with-weak-collections"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb.jpg"
],
"datePublished": "2020-08-25T14:00:00+02:00",
"dateModified": "2020-08-25T14: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": "Improving Observer Pattern APIs in Swift With Weak Collections",
"abstract": "In this article, I'll show you a memory management trick with some old-school Foundation types that can be used in any object that applies the Observer pattern to create better and safer APIs."
}
</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=Improving Observer Pattern APIs in Swift With Weak Collections-->
<!--WRITEIT_POST_HTML_NAME=improving-observer-pattern-apis-in-swift-with-weak-collections-->
<!--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=In this article, I'll show you a memory management trick with some old-school Foundation types that can be used in any object that applies the Observer pattern to create better and safer APIs.-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-08-25T14:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2020-08-25T14:00:00+02:00-->
<title>Improving Observer Pattern APIs in Swift With Weak Collections</title>
<div class="blog-post">
<div class="post-title-index">
<h1>Improving Observer Pattern APIs in Swift With Weak Collections</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 25 Aug 2020
</div>
</div>
<p>Even if you don't know what the <b>Observer</b> pattern is, there's a good chance that you applied it somewhere in the past. This design pattern is used to create APIs that notify one or more subjects about changes in a certain object, with the <code>NotificationCenter</code> being the most popular use of this pattern in iOS.</p>
<div class="sponsor-article-ad-auto hidden"></div>
<p>One simple way to reproduce what the <code>NotificationCenter</code> does is to create a dictionary that maps a string (a notification) to an array of closures. Whenever that notification is "posted", all of the closures are executed.</p>
<pre><code>final class NotificationCenter {
var notifications = [String: [() -> Void]]()
func register(_ closure: @escaping () -> Void, forNotification string: String) {
notifications[string, default: []].append(closure)
}
func post(notification: String) {
notifications[notification]?.forEach { $0() }
}
}</code></pre>
<p>However, the point of this article is not to attempt to reproduce the <code>NotificationCenter</code>, but to show you <b>what</b> this implementation implies. You must be aware that whenever you're using the basic Swift dictionaries, arrays or sets, <b>all keys and values are retained!</b> Additionally, you need to be aware that <b>closures are reference types</b>, so they are retained as well and can outlive their owners.</p>
<p>What this means is that you'll see that while this implementation works, it's going to be a huge memory issue. Because it's retaining the closures, they will never be unregistered. The notifications will attempt to execute them even if the object that registered it is <b>long gone</b>.</p>
<p>If you're been working with iOS for a long time, you might remember that iOS's own <code>NotificationCenter</code> had this issue! Prior to iOS 9, every observer had to be unregistered when being deallocated, because if you didn't, it would attempt to execute it when it shouldn't and crash your app.</p>
<pre><code>deinit {
NotificationCenter.default.removeObserver(self, ...)
}</code></pre>
<p>In the case of our implementation, we could replicate this by adding the concept of "owners" to our closures, so that we are able to remove them if someone wishes to be unregistered. Fortunately, not only we don't need to go this far, but it's good if we don't. If you're developing an API, its usability should be one of your main priorities. In this case, let's take a look at how we can create an observer API that <b>is</b> memory safe while also <b>not having</b> to manually unregister the observers. The problem shown above that <code>NotificationCenter</code> had was fixed in iOS 9 (removing observers became an automatic process) when Apple started applying the same concept.</p>
<h2>Weak Collections</h2>
<p>Let's pretend we have a deeplink-based navigation system where "feature providers" can provide a feature (represented as an <code>UIViewController</code>) if they recognize the deeplink that the app wants to present:</p>
<pre><code>final class FeaturePusher {
typealias FeatureProvider = (URL) -> UIViewController?
lazy var providers = [FeatureProvider]()
func register(featureProvider: @escaping FeatureProvider) {
providers.append(featureProvider)
}
func feature(forUrl url: URL) -> UIViewController? {
return providers.lazy.compactMap { $0(url) }.first
}
}</code></pre>
<p>Like in the notification center example, this suffers from a memory issue. If whoever provided those closures ceases to exist, the <code>FeaturePusher</code> class will still be able to execute the closure and potentially crash the app. Fortunately, there are a few useful types in <code>Foundation</code> that can assist us in improving that.</p>
<p>As I've shown before in my <a href="https://swiftrocks.com/weak-dictionary-values-in-swift">Weak Dictionary Values</a> article, <code>Foundation</code> offers a series of lower-level Obj-C collection types that are more powerful than the basic Swift ones. Two of them specifically are <code>NSMapTable</code> and <code>NSHashTable</code>, which are Obj-C versions of <code>Dictionary</code> and <code>Set</code>, respectively. Both of them allow a higher range of memory management options, which include weak references for both values and keys. If instead of using a base Swift array we used a <code>NSMapTable</code> that has our closures as values and <b>weak</b> references to whoever provided that block as a key, our navigation system would automatically evict and deallocate the closures whenever the related providers are deallocated. That's because in weak collections if the weak component is deallocated, the entire entry will be evicted from the collection.</p>
<p>Creating Weak Collections is just a matter of using the correct initializer. A dictionary with weak keys can be initialized with <code>NSMapTable.weakToStrongObjects()</code>, while one with weak values can be initialized with <code>NSMapTable.strongToWeakObjects()</code>. If we want our navigation system's closures to be automatically unregistered if the object that registered them was deallocated, we can create a weak-keyed dictionary that maps an object to an array of closures:</p>
<pre><code>lazy var providers = NSMapTable<AnyObject, NSHashTable<FeatureProviderBox>>.weakToStrongObjects()</code></pre>
<p>Because the keys are weak, the closures will automatically be evicted from the dictionary if the key ceases to exist.</p>
<p>Note that <code>NSMapTable</code> is an Obj-C API, so all keys and values must be class objects. That's why we have to use a <code>NSHashTable</code> as a value instead of a regular <code>Set</code> or <code>Array</code>.</p>
<p>You can make Obj-C types like <code>NSMapTable</code> able to hold Swift structs by creating a generic <code>Box</code> class wrapper type. Here, we create one to be able to represent our feature closure as a class object (<code>FeatureProviderBox</code>) in order to be able to store it inside the <code>NSHashTable</code>.</p>
<pre><code>final class Box<T> {
let obj: T
init(obj: T) {
self.obj = obj
}
}
final class FeaturePusher {
typealias FeatureProvider = (URL) -> UIViewController?
typealias FeatureProviderBox = Box<FeatureProvider>
lazy var providers = NSMapTable<AnyObject, NSHashTable<FeatureProviderBox>>.weakToStrongObjects()
func register(featureProvider: @escaping FeatureProvider, forObject object: AnyObject) {
if providers.object(forKey: object) == nil {
providers.setObject(NSHashTable(), forKey: object)
}
let box = FeatureProviderBox(obj: featureProvider)
providers.object(forKey: object)?.add(box)
}
func feature(forUrl url: URL) -> UIViewController? {
let allValues = providers.objectEnumerator()
while let table = allValues?.nextObject() as? NSHashTable<FeatureProviderBox> {
if let feature = table.allObjects.lazy.compactMap { $0.obj(url) }.first {
return feature
}
}
return nil
}
}</code></pre>
<h2>Unit Testing Weak Collections (and Reference Cycles)</h2>
<p>To check if our improvement worked, we can create a unit test that checks if the correct view controllers are returned:</p>
<pre><code>func test_observerReturnsTheCorrectFeature() {
let pusher = FeaturePusher()
let swiftRocksUrl = URL(string: "myApp://swiftRocks")!
let swiftRocksVC = SwiftRocksViewController()
let observerObject: UIView = UIView()
pusher.register(featureProvider: { url in
return url == swiftRocksUrl ? swiftRocksVC : nil
}, forObject: observerObject)
XCTAssertTrue(pusher.feature(forUrl: swiftRocksUrl) === swiftRocksVC)
let someOtherURL = URL(string: "myApp://notSwiftRocks")!
XCTAssertNil(pusher.feature(forUrl: someOtherURL))
}</code></pre>
<p>However, we are mostly interested in seeing if the automatic eviction is working. To test that the observers are being deallocated and the closures are being evicted, we can use an <code>autoreleasepool</code>. As described in my <a href="https://swiftrocks.com/autoreleasepool-in-2019-swift">autoreleasepool article</a>, you can use a pool whenever you want something to be deallocated as soon as possible:</p>
<pre><code>func test_observerIsDeallocated() {
let pusher = FeaturePusher()
let swiftRocksUrl = URL(string: "myApp://swiftRocks")!
let swiftRocksVC = SwiftRocksViewController()
autoreleasepool {
let observerObject: UIView = UIView()
pusher.register(featureProvider: { url in
return url == swiftRocksUrl ? swiftRocksVC : nil
}, forObject: observerObject)
XCTAssertTrue(pusher.feature(forUrl: swiftRocksUrl) === swiftRocksVC)
let someOtherURL = URL(string: "myApp://notSwiftRocks")!
XCTAssertNil(pusher.feature(forUrl: someOtherURL))
}
XCTAssertNil(pusher.feature(forUrl: swiftRocksUrl))
}</code></pre>
<div class="sponsor-article-ad-auto hidden"></div>
<p>You'll see that this test will pass, but if you're not sure why, try removing the pool to see what happens. The test will fail, and the reason is that objects aren't deallocated as soon as they go out of scope in iOS (that will usually happen at the end of a <code>RunLoop</code>). In this case, the pool is simply a way to force it to deallocate immediately for unit testing purposes. This same trick can be applied to unit test any type of reference cycle situation :)</p>
<h2>Conclusion: Final considerations for Weak Collections</h2>
<p>Weak Collections are a great way to build better APIs, but you must be aware of their possible limitations. While types like <code>NSHashTable</code> and <code>NSPointerArray</code> are all-around great tools, you may see that <code>NSMapTable</code>'s documentation tells you to be careful with <code>weakToStrongObjects()</code>. In that configuration, although the values are ejected from the table as expected, they still may be held in memory for a larger period of time. That's why this article didn't attempt to fully reproduce the NotificationCenter, as <a href="https://forums.swift.org/t/why-is-this-unreferenced-object-refusing-to-deallocate/39604?u=rockbruno">it took me a while to realize that doing so would require a pretty ugly workaround.</a> However, you'll find that <code>NSHashTable</code> is good to go under any configuration.</p>
<h2>References and Good Reads</h2>
<a href="https://swiftrocks.com/weak-dictionary-values-in-swift">Weak Dictionary Values (SwiftRocks)</a>
<br>
<a href="https://swiftrocks.com/autoreleasepool-in-2019-swift">@autoreleasepool uses in 2019 Swift (SwiftRocks)</a>
<br>
</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/bootstrap@5.0.2/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>