-
Notifications
You must be signed in to change notification settings - Fork 5
/
atom.xml
548 lines (341 loc) · 517 KB
/
atom.xml
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>SwiftGG</title>
<subtitle>走心的 Swift 翻译组</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://swift.gg/"/>
<updated>2022-08-26T13:33:55.038Z</updated>
<id>https://swift.gg/</id>
<author>
<name>SwiftGG</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>给 UIView 来点烟花</title>
<link href="https://swift.gg/2019/08/14/add-fireworks-and-sparks-to-a-uiview/"/>
<id>https://swift.gg/2019/08/14/add-fireworks-and-sparks-to-a-uiview/</id>
<published>2019-08-14T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.038Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Tomasz Szulc,<a href="http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09<br>译者:<a href="https://github.com/joeytat" target="_blank" rel="noopener">Joeytat</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;定稿:<a href="undefined">Pancf</a></p></blockquote><!--此处开始正文--><a id="more"></a><p>你也很喜欢常用 app 里的那些小细节吧?当我从 <a href="https://dribbble.com/" target="_blank" rel="noopener">dribbble</a> 中寻找灵感时,就发现了这个漂亮的设计:当用户在某个重要的视图中修改设置或者进行了什么操作时,会有烟花在周围绽放。于是我就在想这个东西有多难实现,然后过了一段时间,我完成了 :)</p><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/hero.gif" alt="hero"></p><h2 id="烟花的细节"><a href="#烟花的细节" class="headerlink" title="烟花的细节"></a><em>烟花的细节</em></h2><p>下面是对于这个效果的详细描述。烟花应该在视图周围的某个特殊的位置爆开,可能是按钮在点击事件响应时。当点击发生时,烟花应该在按钮的四角爆开,并且爆炸产生的火花应该按照自身的轨迹移动。</p><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/final.jpg" alt="final"></p><p><em>超喜欢这个效果! 不仅让我感受到视觉上的愉悦,还让我想要不停地戳这个按钮! :) 🎉</em></p><p>现在让我们再看一眼这个动画。每次生成的烟花,其整体行为是大致相似的。但还是在火花的轨迹和大小上有一些区别。让我们拆开来说。</p><ul><li>每一次点击都会产生<em>两处烟花</em>,</li><li>每一处烟花会产生 <em>8 个火花</em>,</li><li>每个火花都遵循着自己的<em>轨迹</em>,</li><li>轨迹看起来<em>相似</em>,但其实<em>不完全一样</em>。从爆炸<em>开始</em>的位置来看,有部分朝<em>右</em>,有部分朝<em>左</em>,剩余的朝<em>上</em>或<em>下</em>。</li></ul><h2 id="火花的分布"><a href="#火花的分布" class="headerlink" title="火花的分布"></a><em>火花的分布</em></h2><p>这个烟花特效有着简单的火花分布规则。将爆炸点分为四块「视线区域」来看:上左,上右,下左,下右,每个区域都有两个火花。</p><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/sparks-distribution.jpg" alt="sparks distribution"></p><h2 id="火花的轨迹"><a href="#火花的轨迹" class="headerlink" title="火花的轨迹"></a><em>火花的轨迹</em></h2><p>火花的移动有着自己的轨迹。在一处烟花中有 8 个火花,那至少需要 8 道轨迹。理想状态下应该有更多的轨迹,可以增加一些随机性,这样连续爆发烟花的时候,不会看起来和前一个完全一样。</p><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/spark-trajectories.jpg" alt="spark-trajectories"></p><p>我为每一个区域创建了 4 条轨迹,这样就赋予了两倍于火花数量的随机性。为了方便计算,我统一了每条轨迹的初始点。因为我用了不同的工具来可视化这些轨迹,所以图上的轨迹和我完成的效果略有不同 - 但你能明白我的想法就行 :)</p><h2 id="实现"><a href="#实现" class="headerlink" title="_实现_"></a>_实现_</h2><p>理论足够了。接下来让我们把各个模块拼凑起来。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">SparkTrajectory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 存储着定义轨迹所需要的所有的点</span></span><br><span class="line"> <span class="keyword">var</span> points: [<span class="type">CGPoint</span>] { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 用 path 来表现轨迹</span></span><br><span class="line"> <span class="keyword">var</span> path: <span class="type">UIBezierPath</span> { <span class="keyword">get</span> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这是一个用于表示火花轨迹的协议。为了能够更简单地创建各式各样的轨迹,我定义了这个通用接口协议,并且选择基于三阶 <a href="https://en.wikipedia.org/wiki/B%C3%A9zier_curve" target="_blank" rel="noopener">贝塞尔曲线</a> 来实现轨迹;还添加了一个 <code>init</code> 方法,这样我就可以通过一行代码来创建轨迹了。三阶贝塞尔曲线必须包含四个点。第一个和最后一个点定义了轨迹的开始和结束的位置,中间的两个点用于控制曲线的弯曲度。你可以用在线数学工具 <a href="https://www.desmos.com/calculator/epunzldltu" target="_blank" rel="noopener">desmos</a> 来调整自己的贝塞尔曲线。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">/// 拥有两个控制点的贝塞尔曲线</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">CubicBezierTrajectory</span>: <span class="title">SparkTrajectory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> points = [<span class="type">CGPoint</span>]()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">init</span>(<span class="number">_</span> x0: <span class="type">CGFloat</span>, <span class="number">_</span> y0: <span class="type">CGFloat</span>,</span><br><span class="line"> <span class="number">_</span> x1: <span class="type">CGFloat</span>, <span class="number">_</span> y1: <span class="type">CGFloat</span>,</span><br><span class="line"> <span class="number">_</span> x2: <span class="type">CGFloat</span>, <span class="number">_</span> y2: <span class="type">CGFloat</span>,</span><br><span class="line"> <span class="number">_</span> x3: <span class="type">CGFloat</span>, <span class="number">_</span> y3: <span class="type">CGFloat</span>) {</span><br><span class="line"> <span class="keyword">self</span>.points.append(<span class="type">CGPoint</span>(x: x0, y: y0))</span><br><span class="line"> <span class="keyword">self</span>.points.append(<span class="type">CGPoint</span>(x: x1, y: y1))</span><br><span class="line"> <span class="keyword">self</span>.points.append(<span class="type">CGPoint</span>(x: x2, y: y2))</span><br><span class="line"> <span class="keyword">self</span>.points.append(<span class="type">CGPoint</span>(x: x3, y: y3))</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> path: <span class="type">UIBezierPath</span> {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">self</span>.points.<span class="built_in">count</span> == <span class="number">4</span> <span class="keyword">else</span> { <span class="built_in">fatalError</span>(<span class="string">"4 points required"</span>) }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> path = <span class="type">UIBezierPath</span>()</span><br><span class="line"> path.move(to: <span class="keyword">self</span>.points[<span class="number">0</span>])</span><br><span class="line"> path.addCurve(to: <span class="keyword">self</span>.points[<span class="number">3</span>], controlPoint1: <span class="keyword">self</span>.points[<span class="number">1</span>], controlPoint2: <span class="keyword">self</span>.points[<span class="number">2</span>])</span><br><span class="line"> <span class="keyword">return</span> path</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/desmos-tool.png" alt="desmos-tool"></p><p>接下来要实现的是一个能够创建随机轨迹的工厂。前面的图中你可以看到轨迹是根据颜色来分组的。我只创建了上右和下右两块位置的轨迹,然后进行了镜像复制。这对于我们将要发射的烟花来说已经足够了🚀</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">SparkTrajectoryFactory</span> </span>{}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">ClassicSparkTrajectoryFactoryProtocol</span>: <span class="title">SparkTrajectoryFactory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">randomTopRight</span><span class="params">()</span></span> -> <span class="type">SparkTrajectory</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">randomBottomRight</span><span class="params">()</span></span> -> <span class="type">SparkTrajectory</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">ClassicSparkTrajectoryFactory</span>: <span class="title">ClassicSparkTrajectoryFactoryProtocol</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="built_in">lazy</span> <span class="keyword">var</span> topRight: [<span class="type">SparkTrajectory</span>] = {</span><br><span class="line"> <span class="keyword">return</span> [</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.31</span>, -<span class="number">0.46</span>, <span class="number">0.74</span>, -<span class="number">0.29</span>, <span class="number">0.99</span>, <span class="number">0.12</span>),</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.31</span>, -<span class="number">0.46</span>, <span class="number">0.62</span>, -<span class="number">0.49</span>, <span class="number">0.88</span>, -<span class="number">0.19</span>),</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.10</span>, -<span class="number">0.54</span>, <span class="number">0.44</span>, -<span class="number">0.53</span>, <span class="number">0.66</span>, -<span class="number">0.30</span>),</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.19</span>, -<span class="number">0.46</span>, <span class="number">0.41</span>, -<span class="number">0.53</span>, <span class="number">0.65</span>, -<span class="number">0.45</span>),</span><br><span class="line"> ]</span><br><span class="line"> }()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="built_in">lazy</span> <span class="keyword">var</span> bottomRight: [<span class="type">SparkTrajectory</span>] = {</span><br><span class="line"> <span class="keyword">return</span> [</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.42</span>, -<span class="number">0.01</span>, <span class="number">0.68</span>, <span class="number">0.11</span>, <span class="number">0.87</span>, <span class="number">0.44</span>),</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.35</span>, <span class="number">0.00</span>, <span class="number">0.55</span>, <span class="number">0.12</span>, <span class="number">0.62</span>, <span class="number">0.45</span>),</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.21</span>, <span class="number">0.05</span>, <span class="number">0.31</span>, <span class="number">0.19</span>, <span class="number">0.32</span>, <span class="number">0.45</span>),</span><br><span class="line"> <span class="type">CubicBezierTrajectory</span>(<span class="number">0.00</span>, <span class="number">0.00</span>, <span class="number">0.18</span>, <span class="number">0.00</span>, <span class="number">0.31</span>, <span class="number">0.11</span>, <span class="number">0.35</span>, <span class="number">0.25</span>),</span><br><span class="line"> ]</span><br><span class="line"> }()</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">randomTopRight</span><span class="params">()</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.topRight[<span class="type">Int</span>(arc4random_uniform(<span class="type">UInt32</span>(<span class="keyword">self</span>.topRight.<span class="built_in">count</span>)))]</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">randomBottomRight</span><span class="params">()</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.bottomRight[<span class="type">Int</span>(arc4random_uniform(<span class="type">UInt32</span>(<span class="keyword">self</span>.bottomRight.<span class="built_in">count</span>)))]</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里先创建了用来表示火花轨迹工厂的抽象协议,还有一个我将其命名为<em>经典烟花</em>的火花轨迹的抽象协议,这样的抽象可以方便后续将其替换成其他的轨迹协议。</p><p>如同我前面提到的,我通过 <a href="https://www.desmos.com/calculator/epunzldltu" target="_blank" rel="noopener">desmos</a> 创建了两组轨迹,对应着右上,和右下两块区域。</p><p><strong>重要提醒</strong>:如果在 desmos 上 y 轴所显示的是正数,那么你应该将其转换成负数。因为在 iOS 系统中,越接近屏幕顶部 y 轴的值越小,所以 y 轴的值需要翻转一下。</p><p>并且值得一提的是,为了后面好计算,所有的轨迹初始点都是 (0,0)。</p><p>我们现在创建好了轨迹。接下来创建一些视图来表示火花。对于经典烟花来说,只需要有颜色的圆圈就行。通过抽象可以让我们在未来以更低的成本,创建不同的火花视图。比如小鸭子图片,或者是胖吉猫 :)</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SparkView</span>: <span class="title">UIView</span> </span>{}</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">CircleColorSparkView</span>: <span class="title">SparkView</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">init</span>(color: <span class="type">UIColor</span>, size: <span class="type">CGSize</span>) {</span><br><span class="line"> <span class="keyword">super</span>.<span class="keyword">init</span>(frame: <span class="type">CGRect</span>(origin: .zero, size: size))</span><br><span class="line"> <span class="keyword">self</span>.backgroundColor = color</span><br><span class="line"> <span class="keyword">self</span>.layer.cornerRadius = <span class="keyword">self</span>.frame.width / <span class="number">2.0</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">required</span> <span class="keyword">init</span>?(coder aDecoder: <span class="type">NSCoder</span>) {</span><br><span class="line"> <span class="keyword">super</span>.<span class="keyword">init</span>(coder: aDecoder)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">UIColor</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> sparkColorSet1: [<span class="type">UIColor</span>] = {</span><br><span class="line"> <span class="keyword">return</span> [</span><br><span class="line"> <span class="type">UIColor</span>(red:<span class="number">0.89</span>, green:<span class="number">0.58</span>, blue:<span class="number">0.70</span>, alpha:<span class="number">1.00</span>),</span><br><span class="line"> <span class="type">UIColor</span>(red:<span class="number">0.96</span>, green:<span class="number">0.87</span>, blue:<span class="number">0.62</span>, alpha:<span class="number">1.00</span>),</span><br><span class="line"> <span class="type">UIColor</span>(red:<span class="number">0.67</span>, green:<span class="number">0.82</span>, blue:<span class="number">0.94</span>, alpha:<span class="number">1.00</span>),</span><br><span class="line"> <span class="type">UIColor</span>(red:<span class="number">0.54</span>, green:<span class="number">0.56</span>, blue:<span class="number">0.94</span>, alpha:<span class="number">1.00</span>),</span><br><span class="line"> ]</span><br><span class="line"> }()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>为了创建火花视图,我们还需要一个工厂数据以填充,需要的数据是火花的大小,以及用来决定火花在哪个烟花的索引(用于增加随机性)。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">SparkViewFactoryData</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> size: <span class="type">CGSize</span> { <span class="keyword">get</span> }</span><br><span class="line"> <span class="keyword">var</span> index: <span class="type">Int</span> { <span class="keyword">get</span> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">SparkViewFactory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">create</span><span class="params">(with data: SparkViewFactoryData)</span></span> -> <span class="type">SparkView</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CircleColorSparkViewFactory</span>: <span class="title">SparkViewFactory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> colors: [<span class="type">UIColor</span>] {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">UIColor</span>.sparkColorSet1</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">create</span><span class="params">(with data: SparkViewFactoryData)</span></span> -> <span class="type">SparkView</span> {</span><br><span class="line"> <span class="keyword">let</span> color = <span class="keyword">self</span>.colors[data.index % <span class="keyword">self</span>.colors.<span class="built_in">count</span>]</span><br><span class="line"> <span class="keyword">return</span> <span class="type">CircleColorSparkView</span>(color: color, size: data.size)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>你看这样抽象了之后,就算再实现一个像胖吉猫的火花也会很简单。接下来让我们来创建<em>经典烟花</em>。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typealias</span> <span class="type">FireworkSpark</span> = (sparkView: <span class="type">SparkView</span>, trajectory: <span class="type">SparkTrajectory</span>)</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">Firework</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 烟花的初始位置</span></span><br><span class="line"> <span class="keyword">var</span> origin: <span class="type">CGPoint</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 定义了轨迹的大小. 轨迹都是统一大小</span></span><br><span class="line"> <span class="comment">/// 所以需要在展示到屏幕上前将其放大</span></span><br><span class="line"> <span class="keyword">var</span> scale: <span class="type">CGFloat</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 火花的大小</span></span><br><span class="line"> <span class="keyword">var</span> sparkSize: <span class="type">CGSize</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 获取轨迹</span></span><br><span class="line"> <span class="keyword">var</span> trajectoryFactory: <span class="type">SparkTrajectoryFactory</span> { <span class="keyword">get</span> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 获取火花视图</span></span><br><span class="line"> <span class="keyword">var</span> sparkViewFactory: <span class="type">SparkViewFactory</span> { <span class="keyword">get</span> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">sparkViewFactoryData</span><span class="params">(at index: Int)</span></span> -> <span class="type">SparkViewFactoryData</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">sparkView</span><span class="params">(at index: Int)</span></span> -> <span class="type">SparkView</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">trajectory</span><span class="params">(at index: Int)</span></span> -> <span class="type">SparkTrajectory</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Firework</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 帮助方法,用于返回火花视图及对应的轨迹</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">spark</span><span class="params">(at index: Int)</span></span> -> <span class="type">FireworkSpark</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">FireworkSpark</span>(<span class="keyword">self</span>.sparkView(at: index), <span class="keyword">self</span>.trajectory(at: index))</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这就是烟花的抽象。为了表示一个烟花需要这些东西:</p><ul><li><em>origin</em></li><li><em>scale</em></li><li><em>sparkSize</em></li><li><em>trajectoryFactory</em></li><li><em>sparkViewFactory</em></li></ul><p>在我们实现协议之前,还有一个我之前没有提到过的叫做<em>按轨迹缩放</em>的概念。当火花处于轨迹 <-1, 1> 或相似的位置时,我们希望它的大小会跟随轨迹变化。我们还需要放大路径以覆盖更大的屏幕显示效果。此外,我们还需要支持水平翻转路径,以方便我们实现经典烟花左侧部分的轨迹,并且还要让轨迹能朝某个指定方向偏移一点(增加随机性)。下面是两个能够帮助我们达到目的的方法,我相信这段代码已经不需要更多描述了。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">SparkTrajectory</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 缩放轨迹使其符合各种 UI 的要求</span></span><br><span class="line"> <span class="comment">/// 在各种形变和 shift: 之前使用</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">scale</span><span class="params">(by value: CGFloat)</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">var</span> copy = <span class="keyword">self</span></span><br><span class="line"> (<span class="number">0</span>..<<span class="keyword">self</span>.points.<span class="built_in">count</span>).forEach { copy.points[$<span class="number">0</span>].multiply(by: value) }</span><br><span class="line"> <span class="keyword">return</span> copy</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 水平翻转轨迹</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">flip</span><span class="params">()</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">var</span> copy = <span class="keyword">self</span></span><br><span class="line"> (<span class="number">0</span>..<<span class="keyword">self</span>.points.<span class="built_in">count</span>).forEach { copy.points[$<span class="number">0</span>].x *= -<span class="number">1</span> }</span><br><span class="line"> <span class="keyword">return</span> copy</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 偏移轨迹,在每个点上生效</span></span><br><span class="line"> <span class="comment">/// 在各种形变和 scale: 和之后使用</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">shift</span><span class="params">(to point: CGPoint)</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">var</span> copy = <span class="keyword">self</span></span><br><span class="line"> <span class="keyword">let</span> vector = <span class="type">CGVector</span>(dx: point.x, dy: point.y)</span><br><span class="line"> (<span class="number">0</span>..<<span class="keyword">self</span>.points.<span class="built_in">count</span>).forEach { copy.points[$<span class="number">0</span>].add(vector: vector) }</span><br><span class="line"> <span class="keyword">return</span> copy</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>好了,接下来就是实现经典烟花。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ClassicFirework</span>: <span class="title">Firework</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> x | x</span></span><br><span class="line"><span class="comment"> x | x</span></span><br><span class="line"><span class="comment"> |</span></span><br><span class="line"><span class="comment"> ---------------</span></span><br><span class="line"><span class="comment"> x | x</span></span><br><span class="line"><span class="comment"> x |</span></span><br><span class="line"><span class="comment"> | x</span></span><br><span class="line"><span class="comment"> **/</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="class"><span class="keyword">struct</span> <span class="title">FlipOptions</span>: <span class="title">OptionSet</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> rawValue: <span class="type">Int</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">let</span> horizontally = <span class="type">FlipOptions</span>(rawValue: <span class="number">1</span> << <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">let</span> vertically = <span class="type">FlipOptions</span>(rawValue: <span class="number">1</span> << <span class="number">1</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="class"><span class="keyword">enum</span> <span class="title">Quarter</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">case</span> topRight</span><br><span class="line"> <span class="keyword">case</span> bottomRight</span><br><span class="line"> <span class="keyword">case</span> bottomLeft</span><br><span class="line"> <span class="keyword">case</span> topLeft</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> origin: <span class="type">CGPoint</span></span><br><span class="line"> <span class="keyword">var</span> scale: <span class="type">CGFloat</span></span><br><span class="line"> <span class="keyword">var</span> sparkSize: <span class="type">CGSize</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> maxChangeValue: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="number">10</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> trajectoryFactory: <span class="type">SparkTrajectoryFactory</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">ClassicSparkTrajectoryFactory</span>()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> classicTrajectoryFactory: <span class="type">ClassicSparkTrajectoryFactoryProtocol</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.trajectoryFactory <span class="keyword">as</span>! <span class="type">ClassicSparkTrajectoryFactoryProtocol</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> sparkViewFactory: <span class="type">SparkViewFactory</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">CircleColorSparkViewFactory</span>()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">var</span> quarters = [<span class="type">Quarter</span>]()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">init</span>(origin: <span class="type">CGPoint</span>, sparkSize: <span class="type">CGSize</span>, scale: <span class="type">CGFloat</span>) {</span><br><span class="line"> <span class="keyword">self</span>.origin = origin</span><br><span class="line"> <span class="keyword">self</span>.scale = scale</span><br><span class="line"> <span class="keyword">self</span>.sparkSize = sparkSize</span><br><span class="line"> <span class="keyword">self</span>.quarters = <span class="keyword">self</span>.shuffledQuarters()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">sparkViewFactoryData</span><span class="params">(at index: Int)</span></span> -> <span class="type">SparkViewFactoryData</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">DefaultSparkViewFactoryData</span>(size: <span class="keyword">self</span>.sparkSize, index: index)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">sparkView</span><span class="params">(at index: Int)</span></span> -> <span class="type">SparkView</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.sparkViewFactory.create(with: <span class="keyword">self</span>.sparkViewFactoryData(at: index))</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">trajectory</span><span class="params">(at index: Int)</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">let</span> quarter = <span class="keyword">self</span>.quarters[index]</span><br><span class="line"> <span class="keyword">let</span> flipOptions = <span class="keyword">self</span>.flipOptions(<span class="keyword">for</span>: quarter)</span><br><span class="line"> <span class="keyword">let</span> changeVector = <span class="keyword">self</span>.randomChangeVector(flipOptions: flipOptions, maxValue: <span class="keyword">self</span>.maxChangeValue)</span><br><span class="line"> <span class="keyword">let</span> sparkOrigin = <span class="keyword">self</span>.origin.adding(vector: changeVector)</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.randomTrajectory(flipOptions: flipOptions).scale(by: <span class="keyword">self</span>.scale).shift(to: sparkOrigin)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">flipOptions</span><span class="params">(`<span class="keyword">for</span>` quarter: Quarter)</span></span> -> <span class="type">FlipOptions</span> {</span><br><span class="line"> <span class="keyword">var</span> flipOptions: <span class="type">FlipOptions</span> = []</span><br><span class="line"> <span class="keyword">if</span> quarter == .bottomLeft || quarter == .topLeft {</span><br><span class="line"> flipOptions.insert(.horizontally)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> quarter == .bottomLeft || quarter == .bottomRight {</span><br><span class="line"> flipOptions.insert(.vertically)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> flipOptions</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">shuffledQuarters</span><span class="params">()</span></span> -> [<span class="type">Quarter</span>] {</span><br><span class="line"> <span class="keyword">var</span> quarters: [<span class="type">Quarter</span>] = [</span><br><span class="line"> .topRight, .topRight,</span><br><span class="line"> .bottomRight, .bottomRight,</span><br><span class="line"> .bottomLeft, .bottomLeft,</span><br><span class="line"> .topLeft, .topLeft</span><br><span class="line"> ]</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> shuffled = [<span class="type">Quarter</span>]()</span><br><span class="line"> <span class="keyword">for</span> <span class="number">_</span> <span class="keyword">in</span> <span class="number">0</span>..<quarters.<span class="built_in">count</span> {</span><br><span class="line"> <span class="keyword">let</span> idx = <span class="type">Int</span>(arc4random_uniform(<span class="type">UInt32</span>(quarters.<span class="built_in">count</span>)))</span><br><span class="line"> shuffled.append(quarters[idx])</span><br><span class="line"> quarters.remove(at: idx)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> shuffled</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">randomTrajectory</span><span class="params">(flipOptions: FlipOptions)</span></span> -> <span class="type">SparkTrajectory</span> {</span><br><span class="line"> <span class="keyword">var</span> trajectory: <span class="type">SparkTrajectory</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> flipOptions.<span class="built_in">contains</span>(.vertically) {</span><br><span class="line"> trajectory = <span class="keyword">self</span>.classicTrajectoryFactory.randomBottomRight()</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> trajectory = <span class="keyword">self</span>.classicTrajectoryFactory.randomTopRight()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> flipOptions.<span class="built_in">contains</span>(.horizontally) ? trajectory.flip() : trajectory</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">randomChangeVector</span><span class="params">(flipOptions: FlipOptions, maxValue: Int)</span></span> -> <span class="type">CGVector</span> {</span><br><span class="line"> <span class="keyword">let</span> values = (<span class="keyword">self</span>.randomChange(maxValue), <span class="keyword">self</span>.randomChange(maxValue))</span><br><span class="line"> <span class="keyword">let</span> changeX = flipOptions.<span class="built_in">contains</span>(.horizontally) ? -values.<span class="number">0</span> : values.<span class="number">0</span></span><br><span class="line"> <span class="keyword">let</span> changeY = flipOptions.<span class="built_in">contains</span>(.vertically) ? values.<span class="number">1</span> : -values.<span class="number">0</span></span><br><span class="line"> <span class="keyword">return</span> <span class="type">CGVector</span>(dx: changeX, dy: changeY)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">randomChange</span><span class="params">(<span class="number">_</span> maxValue: Int)</span></span> -> <span class="type">CGFloat</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">CGFloat</span>(arc4random_uniform(<span class="type">UInt32</span>(maxValue)))</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>大多数代码都是 <code>Firework</code> 协议的实现,所以应该很容易理解。我们在各处传递了需要的工厂类,还添加了一个额外的枚举类型来随机地为每个火花指定轨迹。</p><p>有少数几个方法用来为烟花和火花增加随机性。</p><p>还引入了一个 <code>quarters</code> 属性,其中包含了火花的所有的方位。我们通过 <code>shuffledQuarters:</code> 来重新排列,以确保我们不会总是在相同的方位创建相同数量的火花。</p><p>好了,我们创建好了烟花,接下来怎么让火花动起来呢?这就引入了火花动画启动器的概念。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">SparkViewAnimator</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">animate</span><span class="params">(spark: FireworkSpark, duration: TimeInterval)</span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法接受一个包含火花视图和其对应轨迹的元组 <code>FireworkSpark</code>,以及动画的持续时间。方法的实现取决于我们。我自己的实现蛮多的,但主要做了三件事情:让火花视图跟随轨迹,同时缩放火花(带有随机性),修改其不透明度。简单吧。同时得益于 <code>SparkViewAnimator</code> 的抽象度,我们还可以很简单地将其替换成任何我们想要的动画效果。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">ClassicFireworkAnimator</span>: <span class="title">SparkViewAnimator</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">animate</span><span class="params">(spark: FireworkSpark, duration: TimeInterval)</span></span> {</span><br><span class="line"> spark.sparkView.isHidden = <span class="literal">false</span> <span class="comment">// show previously hidden spark view</span></span><br><span class="line"></span><br><span class="line"> <span class="type">CATransaction</span>.begin()</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 火花的位置</span></span><br><span class="line"> <span class="keyword">let</span> positionAnim = <span class="type">CAKeyframeAnimation</span>(keyPath: <span class="string">"position"</span>)</span><br><span class="line"> positionAnim.path = spark.trajectory.path.cgPath</span><br><span class="line"> positionAnim.calculationMode = kCAAnimationLinear</span><br><span class="line"> positionAnim.rotationMode = kCAAnimationRotateAuto</span><br><span class="line"> positionAnim.duration = duration</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 火花的缩放</span></span><br><span class="line"> <span class="keyword">let</span> randomMaxScale = <span class="number">1.0</span> + <span class="type">CGFloat</span>(arc4random_uniform(<span class="number">7</span>)) / <span class="number">10.0</span></span><br><span class="line"> <span class="keyword">let</span> randomMinScale = <span class="number">0.5</span> + <span class="type">CGFloat</span>(arc4random_uniform(<span class="number">3</span>)) / <span class="number">10.0</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> fromTransform = <span class="type">CATransform3DIdentity</span></span><br><span class="line"> <span class="keyword">let</span> byTransform = <span class="type">CATransform3DScale</span>(fromTransform, randomMaxScale, randomMaxScale, randomMaxScale)</span><br><span class="line"> <span class="keyword">let</span> toTransform = <span class="type">CATransform3DScale</span>(<span class="type">CATransform3DIdentity</span>, randomMinScale, randomMinScale, randomMinScale)</span><br><span class="line"> <span class="keyword">let</span> transformAnim = <span class="type">CAKeyframeAnimation</span>(keyPath: <span class="string">"transform"</span>)</span><br><span class="line"></span><br><span class="line"> transformAnim.values = [</span><br><span class="line"> <span class="type">NSValue</span>(caTransform3D: fromTransform),</span><br><span class="line"> <span class="type">NSValue</span>(caTransform3D: byTransform),</span><br><span class="line"> <span class="type">NSValue</span>(caTransform3D: toTransform)</span><br><span class="line"> ]</span><br><span class="line"></span><br><span class="line"> transformAnim.duration = duration</span><br><span class="line"> transformAnim.timingFunction = <span class="type">CAMediaTimingFunction</span>(name: kCAMediaTimingFunctionEaseOut)</span><br><span class="line"> spark.sparkView.layer.transform = toTransform</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 火花的不透明度</span></span><br><span class="line"> <span class="keyword">let</span> opacityAnim = <span class="type">CAKeyframeAnimation</span>(keyPath: <span class="string">"opacity"</span>)</span><br><span class="line"> opacityAnim.values = [<span class="number">1.0</span>, <span class="number">0.0</span>]</span><br><span class="line"> opacityAnim.keyTimes = [<span class="number">0.95</span>, <span class="number">0.98</span>]</span><br><span class="line"> opacityAnim.duration = duration</span><br><span class="line"> spark.sparkView.layer.opacity = <span class="number">0.0</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 组合动画</span></span><br><span class="line"> <span class="keyword">let</span> groupAnimation = <span class="type">CAAnimationGroup</span>()</span><br><span class="line"> groupAnimation.animations = [positionAnim, transformAnim, opacityAnim]</span><br><span class="line"> groupAnimation.duration = duration</span><br><span class="line"></span><br><span class="line"> <span class="type">CATransaction</span>.setCompletionBlock({</span><br><span class="line"> spark.sparkView.removeFromSuperview()</span><br><span class="line"> })</span><br><span class="line"></span><br><span class="line"> spark.sparkView.layer.add(groupAnimation, forKey: <span class="string">"spark-animation"</span>)</span><br><span class="line"></span><br><span class="line"> <span class="type">CATransaction</span>.commit()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在的代码已经足够让我们在特定的视图上展示烟花了。我又更进了一步,创建了一个 <code>ClassicFireworkController</code> 来处理所有的工作,这样用一行代码就能启动烟花。</p><p>这个烟花控制器还做了另一件事。它可以修改烟花的 <code>zPosition</code>,这样我们可以让烟花一前一后地展示,效果更好看一些。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ClassicFireworkController</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> sparkAnimator: <span class="type">SparkViewAnimator</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">ClassicFireworkAnimator</span>()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">createFirework</span><span class="params">(at origin: CGPoint, sparkSize: CGSize, scale: CGFloat)</span></span> -> <span class="type">Firework</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">ClassicFirework</span>(origin: origin, sparkSize: sparkSize, scale: scale)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 让烟花在其源视图的角落附近爆开</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">addFireworks</span><span class="params">(<span class="built_in">count</span> fireworksCount: Int = <span class="number">1</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> sparks sparksCount: Int,</span></span></span><br><span class="line"><span class="function"><span class="params"> around sourceView: UIView,</span></span></span><br><span class="line"><span class="function"><span class="params"> sparkSize: CGSize = CGSize<span class="params">(width: <span class="number">7</span>, height: <span class="number">7</span>)</span></span></span>,</span><br><span class="line"> scale: <span class="type">CGFloat</span> = <span class="number">45.0</span>,</span><br><span class="line"> maxVectorChange: <span class="type">CGFloat</span> = <span class="number">15.0</span>,</span><br><span class="line"> animationDuration: <span class="type">TimeInterval</span> = <span class="number">0.4</span>,</span><br><span class="line"> canChangeZIndex: <span class="type">Bool</span> = <span class="literal">true</span>) {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> superview = sourceView.superview <span class="keyword">else</span> { <span class="built_in">fatalError</span>() }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> origins = [</span><br><span class="line"> <span class="type">CGPoint</span>(x: sourceView.frame.minX, y: sourceView.frame.minY),</span><br><span class="line"> <span class="type">CGPoint</span>(x: sourceView.frame.maxX, y: sourceView.frame.minY),</span><br><span class="line"> <span class="type">CGPoint</span>(x: sourceView.frame.minX, y: sourceView.frame.maxY),</span><br><span class="line"> <span class="type">CGPoint</span>(x: sourceView.frame.maxX, y: sourceView.frame.maxY),</span><br><span class="line"> ]</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> <span class="number">_</span> <span class="keyword">in</span> <span class="number">0</span>..<fireworksCount {</span><br><span class="line"> <span class="keyword">let</span> idx = <span class="type">Int</span>(arc4random_uniform(<span class="type">UInt32</span>(origins.<span class="built_in">count</span>)))</span><br><span class="line"> <span class="keyword">let</span> origin = origins[idx].adding(vector: <span class="keyword">self</span>.randomChangeVector(<span class="built_in">max</span>: maxVectorChange))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> firework = <span class="keyword">self</span>.createFirework(at: origin, sparkSize: sparkSize, scale: scale)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> sparkIndex <span class="keyword">in</span> <span class="number">0</span>..<sparksCount {</span><br><span class="line"> <span class="keyword">let</span> spark = firework.spark(at: sparkIndex)</span><br><span class="line"> spark.sparkView.isHidden = <span class="literal">true</span></span><br><span class="line"> superview.addSubview(spark.sparkView)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> canChangeZIndex {</span><br><span class="line"> <span class="keyword">let</span> zIndexChange: <span class="type">CGFloat</span> = arc4random_uniform(<span class="number">2</span>) == <span class="number">0</span> ? -<span class="number">1</span> : +<span class="number">1</span></span><br><span class="line"> spark.sparkView.layer.zPosition = sourceView.layer.zPosition + zIndexChange</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> spark.sparkView.layer.zPosition = sourceView.layer.zPosition</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">self</span>.sparkAnimator.animate(spark: spark, duration: animationDuration)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">randomChangeVector</span><span class="params">(<span class="built_in">max</span>: CGFloat)</span></span> -> <span class="type">CGVector</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">CGVector</span>(dx: <span class="keyword">self</span>.randomChange(<span class="built_in">max</span>: <span class="built_in">max</span>), dy: <span class="keyword">self</span>.randomChange(<span class="built_in">max</span>: <span class="built_in">max</span>))</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">randomChange</span><span class="params">(<span class="built_in">max</span>: CGFloat)</span></span> -> <span class="type">CGFloat</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">CGFloat</span>(arc4random_uniform(<span class="type">UInt32</span>(<span class="built_in">max</span>))) - (<span class="built_in">max</span> / <span class="number">2.0</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个控制器只做了几件事情。随机选择了一个角落展示烟花。在烟花出现的位置,烟花和火花的数量上增加了一些随机性。然后将火花添加到目标视图上,如果需要的话还会调整 <code>zIndex</code>,最后启动了动画。</p><p>几乎所有的参数都设置了默认参数,所以你可以不管他们。直接通过你的控制器调用这个:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">self</span>.fireworkController.addFireworks(<span class="built_in">count</span>: <span class="number">2</span>, sparks: <span class="number">8</span>, around: button)</span><br></pre></td></tr></table></figure><p>然后,哇!</p><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/classic.gif" alt="classic"></p><p>从这一步起,新添加一个像下面这样的烟花就变得非常简单了。你只需要定义新的轨迹,创建一个新的烟花,并且按照你希望的样子来实现即可。将这些代码放入一个控制器可以让你想在哪里启动烟花都很简单 :) 或者你也可以直接使用这个<em>喷泉烟花</em>,我已经把它放在了我的 github 项目 <a href="https://github.com/tomkowz/fireworks" target="_blank" rel="noopener">tomkowz/fireworks</a> 中。</p><p><img src="http://szulctomasz.com/uploads/programming-blog/post-54/fountain.gif" alt="fountain"></p><h2 id="总结"><a href="#总结" class="headerlink" title="_总结_"></a>_总结_</h2><p>这个动画效果的实现并不简单但也不算很难。通过对问题(在我们的情况下是动画效果)的正确分析,我们可以将其分解成多个小问题,逐个解决然后将其组合在一起。真希望我有机会能够在未来的的项目中使用这个效果🎉</p><p>好啦这就是今天的内容。感谢阅读!</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Tomasz Szulc,<a href="http://szulctomasz.com/programming-blog/2018/09/add-fireworks-and-sparks-to-a-uiview/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09<br>译者:<a href="https://github.com/joeytat" target="_blank" rel="noopener">Joeytat</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;定稿:<a href="undefined">Pancf</a></p>
</blockquote>
<!--此处开始正文-->
</summary>
<category term="Tomasz Szulc" scheme="https://swift.gg/categories/Tomasz-Szulc/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="教程" scheme="https://swift.gg/tags/%E6%95%99%E7%A8%8B/"/>
</entry>
<entry>
<title>Bundles and Packages</title>
<link href="https://swift.gg/2019/07/19/nshipster-bundles-and-packages/"/>
<id>https://swift.gg/2019/07/19/nshipster-bundles-and-packages/</id>
<published>2019-07-19T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.038Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Mattt,<a href="https://nshipster.com/bundles-and-packages/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-12-17<br>译者:<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://bignerdcoding.com/" target="_blank" rel="noopener">BigNerdCoding</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>在这个给予的季节,让我们停下脚步,思考一个现代计算机系统赐予我们的最棒的礼物:抽象。</p><p>在数百万 CPU 晶体管、SSD 扇区和 LCD 像素共同协作下,全球数十亿人能够日常使用计算机和移动设备而对此全然不知。这一切都应归功于像文件,目录,应用和文档这样的抽象。</p><p>这周的 NSHipster,我们将讨论苹果平台上两个重要的抽象:包与包裹。🎁</p><a id="more"></a><hr><p>尽管是不同的概念,包与包裹这两个术语经常会被替换使用。毫无疑问,造成困惑的部分原因出自它们相似的名称,但或许主要原因是许多包恰好也是包裹(反之亦然)。</p><p>在我们深入之前,先定义一下这两个术语:</p><ul><li>包是指具有已知结构的,包含可执行代码,以及代码所需的资源的目录。</li><li>包裹是指在访达中看起来像是文件的目录。</li></ul><p>下图展示了包与包裹之间的关系,将应用、框架包、插件包和文档分别放入一个或多个分类之中:<br><img src="/img/articles/nshipster-bundles-and-packages/packages-and-bundles-diagram-a604d818c7decc7430fffc8642f0743728d2f6be4dfae15b274a599655cd3e40.svg1563524952.9887984" alt="diagram"></p><blockquote><p>如果对两者的区别你依然感到困惑,这个类比或许能帮助你理解:<br>把包裹想象成是一个内容被隐藏的盒子(📦),作为一个独立的实体而存在。这点与包不同,包更像是一个背包(🎒) —— 每一款都有特殊的口袋和隔层用来携带你需要的东西,不同的配置用以决定是带去学校,去工作,还是去健身房。如果某样东西既是包也是包裹,恰似行李(🧳)一般:像盒子一样浑然一体,像背包一样分隔自如。</p></blockquote><h2 id="包(Bundles)"><a href="#包(Bundles)" class="headerlink" title="包(Bundles)"></a>包(Bundles)</h2><p>包为代码和资源的组织提供了特定结构,意在<strong>提升开发者的体验</strong>。这个结构不仅允许预测性的加载代码和资源,同时也支持类似于本地化这样的系统性特性。</p><p>包分属于以下三个类别,每一种都有它自己特殊的结构和要求:</p><ul><li><strong>应用包(App Bundles)</strong>:包含一个能被启动的可执行文件,一个描述可执行文件的 <code>Info.plist</code> 文件,应用图标,启动图片,能被可执行文件调用的接口文件,字符串文件,以及数据文件。</li><li><strong>框架包(Framework Bundles)</strong>:包含动态分享库所需要的代码和资源。</li><li><strong>可加载包(Loadable Bundles)</strong>:类似于插件,包含扩展应用功能的可执行代码和资源。</li></ul><h3 id="访问包内容"><a href="#访问包内容" class="headerlink" title="访问包内容"></a>访问包内容</h3><p>对于应用,playgrounds,以及其它你感兴趣的包来说,都能通过 <code>Bundle.main</code> 进行访问。大多数情况,可以使用 <code>url(forResource:withExtension:)</code>(或它的一种变体)来获取特定资源的路径。</p><p>举例来说,如果应用中包含了一个名叫 <code>Photo.jpg</code> 的文件,用下面的方法能获得访问它的 URL:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Bundle</span>.main.url(forResource: <span class="string">"Photo"</span>, withExtension: <span class="string">"jpg"</span>)</span><br></pre></td></tr></table></figure></p><blockquote><p>如果使用 Asset Catalog,你可以从媒体库(<kbd>⇧</kbd><kbd>⌘</kbd><kbd>M</kbd>)拖拽到编辑器来创建图像。</p></blockquote><p>除此之外,<code>Bundle</code> 提供了一些实例方法和变量来获取标准包内容的位置,返回 URL 或 String 类型的路径:</p><table><thead><tr><th>URL</th><th>Path</th><th>描述</th></tr></thead><tbody><tr><td>executableURL</td><td>executablePath</td><td>可执行文件</td></tr><tr><td>url(forAuxiliaryExecutable:)</td><td>path(forAuxiliaryExecutable:)</td><td>辅助的可执行文件</td></tr><tr><td>resourceURL</td><td>resourcePath</td><td>包含资源的子目录</td></tr><tr><td>sharedFrameworksURL</td><td>sharedFrameworksPath</td><td>包含共享框架的子目录</td></tr><tr><td>privateFrameworksURL</td><td>privateFrameworksPath</td><td>包含私有框架的子目录</td></tr><tr><td>builtInPlugInsURL</td><td>builtInPlugInsPath</td><td>包含插件的子目录</td></tr><tr><td>sharedSupportURL</td><td>sharedSupportPath</td><td>包含共享支援文件的子目录</td></tr><tr><td>appStoreReceiptURL</td><td></td><td>App Store 的收据</td></tr></tbody></table><h3 id="获取应用信息"><a href="#获取应用信息" class="headerlink" title="获取应用信息"></a>获取应用信息</h3><p>所有的应用包都必须有一个包含应用信息的 <code>Info.plist</code> 文件。</p><p><code>bundleURL</code> 和 <code>bundleIdentifier</code> 这样的原数据能够通过 bundle 实例被直接访问。<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> bundle = <span class="type">Bundle</span>.main</span><br><span class="line"></span><br><span class="line">bundle.bundleURL <span class="comment">// "/path/to/Example.app"</span></span><br><span class="line">bundle.bundleIdentifier <span class="comment">// "com.nshipster.example"</span></span><br></pre></td></tr></table></figure></p><p>通过下标能从 <code>infoDictionary</code> 变量获得其他信息(如果信息要展示给用户,请使用 <code>localizedInfoDictionary</code>)。<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">bundle.infoDictionary[<span class="string">"CFBundleName"</span>] <span class="comment">// "Example"</span></span><br><span class="line">bundle.localizedInfoDictionary[<span class="string">"CFBundleName"</span>] <span class="comment">// "Esempio" (`it_IT` locale)</span></span><br></pre></td></tr></table></figure></p><h3 id="获取本地化字符串"><a href="#获取本地化字符串" class="headerlink" title="获取本地化字符串"></a>获取本地化字符串</h3><p>包的存在让本地化变得容易。强制本地化资源的存放位置后,系统便能将加载哪个版本的文件的逻辑从开发者层面抽象出来。</p><p>举个例子,包负责加载应用的本地化字符串。使用 <code>localizedString(forKey:value:table:)</code> 方法就可以获取到这些值。<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> bundle = <span class="type">Bundle</span>.main</span><br><span class="line">bundle.localizedString(forKey: <span class="string">"Hello, %@"</span>,</span><br><span class="line"> value: <span class="string">"Hello, ${username}"</span>,</span><br><span class="line"> table: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure></p><p>然而,通常来说用 <code>NSLocalizedString</code> 会更好,像 <code>genstrings</code> 这样的工具能够自动取出键和注释到 <code>.strings</code> 文件中便于翻译。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">// Terminal</span><br><span class="line">$ find . \( -name "*.swift" ! \ # 找出所有 swift 文件</span><br><span class="line"> ! -path "./Carthage/*" \ # 无视 Carthage 与 CocoaPods 的依赖</span><br><span class="line"> ! -path "./Pods/*"</span><br><span class="line"> \) | \</span><br><span class="line"> tr '\n' '\0' | \ # 替换分隔符</span><br><span class="line"> xargs -0 genstrings -o . \ # 处理带空格的路径</span><br></pre></td></tr></table></figure></p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">NSLocalizedString</span>(<span class="string">"Hello, %@"</span>, comment: <span class="string">"Hello, ${username}"</span>)</span><br></pre></td></tr></table></figure><h2 id="包裹(Packages)"><a href="#包裹(Packages)" class="headerlink" title="包裹(Packages)"></a>包裹(Packages)</h2><p>包裹把相关资源封装和加固成一个独立单元,意在<strong>提升用户体验</strong>。</p><p>满足以下任意一个条件,目录就会被访达认为是包裹:</p><ul><li>目录有类似于 <code>.app</code>,<code>.playground</code> 或 <code>.plugin</code> 等特殊扩展。</li><li>目录有一个被一个应用注册作为文档类型的扩展。</li><li>目录具有有扩展属性,将其指定为包裹。</li></ul><h3 id="访问包裹中的内容"><a href="#访问包裹中的内容" class="headerlink" title="访问包裹中的内容"></a>访问包裹中的内容</h3><p>在访达中,右键展示选中项目的可操作目录。如果选中项目是包裹,“打开”操作下会出现“显示包内容”选项。<br><img src="/img/articles/nshipster-bundles-and-packages/show-package-contents-c7cc72f58a573cb2fbe349e6f76a4ef29d14fbada3cd9b8376fc37979da16bf3.png1563524953.1915808" alt></p><p>点击这个选项会从包裹目录打开一个新的访达窗口。</p><p>当然,也可以通过代码访问包裹中的内容。包裹的类型决定了获取内容的最佳方式:</p><ul><li>如果包裹有包的结构,前文所说的 <code>Bundle</code> 就能轻松胜任。</li><li>如果包裹是一个文档,在 macOS 上使用 <code>NSDocument</code> 或在 iOS 上使用 <code>UIDocument</code> 来访问。</li><li>其他情况下,用 <code>FileWrapper</code> 导航目录,文件和符号链接,用 <code>FileHandler</code> 来读写文件描述。</li></ul><h3 id="判断一个目录是否是包裹"><a href="#判断一个目录是否是包裹" class="headerlink" title="判断一个目录是否是包裹"></a>判断一个目录是否是包裹</h3><p>虽说是由访达决定如何展示文件和目录,大多数的判断会被代理给操作系统以及管理统一类型标识(UTI)的服务。</p><p>如果想要确定一个文件扩展是一个内置系统包裹类型,还是一个被已安装的应用使用的文档类型,调用 Core Services 方法 <code>UTTypeCreatePreferredIdentifierForTag(_:_:_:)</code> 与 <code>UTTypeConformsTo(_:_:)</code> 能满足你的需求:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"><span class="keyword">import</span> CoreServices</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">directoryIsPackage</span><span class="params">(<span class="number">_</span> url: URL)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">let</span> filenameExtension: <span class="type">CFString</span> = url.pathExtension <span class="keyword">as</span> <span class="type">NSString</span></span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> uti = <span class="type">UTTypeCreatePreferredIdentifierForTag</span>(</span><br><span class="line"> kUTTagClassFilenameExtension,</span><br><span class="line"> filenameExtension, <span class="literal">nil</span></span><br><span class="line"> )?.takeRetainedValue()</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="type">UTTypeConformsTo</span>(uti, kUTTypePackage)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> xcode = <span class="type">URL</span>(fileURLWithPath: <span class="string">"/Applications/Xcode.app"</span>)</span><br><span class="line">directoryIsPackage(xcode) <span class="comment">// true</span></span><br></pre></td></tr></table></figure></p><blockquote><p>我们找不到任何描述如何设置所谓的包裹比特(package bit)的文档,但根据 <a href="https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-8A428/Finder.h" target="_blank" rel="noopener">CarbonCore/Finder.h</a>,在 <code>com.apple.FindlerInfo</code> 扩展参数中设置 <code>kHasBundle(0x2000)</code> 标示能够实现:<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">> $ xattr -wx com.apple.FinderInfo /path/to/package \</span><br><span class="line">> 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 \</span><br><span class="line">> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class="line">></span><br></pre></td></tr></table></figure></p></blockquote><hr><p>正如我们看到的那样,并非只有终端用户从抽象中获益 —— 无论是像 Swift 这样的高级编程语言的安全性和表现力,还是像 Foundation 这样的 API 的便利性,作为开发者也可以利用抽象开发出优秀的软件。</p><p>或许我们会抱怨 <a href="https://en.wikipedia.org/wiki/Leaky_abstraction" target="_blank" rel="noopener">抽象泄漏</a> 与 <a href="https://en.wikipedia.org/wiki/Abstraction_inversion" target="_blank" rel="noopener">抽象反转</a> 带来的问题,但重要的是退一步,了解我们每天处理多少有用的抽象,以及它们带给了我们多少可能性。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Mattt,<a href="https://nshipster.com/bundles-and-packages/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-12-17<br>译者:<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://bignerdcoding.com/" target="_blank" rel="noopener">BigNerdCoding</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p>在这个给予的季节,让我们停下脚步,思考一个现代计算机系统赐予我们的最棒的礼物:抽象。</p>
<p>在数百万 CPU 晶体管、SSD 扇区和 LCD 像素共同协作下,全球数十亿人能够日常使用计算机和移动设备而对此全然不知。这一切都应归功于像文件,目录,应用和文档这样的抽象。</p>
<p>这周的 NSHipster,我们将讨论苹果平台上两个重要的抽象:包与包裹。🎁</p>
</summary>
<category term="Swift" scheme="https://swift.gg/categories/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/categories/Swift/NSHipster/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/tags/NSHipster/"/>
</entry>
<entry>
<title>Swift 中的集合(Set)</title>
<link href="https://swift.gg/2019/07/09/Sets-in-Swift/"/>
<id>https://swift.gg/2019/07/09/Sets-in-Swift/</id>
<published>2019-07-09T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.037Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Thomas Hanning,<a href="http://www.thomashanning.com/sets-in-swift/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09-06<br>译者:<a href="https://github.com/rsenjoyer" target="_blank" rel="noopener">rsenjoyer</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>集合(Set)是 Swift 集合类型(collection types)之一,集合用来存储类型相同且没有确定顺序唯一的值。你可以将集合想象成一盒台球:它们在颜色和数量上独一无二,但在盒内是无序的。</p><p><img src="/img/articles/Sets-in-Swift/billiard.jpg1562643187.9223473" alt></p><a id="more"></a><p><em>提示:这篇文章使用的是 Swift 4 和 Xcode 10</em></p><h2 id="创建集合"><a href="#创建集合" class="headerlink" title="创建集合"></a>创建集合</h2><p>创建一个集合非常简单:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> setA: <span class="type">Set</span><<span class="type">String</span>> = [<span class="string">"a"</span>,<span class="string">"b"</span>,<span class="string">"c"</span>]</span><br></pre></td></tr></table></figure><p>在这个例子中,创建一个 <code>String</code> 类型的集合,命名为 <code>setA</code>。它存储着 <code>a</code>、<code>b</code>、<code>c</code> 三个值。与数组相比,集合内元素是无序的。通过编译器的类型推导功能,你也可以像如下方式创建集合:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> setB: <span class="type">Set</span> = [<span class="string">"a"</span>,<span class="string">"b"</span>,<span class="string">"c"</span>]</span><br></pre></td></tr></table></figure><p>同样也可以使用集合的构造器:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> setC = <span class="type">Set</span>([<span class="string">"a"</span>,<span class="string">"b"</span>,<span class="string">"c"</span>])</span><br></pre></td></tr></table></figure><p>跟数组一样,如果使用 <code>let</code> 来定义一个集合,它就是不可变的。使用 <code>var</code>定义的是一个可变集合。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> setD = <span class="type">Set</span>([<span class="string">"a"</span>,<span class="string">"b"</span>])</span><br></pre></td></tr></table></figure><p>稍后我们将了解更多有关可变集合的信息。</p><h2 id="访问集合中的元素"><a href="#访问集合中的元素" class="headerlink" title="访问集合中的元素"></a>访问集合中的元素</h2><p>你可以使用循环来访问集合中的元素:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">for</span> value <span class="keyword">in</span> setA {</span><br><span class="line"> <span class="built_in">print</span>(value)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意:每次运行代码时,循环中值的顺序可能不同。从表面来看,它们像是随机返回一样。</p><h2 id="集合分析"><a href="#集合分析" class="headerlink" title="集合分析"></a>集合分析</h2><p>首先,你可以检查集合是否为空:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="built_in">print</span>(setA.isEmpty)</span><br></pre></td></tr></table></figure><p>也可以获取集合中元素的个数:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="built_in">print</span>(setA.<span class="built_in">count</span>)</span><br></pre></td></tr></table></figure><p>上面的操作对数组同样有效,对集合而言,更加普遍的问题是判断集合中是否包含某个元素。为此,你可以使用 <code>contains</code> 方法:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="built_in">print</span>(setA.<span class="built_in">contains</span>(<span class="string">"a"</span>))</span><br></pre></td></tr></table></figure><h2 id="从集合中添加和删除元素"><a href="#从集合中添加和删除元素" class="headerlink" title="从集合中添加和删除元素"></a>从集合中添加和删除元素</h2><p>你可以向可变集合里面添加和删除元素:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">setD.insert(<span class="string">"c"</span>)</span><br><span class="line">setD.remove(<span class="string">"a"</span>)</span><br></pre></td></tr></table></figure><p>由于集合元素的唯一性,因此只能将同一个元素添加到集合中一次。可以多次使用相同的值调用 <code>insert</code> 方法,但集合不会改变。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> setE: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>]</span><br><span class="line"> </span><br><span class="line">setE.insert(<span class="number">5</span>)</span><br><span class="line">setE.insert(<span class="number">5</span>)</span><br><span class="line">setE.insert(<span class="number">5</span>)</span><br><span class="line"> </span><br><span class="line"><span class="built_in">print</span>(setE) <span class="comment">//[4,5,1,2,3]</span></span><br></pre></td></tr></table></figure><p>和前面所说的一样,上面代码每次执行时输出的顺序可能不同,因为集合元素无序。</p><h2 id="集合比较"><a href="#集合比较" class="headerlink" title="集合比较"></a>集合比较</h2><p>集合间能进行比较。显然,可以比较两个集合是否相等:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> setA: = [“a”, “b”, “<span class="built_in">c</span>”]</span><br><span class="line"><span class="keyword">let</span> setB: = [“a”, “b”, “<span class="built_in">c</span>”]</span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> setA == setB {</span><br><span class="line"> <span class="built_in">print</span>(“the sets are <span class="built_in">equal</span>”)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种情况下,集合是相等的。</p><p>比较两个集合的大小是没有明确的定义,但可以检查一个集合是否是另一个集合的子集:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> intSetA: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">6</span>,<span class="number">7</span>,<span class="number">8</span>,<span class="number">9</span>,<span class="number">0</span>]</span><br><span class="line"><span class="keyword">let</span> intSetB: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line">intSetB.isSubset(of: intSetA) <span class="comment">//true</span></span><br></pre></td></tr></table></figure><p>也可以检查集合是否是另一个集合的真子集。这种情况就是该集合是另一个集合的子集但不想等。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> intSetA: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">6</span>,<span class="number">7</span>,<span class="number">8</span>,<span class="number">9</span>,<span class="number">0</span>]</span><br><span class="line"><span class="keyword">let</span> intSetB: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">6</span>,<span class="number">7</span>,<span class="number">8</span>,<span class="number">9</span>,<span class="number">0</span>]</span><br><span class="line"><span class="keyword">let</span> intSetC: <span class="type">Set</span> = [<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>]</span><br><span class="line"> </span><br><span class="line">intSetB.isSubset(of: intSetA) <span class="comment">//true</span></span><br><span class="line">intSetB.isStrictSubset(of: intSetA) <span class="comment">//false</span></span><br><span class="line">intSetC.isSubset(of: intSetA) <span class="comment">// true</span></span><br><span class="line">intSetC.isStrictSubset(of: intSetA) <span class="comment">//true</span></span><br></pre></td></tr></table></figure><p>与之相对的概念就是超集:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> intSetA: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">6</span>,<span class="number">7</span>,<span class="number">8</span>,<span class="number">9</span>,<span class="number">0</span>]</span><br><span class="line"><span class="keyword">let</span> intSetC: <span class="type">Set</span> = [<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>]</span><br><span class="line">intSetA.isSuperset(of: intSetC) <span class="comment">//true</span></span><br><span class="line">intSetA.isStrictSuperset(of: intSetC) <span class="comment">//true</span></span><br></pre></td></tr></table></figure><p>如果两个集合没有相同的元素,那么就说这两个集合不相交</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> intSetA: <span class="type">Set</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">6</span>,<span class="number">7</span>,<span class="number">8</span>,<span class="number">9</span>,<span class="number">0</span>]</span><br><span class="line"><span class="keyword">let</span> intSetC: <span class="type">Set</span> = [<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>]</span><br><span class="line"><span class="keyword">let</span> intSetD: <span class="type">Set</span> = [<span class="number">13</span>,<span class="number">14</span>,<span class="number">15</span>]</span><br><span class="line"> </span><br><span class="line">intSetA.isDisjoint(with: intSetC) <span class="comment">//false</span></span><br><span class="line">intSetA.isDisjoint(with: intSetD) <span class="comment">//true</span></span><br></pre></td></tr></table></figure><h2 id="集合结合"><a href="#集合结合" class="headerlink" title="集合结合"></a>集合结合</h2><p>你可以将两个集合合并成为一个新集合,新的集合中包含两个集合中所有的元素:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> stringSetA: <span class="type">Set</span> = [<span class="string">"a"</span>,<span class="string">"b"</span>,<span class="string">"c"</span>]</span><br><span class="line"><span class="keyword">let</span> stringSetB: <span class="type">Set</span> = [<span class="string">"c"</span>,<span class="string">"d"</span>,<span class="string">"e"</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> unionSetAB = stringSetA.union(stringSetB)</span><br><span class="line"><span class="built_in">print</span>(unionSetAB) <span class="comment">//["d", "b", "c", "a", "e"]</span></span><br></pre></td></tr></table></figure><p>另一方面,交集就是仅包含两个集合中共同的元素:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> stringSetA: <span class="type">Set</span> = [<span class="string">"a"</span>,<span class="string">"b"</span>,<span class="string">"c"</span>]</span><br><span class="line"><span class="keyword">let</span> stringSetB: <span class="type">Set</span> = [<span class="string">"c"</span>,<span class="string">"d"</span>,<span class="string">"e"</span>]</span><br><span class="line"> </span><br><span class="line"><span class="keyword">let</span> intersectionAB = stringSetA.intersection(stringSetB)</span><br><span class="line"><span class="built_in">print</span>(intersectionAB) <span class="comment">//[“c”]</span></span><br></pre></td></tr></table></figure><h2 id="自定义集合元素类型"><a href="#自定义集合元素类型" class="headerlink" title="自定义集合元素类型"></a>自定义集合元素类型</h2><p>你可以在集合中存储自定义的类型。这种类型可以是类或者结构体。为了能正常使用集合,该类型必须遵循 <code>hashable</code> 协议。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Movie</span>: <span class="title">Hashable</span> </span>{</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">var</span> title: <span class="type">String</span></span><br><span class="line"> <span class="keyword">var</span> year: <span class="type">Int</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">init</span>(title: <span class="type">String</span>, year: <span class="type">Int</span>) {</span><br><span class="line"> <span class="keyword">self</span>.title = title</span><br><span class="line"> <span class="keyword">self</span>.year = year</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">static</span> <span class="function"><span class="keyword">func</span> == <span class="params">(lhs: Movie, rhs: Movie)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">return</span> lhs.title == rhs.title &&</span><br><span class="line"> lhs.year == rhs.year</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">var</span> hashValue: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> title.hashValue ^ year.hashValue</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"><span class="keyword">let</span> terminator = <span class="type">Movie</span>(title: <span class="string">"Terminator"</span>, year: <span class="number">1980</span>)</span><br><span class="line"><span class="keyword">let</span> backToTheFuture = <span class="type">Movie</span>(title: <span class="string">"Back to the Future"</span>, year: <span class="number">1985</span>)</span><br><span class="line"> </span><br><span class="line"><span class="keyword">let</span> movieSetA: <span class="type">Set</span> = [terminator,backToTheFuture]</span><br></pre></td></tr></table></figure><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
关于 Swift 中的集合(Set)
</summary>
<category term="thomashanning" scheme="https://swift.gg/categories/thomashanning/"/>
<category term="Swift,iOS开发,Swift进阶" scheme="https://swift.gg/tags/Swift%EF%BC%8CiOS%E5%BC%80%E5%8F%91%EF%BC%8CSwift%E8%BF%9B%E9%98%B6/"/>
</entry>
<entry>
<title>PhotoKit 的数据模型</title>
<link href="https://swift.gg/2019/07/01/photos-data-model/"/>
<id>https://swift.gg/2019/07/01/photos-data-model/</id>
<published>2019-07-01T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.037Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Ole Begemann,<a href="https://oleb.net/2018/photos-data-model/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09-28<br>译者:<a href="https://github.com/zhangchi25806" target="_blank" rel="noopener">张弛</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>在 iOS 系统中,<a href="https://developer.apple.com/documentation/photokit" target="_blank" rel="noopener">PhotoKit 框架</a> 不仅被系统的照片 App 所使用,同时它也为开发人员访问设备的照片库提供了接口支持。而它的底层则是 <a href="https://developer.apple.com/documentation/coredata" target="_blank" rel="noopener">Core Data</a> 实现的。</p><a id="more"></a><p>至少从这两个地方,你就可以确认这一点:</p><ol><li>编写一个能够访问照片库的应用,并使用 <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/TroubleshootingCoreData.html#//apple_ref/doc/uid/TP40001075-CH26-SW21" target="_blank" rel="noopener"><code>-com.apple.CoreData.SQLDebug 1.</code></a> 来启动这个应用程序。当你访问照片库时,从控制台就可以看到 Core Data 的调试信息。</li><li>如果查看 PhotoKit 框架的 <a href="http://stevenygard.com/projects/class-dump/" target="_blank" rel="noopener">class dump</a>,你将会在主要的类中看到对 <a href="https://developer.apple.com/documentation/coredata/nsmanagedobjectid" target="_blank" rel="noopener"><code>NSManagedObjectID</code></a> 和其他 Core Data 类型的引用,例如, <a href="https://github.com/nst/iOS-Runtime-Headers/blob/fbb634c78269b0169efdead80955ba64eaaa2f21/Frameworks/Photos.framework/PHObject.h" target="_blank" rel="noopener"><code>PHObject</code> 有一个 <code>_objectID:NSManagedObjectID</code> 的 ivar</a>。</li></ol><h2 id="寻找-PhotoKit-的核心数据模型"><a href="#寻找-PhotoKit-的核心数据模型" class="headerlink" title="寻找 PhotoKit 的核心数据模型"></a>寻找 PhotoKit 的核心数据模型</h2><p>为了更好地理解 PhotoKit 框架(特别是它的性能特征),我检查了它的数据模型。我在 Xcode 10.0 应用程序的包内容中找到了一个名为 <code>PhotoLibraryServices.framework / photos.momd / photos-10.0.mom</code> 的文件:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">/Applications/Xcode-10.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/photos.momd/photos-10.0.mom</span><br></pre></td></tr></table></figure><blockquote><p>你可以使用如下的命令来查找 Xcode 中模拟器运行时内的其他 Core Data 模型:</p><p><code>find /Applications/Xcode-10.app -name '*.mom'</code></p></blockquote><h2 id="在-Xcode-中打开已编译的-Core-Data-模型"><a href="#在-Xcode-中打开已编译的-Core-Data-模型" class="headerlink" title="在 Xcode 中打开已编译的 Core Data 模型"></a>在 Xcode 中打开已编译的 Core Data 模型</h2><p><code>.mom</code> 文件是<em>已编译的</em> Core Data 数据模型。Xcode 无法直接打开它,但可以将它<em>导入</em>到另一个 Core Data 模型中。通过如下的步骤,我们就可以在 Xcode 中查看这个模型:</p><ol><li>创建一个新的空项目。因为使用 Xcode 10 在项目之外显示 Core Data 模型包并不是一个好的选择。</li><li>在项目中创建一个全新的“Core Data 数据模型”文件。 这将创建一个 <code>.xcdatamodeld</code> 包。</li><li>打开新数据模型,然后选择 编辑器 > 导入…. ,选择要导入的 <code>.mom</code> 文件。</li></ol><p>不幸的是,编译的模型并不存储 Xcode 的模型编辑器的布局信息,因此你必须手动将编辑器中的实体拖出来一个漂亮的样式中。这花了我几个小时。</p><blockquote><p>温馨提示:你可以使用箭头键(和 shift 键+箭头键)精确定位事物。</p><p>专家提示:请勿点击 ⌘Z 撤消移动操作。对图形的编辑不会被 Xcode 视作一个可撤销的操作,因此 Xcode 可能会撤消一开始的导入操作,这意味着你将丢失所有未保存的工作。</p></blockquote><h2 id="带有良好格式的-PhotoKit-的模型"><a href="#带有良好格式的-PhotoKit-的模型" class="headerlink" title="带有良好格式的 PhotoKit 的模型"></a>带有良好格式的 PhotoKit 的模型</h2><p>这是与 Xcode 10.0(iOS 12.0)捆绑在一起的 <code>photos-10.0.mom</code>:</p><p><img src="https://oleb.net/media/photos-10.0-core-data-model-5974px.png" alt="iOS 12.0 中 PhotoLibraryServices.framework 的 Core Data 数据模型。请下载图片并在本地打开以获得最佳效果。"></p><p>并非所有的内容都能在这张图片中被看到。你可以 <a href="https://github.com/ole/AppleCoreDataModels" target="_blank" rel="noopener">下载完整模型</a> 并在 Xcode 中查看它的一些属性。</p><p>请注意,这不一定是 iOS 上的照片的完整数据模型。更多的一些 Core Data 模型被放置在 <code>PrivateFrameworks/PhotoAnalysis.framework</code> 中。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
PhotoKit 的数据模型
</summary>
<category term="PhotoKit" scheme="https://swift.gg/categories/PhotoKit/"/>
<category term="Ole Begemann" scheme="https://swift.gg/categories/PhotoKit/Ole-Begemann/"/>
<category term="iOS 开发" scheme="https://swift.gg/tags/iOS-%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>将 Swift 序列切分为头部和尾部</title>
<link href="https://swift.gg/2019/06/24/sequence-head-tail/"/>
<id>https://swift.gg/2019/06/24/sequence-head-tail/</id>
<published>2019-06-24T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.036Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Ole Begemann,<a href="https://oleb.net/2018/sequence-head-tail/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-29<br>译者:<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://bignerdcoding.com/" target="_blank" rel="noopener">BigNerdCoding</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>函数式编程语言的一个常用范式是把一个列表切分为头部(第一个元素)和尾部(其余元素)。在 Haskell 中,<a href="https://en.wikibooks.org/wiki/Haskell/Pattern_matching" target="_blank" rel="noopener">x:xs</a> 会匹配非空列表,将头部绑定给变量 x,尾部绑定给 xs。</p><p><a href="https://academy.realm.io/posts/tryswift-rob-napier-swift-legacy-functional-programming/" target="_blank" rel="noopener">Swift 不是一门函数式编程语言</a>。既没有内置的 <code>List</code> 类型,也没有集合的特定匹配语法。<a href="#foot1" id="1"><sup>[1]</sup></a></p><a id="more"></a><h2 id="集合(Collections)"><a href="#集合(Collections)" class="headerlink" title="集合(Collections)"></a>集合(Collections)</h2><p>尽管如此,将 <code>Sequence</code> 或 <code>Collection</code> 切分成头部和尾部偶尔很有用。对于集合来说这很容易:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Collection</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> headAndTail: (head: <span class="type">Element</span>, tail: <span class="type">SubSequence</span>)? {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> head = first <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"> <span class="keyword">return</span> (head, <span class="built_in">dropFirst</span>())</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="keyword">let</span> (firstLetter, remainder) = <span class="string">"Hello"</span>.headAndTail {</span><br><span class="line"> <span class="comment">// firstLetter: Character == "H"</span></span><br><span class="line"> <span class="comment">// remainder: Substring == "ello"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="序列(Sequence)"><a href="#序列(Sequence)" class="headerlink" title="序列(Sequence)"></a>序列(Sequence)</h2><p>对于序列来说却很困难,因为它们可以是单向(single-pass)的:一些序列只能被迭代一次,迭代器会消耗其中的元素。以网络流为例,一旦你从缓冲区里读取了一个字节,操作系统便将它抛弃了。你无法让它重新来一遍。</p><p>一个可能的解决方案是 <a href="https://developer.apple.com/documentation/swift/sequence/2885155-makeiterator" target="_blank" rel="noopener">创建一个迭代器</a> 读取第一个元素,并把当前的迭代器状态包裹进一个新的 <a href="https://developer.apple.com/documentation/swift/anysequence" target="_blank" rel="noopener">AnySequence</a> 实例:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Sequence</span> </span>{</span><br><span class="line"><span class="keyword">var</span> headAndTail: (head: <span class="type">Element</span>, tail: <span class="type">AnySequence</span><<span class="type">Element</span>>)? {</span><br><span class="line"> <span class="keyword">var</span> iterator = makeIterator()</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> head = iterator.next() <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"> <span class="keyword">let</span> tail = <span class="type">AnySequence</span> { iterator }</span><br><span class="line"> <span class="keyword">return</span> (head, tail)</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>以上代码能够实现功能,但不是一个好的通用解决方案,尤其是对满足 <code>Collection</code> 的类型而言。将尾部包进 <code>AnySequence</code> 会是一个 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0234-remove-sequence-subsequence.md#type-erasure-performance" target="_blank" rel="noopener">性能杀手</a>,也不可以使用合适的集合类型 <a href="https://developer.apple.com/documentation/swift/sequence/1641117-subsequence#" target="_blank" rel="noopener">SubSequence</a>。</p><p>为了保护集合的 <code>SubSequence</code> 类型,最好给 <code>Collection</code> 和 <code>Sequence</code> 分别写扩展。(我们也将会看到,这是 Swift 5 所推崇的方案,这点会在后面谈到。)</p><h2 id="保护-SubSequence-类型"><a href="#保护-SubSequence-类型" class="headerlink" title="保护 SubSequence 类型"></a>保护 SubSequence 类型</h2><p>我没有找到一个通用的方案,能够让尾部的 <code>SubSequence</code> 类型完好,也同时能让单向序列正常工作。很感谢 <a href="https://twitter.com/dennisvennink" target="_blank" rel="noopener">Dennis Vennink</a> 能够找出一个解决方案并 <a href="https://twitter.com/dennisvennink/status/1060158576679882753" target="_blank" rel="noopener">分享给我</a>。下面是 <a href="https://gist.github.com/dennisvennink/e8b1921916d3c2f90ab52f47291145ef" target="_blank" rel="noopener">他的代码</a>(我略微对格式进行了修改):<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Sequence</span> </span>{</span><br><span class="line"><span class="keyword">var</span> headAndTail: (head: <span class="type">Element</span>, tail: <span class="type">SubSequence</span>)? {</span><br><span class="line"> <span class="keyword">var</span> first: <span class="type">Element?</span> = <span class="literal">nil</span></span><br><span class="line"> <span class="keyword">let</span> tail = drop(<span class="keyword">while</span>: { element <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">if</span> first == <span class="literal">nil</span> {</span><br><span class="line"> first = element</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> head = first <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> (head, tail)</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>Dennis 的窍门是调用 <a href="https://developer.apple.com/documentation/swift/sequence/2965501-drop" target="_blank" rel="noopener">Sequence.drop(while:)</a>,为尾部保留了 <code>SubSequence</code> 类型,同时在 <code>drop(while:)</code> 内部“捕获”了第一个元素。干得漂亮!</p><h2 id="Swift-5"><a href="#Swift-5" class="headerlink" title="Swift 5"></a>Swift 5</h2><p>上面的代码使用 Swift 4.2。在 Swift 5 中由于序列不再会有关联 <code>SubSequence</code> 类型,只存在于集合中(<a href="https://github.com/apple/swift-evolution/blob/master/proposals/0234-remove-sequence-subsequence.md" target="_blank" rel="noopener">Swift Evolution proposal SE-0234</a>),以上代码会崩溃。<a href="#foot2" id="2"><sup>[2]</sup></a></p><p>这个改变有很多优势,但同样意味着不可能有一种通用的方法能够让 <code>SubSequence</code> 同时对 <code>Sequence</code> 和 <code>Collection</code> 有效。</p><p>相对的,我们把那个简单的解决方案添加给 <code>Collection</code>:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Collection</span> </span>{</span><br><span class="line"><span class="keyword">var</span> headAndTail: (head: <span class="type">Element</span>, tail: <span class="type">SubSequence</span>)? {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> head = first <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"> <span class="keyword">return</span> (head, <span class="built_in">dropFirst</span>())</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>如果我们需要让 <code>Sequence</code> 拥有同样的功能,就需要添加一个独立的扩展,使用新的 <code>DropWhileSequence</code> 作为返回类型的尾部:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Sequence</span> </span>{</span><br><span class="line"><span class="keyword">var</span> headAndTail: (head: <span class="type">Element</span>, tail: <span class="type">DropWhileSequence</span><<span class="type">Self</span>>)? {</span><br><span class="line"> <span class="keyword">var</span> first: <span class="type">Element?</span> = <span class="literal">nil</span></span><br><span class="line"> <span class="keyword">let</span> tail = drop(<span class="keyword">while</span>: { element <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">if</span> first == <span class="literal">nil</span> {</span><br><span class="line"> first = element</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> head = first <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> (head, tail)</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>(实现和之前的代码一样,仅仅改变了返回的类型。)</p><hr><p><a id="foot1" href="#1"><sup>[1]</sup></a>为集合添加一种模式匹配结构作为一个 <a href="https://forums.swift.org/t/review-se-0074-implementation-of-binary-search-functions/2438/9" target="_blank" rel="noopener">可行</a> 的特性已经在论坛多次被 <a href="https://forums.swift.org/t/pattern-matching-with-arrays/4735/3" target="_blank" rel="noopener">提及</a>。有了它,你可以像下面这样将一个有序集合解构成头部和尾部:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> numbers = <span class="number">1</span>...<span class="number">10</span></span><br><span class="line"><span class="keyword">let</span> [head, tail...] = numbers</span><br><span class="line"><span class="comment">// head == 1</span></span><br><span class="line"><span class="comment">// tail == 2...10</span></span><br></pre></td></tr></table></figure><p>在 <code>switch</code> 表达式中会很有用。</p><p><a id="foot2" href="#2"><sup>[2]</sup></a>很遗憾我们被误导性的名字 <code>Sequence</code> 给束缚住了。要将 <code>Collection.SubSequence</code> 重命名成更合适的 <code>Slice</code> 会造成 <a href="https://forums.swift.org/t/rationalizing-sequence-subsequence/17586/13" target="_blank" rel="noopener">严重的代码破坏</a>。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Ole Begemann,<a href="https://oleb.net/2018/sequence-head-tail/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-29<br>译者:<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://bignerdcoding.com/" target="_blank" rel="noopener">BigNerdCoding</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p>函数式编程语言的一个常用范式是把一个列表切分为头部(第一个元素)和尾部(其余元素)。在 Haskell 中,<a href="https://en.wikibooks.org/wiki/Haskell/Pattern_matching" target="_blank" rel="noopener">x:xs</a> 会匹配非空列表,将头部绑定给变量 x,尾部绑定给 xs。</p>
<p><a href="https://academy.realm.io/posts/tryswift-rob-napier-swift-legacy-functional-programming/" target="_blank" rel="noopener">Swift 不是一门函数式编程语言</a>。既没有内置的 <code>List</code> 类型,也没有集合的特定匹配语法。<a href="#foot1" id="1"><sup>[1]</sup></a></p>
</summary>
<category term="Ole Begemann" scheme="https://swift.gg/categories/Ole-Begemann/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
</entry>
<entry>
<title>使用 Swift 实现基于堆的优先级队列</title>
<link href="https://swift.gg/2019/05/06/implementing-a-heap-based-priority-queue-using-swift/"/>
<id>https://swift.gg/2019/05/06/implementing-a-heap-based-priority-queue-using-swift/</id>
<published>2019-05-06T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.036Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:APPCODA EDITORIAL TEAM,<a href="https://appcoda.com/swift-algorithm/" target="_blank" rel="noopener">原文链接</a>,原文日期:2019-01-07<br>译者:<a href="undefined">Roc Zhang</a>;校对:<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>,<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>在计算机科学中,有很多问题可以通过将底层数据结构用优先级队列实现来改善算法的时间复杂度。其中 Dijkstra 的最短路径算法便是一个例子,该算法使用了优先级队列来在图中搜索两个顶点间的最短路径。</p><p>不幸的是,Swift 的标准库中并没有提供优先级队列的默认实现。所以我们将会研究如何自行实现基于堆的优先级队列。</p><a id="more"></a><p>如果你想在自己的 IDE 中一起动手来操作,点击 <a href="https://github.com/JimmyMAndersson/HeapPriorityQueue" target="_blank" rel="noopener">此链接</a> 便可获取到源码。</p><h2 id="什么是优先级队列?"><a href="#什么是优先级队列?" class="headerlink" title="什么是优先级队列?"></a>什么是优先级队列?</h2><p>优先级队列是一种可以对具有相对优先级的对象进行高效排序的数据结构。它会根据队列中每个对象的优先级,一个个将队列中的对象进行排序。</p><p>假设你创建了一系列任务并准备在将来的某个时间点运行它们,利用优先级队列就可以让这些任务按照你预期执行。</p><p>在接下来的文章中,我们将使用堆结构来实现我们的优先级队列。</p><h2 id="什么是堆?"><a href="#什么是堆?" class="headerlink" title="什么是堆?"></a>什么是堆?</h2><p>我们可以把堆看作是每个节点最多只有两个子节点的树,但与树不同的是,向堆中添加新节点时要尽可能往顶层左侧放置。如下图所示:</p><p><img src="https://appcoda.com/wp-content/uploads/2019/01/Conceptual-sketch-of-a-new-node-being-inserted-into-a-heap.png" alt></p><p>同时,堆还具有着一些与节点间相对大小关系相关的特性。一个最小堆(就是我们即将要使用的)有着每一个节点比其子节点都要小的特性。最大堆则正好相反。</p><p>为了能够维持这种性质,我们需要通过一些操作来得到节点的正确位置顺序。当我们插入一个新节点时,先将它放在树的顶层左侧开始的第一个空余可用的位置上。如果在放置后最小堆的性质不成立,则将此节点与它的父节点交换,直到最小堆性质成立为止。下图展示了向一个已有的最小堆中插入数字 2 的情况。</p><p><img src="https://appcoda.com/wp-content/uploads/2019/01/Maintaining-the-min-heap-property-on-insertion.png" alt></p><p>当要把一个对象移出队列时,需限制只从队列的某一端进行操作。在这里我们将通过限定只能删除根节点的方式来实现。当根节点被移除时,会被顶层最右边的节点替代。由于新节点成为根节点后有很大概率会过大,我们将把它向下移动,把它与最小的子节点交换,直到我们恢复最小堆。</p><h2 id="关于实现本身的简短说明"><a href="#关于实现本身的简短说明" class="headerlink" title="关于实现本身的简短说明"></a>关于实现本身的简短说明</h2><p>我们将采用数组来实现一个既快速又节省空间的树结构。这里我不打算过于深入其中的数学运算,但如果你有兴趣的话,可以看一看这个 <a href="https://en.wikipedia.org/wiki/Heap_%28data_structure%29#Implementation" target="_blank" rel="noopener">链接</a>,它解释了数学在其中运用的方式与背后的原因。</p><p>准备好了吗?我们开始吧。</p><h2 id="设计协议"><a href="#设计协议" class="headerlink" title="设计协议"></a>设计协议</h2><p>同往常一样,我们先来定义对象要展示给外部用户怎样的功能。我们以定义协议的方式来完成这件事,稍后再让具体的类来遵循它。我为队列设计的协议如下:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">Queue</span> </span>{</span><br><span class="line"> <span class="keyword">associatedtype</span> <span class="type">DataType</span>: <span class="type">Comparable</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 将一个新元素插入到队列中。</span></span><br><span class="line"><span class="comment"> - 参数 item:要添加的元素。</span></span><br><span class="line"><span class="comment"> - 返回值:插入是否成功。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@discardableResult</span> <span class="function"><span class="keyword">func</span> <span class="title">add</span><span class="params">(<span class="number">_</span> item: DataType)</span></span> -> <span class="type">Bool</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 删除首个元素。</span></span><br><span class="line"><span class="comment"> - 返回值:被移除的元素。</span></span><br><span class="line"><span class="comment"> - 抛出值:QueueError 类型的错误。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@discardableResult</span> <span class="function"><span class="keyword">func</span> <span class="title">remove</span><span class="params">()</span></span> <span class="keyword">throws</span> -> <span class="type">DataType</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 获取到队列中的首个元素,并将其移出队列。</span></span><br><span class="line"><span class="comment"> - 返回值:一个包含队列中首个元素的可选值。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">dequeue</span><span class="params">()</span></span> -> <span class="type">DataType?</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 获取队列中的首个元素,但不将它移出队列。</span></span><br><span class="line"><span class="comment"> - 返回值:一个包含队列中首个元素的可选值。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">peek</span><span class="params">()</span></span> -> <span class="type">DataType?</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 清空队列。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">clear</span><span class="params">()</span></span> -> <span class="type">Void</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该协议明确了我们需要实现的功能,供外部用户调用。协议同样还说明了其中的某一个方法可能会抛出错误,且根据文档我们能够了解到它会是一个 QueueError 类型的错误,因此我们同样也要实现它。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">QueueError</span>: <span class="title">Error</span> </span>{</span><br><span class="line"> <span class="keyword">case</span> noSuchItem(<span class="type">String</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这段代码非常简明扼要:当用户尝试从空队列中删除元素时,我们会抛出上面这样的错误。</p><p>现在所有的准备工作已经完成,让我们开始实现队列本身。</p><h2 id="实现优先级队列"><a href="#实现优先级队列" class="headerlink" title="实现优先级队列"></a>实现优先级队列</h2><p>我们将首先从声明 PriorityQueue 类开始,然后再实现它的初始化方法与存储元素,同时完成一些“有则更好”的方法。代码看起来是这样的:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> 基于堆数据结构的 PriorityQueue 实现。</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PriorityQueue</span><<span class="title">DataType</span>: <span class="title">Comparable</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 队列的存储。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">var</span> queue: <span class="type">Array</span><<span class="type">DataType</span>></span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 当前队列的大小。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">var</span> size: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.queue.<span class="built_in">count</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">init</span>() {</span><br><span class="line"> <span class="keyword">self</span>.queue = <span class="type">Array</span><<span class="type">DataType</span>>()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>你也许注意到了,我们目前还没有实现队列的协议。当我们进行编码时,通常希望事物之间能保持相对分离。并且希望能创建出一个概览从而方便我们去进行查找。有些类可能会逐渐变得非常大,解决这种情况的方法之一是使用扩展作用域。这样,每一个扩展倾向于只做一个任务(比如去遵循一个协议,处理存储与初始化,又或是嵌套类的声明等),事后再去查找时就会容易很多。让我们在这里也尝试使用这种方式。首先,实现一个 Int 类型的私有扩展,这能够帮助我们执行一些预先定义好的索引计算:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="class"><span class="keyword">extension</span> <span class="title">Int</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> leftChild: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> (<span class="number">2</span> * <span class="keyword">self</span>) + <span class="number">1</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">var</span> rightChild: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> (<span class="number">2</span> * <span class="keyword">self</span>) + <span class="number">2</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">var</span> parent: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> (<span class="keyword">self</span> - <span class="number">1</span>) / <span class="number">2</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由于是私有的访问权限,这个扩展只在 PriorityQueue 文件中可用。这里聚集了我们将要使用的获取某个节点的子节点与父节点的计算。这样我们就可以通过调用 .leftChild 属性来方便的获取到左子节点的索引,而不必在实现中去进行一堆的数学运算了,以此类推。</p><p>下面是我们对 Queue 协议的遵循实现:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">PriorityQueue</span>: <span class="title">Queue</span> </span>{</span><br><span class="line"> <span class="meta">@discardableResult</span></span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">func</span> <span class="title">add</span><span class="params">(<span class="number">_</span> item: DataType)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">self</span>.queue.append(item)</span><br><span class="line"> <span class="keyword">self</span>.heapifyUp(from: <span class="keyword">self</span>.queue.<span class="built_in">count</span> - <span class="number">1</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@discardableResult</span></span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">func</span> <span class="title">remove</span><span class="params">()</span></span> <span class="keyword">throws</span> -> <span class="type">DataType</span> {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">self</span>.queue.<span class="built_in">count</span> > <span class="number">0</span> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="type">QueueError</span>.noSuchItem(<span class="string">"Attempt to remove item from an empty queue."</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.popAndHeapifyDown()</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">func</span> <span class="title">dequeue</span><span class="params">()</span></span> -> <span class="type">DataType?</span> {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">self</span>.queue.<span class="built_in">count</span> > <span class="number">0</span> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.popAndHeapifyDown()</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">func</span> <span class="title">peek</span><span class="params">()</span></span> -> <span class="type">DataType?</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.queue.first</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">func</span> <span class="title">clear</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.queue.removeAll()</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 弹出队列中的第一个元素,并通过将根元素移向队尾的方式恢复最小堆排序。</span></span><br><span class="line"><span class="comment"> - 返回值: 队列中的第一个元素。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">popAndHeapifyDown</span><span class="params">()</span></span> -> <span class="type">DataType</span> {</span><br><span class="line"> <span class="keyword">let</span> firstItem = <span class="keyword">self</span>.queue[<span class="number">0</span>]</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">self</span>.queue.<span class="built_in">count</span> == <span class="number">1</span> {</span><br><span class="line"> <span class="keyword">self</span>.queue.remove(at: <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">return</span> firstItem</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">self</span>.queue[<span class="number">0</span>] = <span class="keyword">self</span>.queue.remove(at: <span class="keyword">self</span>.queue.<span class="built_in">count</span> - <span class="number">1</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">self</span>.heapifyDown()</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> firstItem</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 通过将元素移向队头的方式恢复最小堆排序。</span></span><br><span class="line"><span class="comment"> - 参数 index: 要移动的元素的索引值。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">heapifyUp</span><span class="params">(from index: Int)</span></span> {</span><br><span class="line"> <span class="keyword">var</span> child = index</span><br><span class="line"> <span class="keyword">var</span> parent = child.parent</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> parent >= <span class="number">0</span> && <span class="keyword">self</span>.queue[parent] > <span class="keyword">self</span>.queue[child] {</span><br><span class="line"> <span class="built_in">swap</span>(parent, with: child)</span><br><span class="line"> child = parent</span><br><span class="line"> parent = child.parent</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 通过将根元素移向队尾的方式恢复队列的最小堆排序。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">heapifyDown</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> parent = <span class="number">0</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> <span class="literal">true</span> {</span><br><span class="line"> <span class="keyword">let</span> leftChild = parent.leftChild</span><br><span class="line"> <span class="keyword">if</span> leftChild >= <span class="keyword">self</span>.queue.<span class="built_in">count</span> {</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> rightChild = parent.rightChild</span><br><span class="line"> <span class="keyword">var</span> minChild = leftChild</span><br><span class="line"> <span class="keyword">if</span> rightChild < <span class="keyword">self</span>.queue.<span class="built_in">count</span> && <span class="keyword">self</span>.queue[minChild] > <span class="keyword">self</span>.queue[rightChild] {</span><br><span class="line"> minChild = rightChild</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">self</span>.queue[parent] > <span class="keyword">self</span>.queue[minChild] {</span><br><span class="line"> <span class="keyword">self</span>.<span class="built_in">swap</span>(parent, with: minChild)</span><br><span class="line"> parent = minChild</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> 交换存储中位于两处索引值位置的元素。</span></span><br><span class="line"><span class="comment"> - 参数 firstIndex:第一个要交换元素的索引。</span></span><br><span class="line"><span class="comment"> - 参数 secondIndex:第二个要交换元素的索引。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">swap</span><span class="params">(<span class="number">_</span> firstIndex: Int, with secondIndex: Int)</span></span> {</span><br><span class="line"> <span class="keyword">let</span> firstItem = <span class="keyword">self</span>.queue[firstIndex]</span><br><span class="line"> <span class="keyword">self</span>.queue[firstIndex] = <span class="keyword">self</span>.queue[secondIndex]</span><br><span class="line"> <span class="keyword">self</span>.queue[secondIndex] = firstItem</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的内容有点多,你也许会想多读上一两次。其中,最上面是我们先前在协议中所定义好的所有方法,下面则是一些私有的,仅在此类中可用的辅助方法。我已经为这些辅助方法加上了注释,以便你能快速的了解到它们是用来做什么的。此外,记得关注一下先前对 Int 的扩展在这里是如何被使用的。依我看来,这是非常简洁实用的设计。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>现在,我们已经完成了所有 PriorityQueue 所需要的功能。现在我们将添加对 CustomStringConvertible 协议的实现,以便在向 print 函数传入一个队列后能得到一些可阅读的内容:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">PriorityQueue</span>: <span class="title">CustomStringConvertible</span> </span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">var</span> description: <span class="type">String</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.queue.description</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>赞!</p><p>上述就是这次的全部内容了。现在你已经知道了如何去实现一个基于堆数据结构的优先级队列。如果有任何疑问,欢迎发表评论。</p><p>要了解 iOS 开发的更多信息,请查看我之前的文章:<br><a href="https://medium.com/swlh/introduction-to-protocol-oriented-programming-1ff3862f9a3c" target="_blank" rel="noopener">Introduction To Protocol Oriented Programming</a><br><a href="https://medium.com/@JimmyMAndersson/using-swift-extensions-to-clean-up-our-code-1aed32da24bc" target="_blank" rel="noopener">Using Swift Extensions To Clean Up Our Code</a></p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
本文解释了优先级队列、堆等相关概念,并展示了如何使用 Swift 实现基于堆的优先级队列。
</summary>
<category term="AppCoda" scheme="https://swift.gg/categories/AppCoda/"/>
<category term="iOS" scheme="https://swift.gg/categories/AppCoda/iOS/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="Algorithm" scheme="https://swift.gg/tags/Algorithm/"/>
<category term="Data Structure" scheme="https://swift.gg/tags/Data-Structure/"/>
<category term="Priority Queue" scheme="https://swift.gg/tags/Priority-Queue/"/>
</entry>
<entry>
<title>Swift 5 字符串插值-简介</title>
<link href="https://swift.gg/2019/04/22/swift5-stringinterpolation-part1/"/>
<id>https://swift.gg/2019/04/22/swift5-stringinterpolation-part1/</id>
<published>2019-04-22T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.036Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Olivier Halligon,<a href="http://alisoftware.github.io/swift/2018/12/15/swift5-stringinterpolation-part1/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-12-15<br>译者:<a href="https://nemocdz.github.io/" target="_blank" rel="noopener">Nemocdz</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote> <!--此处开始正文--><p> <code>StringInterpolation</code> 协议最初的设计效率低下又不易扩展,为了在后续的版本中能够将其彻底重构,Swift 4 中将该协议标记为废弃。即将在 Swift 5 中亮相的 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md" target="_blank" rel="noopener">SE-0228</a> 提案介绍了一种新的 <code>StringInterpolation</code> 设计,使得 String 有了更大的潜能。</p> <a id="more"></a><p> 在 Swift 的 <code>master</code> 分支里实现之后,就可以下载一个 <a href="https://swift.org/download/#snapshots" target="_blank" rel="noopener">快照</a> 来安装最新的 Swift 5 工具链到 Xcode 中,来尝试全新的 <code>StringInterpolation</code>。让我们来把玩一下。</p><h2 id="全新的-StringInterpolation-设计"><a href="#全新的-StringInterpolation-设计" class="headerlink" title="全新的 StringInterpolation 设计"></a>全新的 StringInterpolation 设计</h2><p> 我强烈建议本篇文章的读者阅读一下 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md" target="_blank" rel="noopener">SE-0228</a> 提案,感受一下新 API 的背后的设计思路和动机。</p><p> 要让一个类型遵循 <code>ExpressibleByStringInterpolation</code>,最基本的你需要:</p><ul><li>让这个类型拥有一个类型为 <code>StringInterpolation</code> 的子类型,这个子类型遵循 <code>StringInterpolationProtocol</code> 并将负责解释插值</li><li>这个子类型仅需要实现 <code>appendLiteral(_ literal: String)</code> 方法,再选择一个或多个你自己想要支持的 <code>appendInterpolation(...)</code> 签名的方法</li><li>这个 <code>StringInterpolation</code> 子类型会作为“构造器”服务于你的主类型,然后编译器会调用那些 <code>append…</code> 方法一步一步地构造对象</li><li>然后你的主类型需要实现 <code>init(stringInterpolation: StringInterpolation)</code> ,它会用上一步的结果来实例化它自己。</li></ul><p>你可以实现任何你喜欢的 <code>appenInterpolation(...)</code> 方法,这意味着你可以任意选择支持什么插值。这是一个带来巨大的可能性的超强功能。</p><p>举个例子,如果你实现了 <code>func appendInterpolation(_ string: String, pad: Int)</code>,那么意味着你将可以用类似这样的插值:<code>"Hello \(name, pad: 10), how are you?"</code> 来构造你的类型。插值只需要匹配你的 <code>StringInterpolation</code> 子类型其中一个支持的 <code>appendInterpolation</code> 方法签名。</p><h2 id="一个简单的例子"><a href="#一个简单的例子" class="headerlink" title="一个简单的例子"></a>一个简单的例子</h2><p>让我用一个简单的例子来演示一下插值是如何运作的。一起来构造一个允许引用 issue 编号和用户的 <code>GitHubComment</code> 类型吧。</p><p>这个例子的目标是做到类似下面的写法:</p> <figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> comment: <span class="type">GitHubComment</span> = <span class="string">"""</span></span><br><span class="line"><span class="string"> See \(issue: 123) where \(user: "alisoftware") explains the steps to reproduce.</span></span><br><span class="line"><span class="string"> """</span></span><br></pre></td></tr></table></figure><p>所以我们该怎么实现它呢?</p><p>首先,让我们声明基本的结构体 <code>struct GitHubComment</code> 并让它遵循 <code>ExpressibleByStringLiteral</code>(因为 <code>ExpressibleByStringInterpolation</code> 继承自这个协议所以我们将它的实现抽离)和 <code>CustomStringConvertible</code>(为了 debug 时友好地在控制台中打印)。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">GitHubComment</span> </span>{</span><br><span class="line"> <span class="keyword">let</span> markdown: <span class="type">String</span></span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">GitHubComment</span>: <span class="title">ExpressibleByStringLiteral</span> </span>{</span><br><span class="line"> <span class="keyword">init</span>(stringLiteral value: <span class="type">String</span>) {</span><br><span class="line"> <span class="keyword">self</span>.markdown = value</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">GitHubComment</span>: <span class="title">CustomStringConvertible</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> description: <span class="type">String</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>.markdown</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后,我们让 <code>GitHubComment</code> 遵循 <code>ExpressibleByStringInterpolation</code>。这意味着在剩下需要实现的功能,将由一个 <code>StringInterpolation</code> 子类型来完成:</p><ul><li><p>首先初始化它自己:<code>init(literalCapacity: Int, interpolationCount: Int)</code> 提供给你保留一部分数据到缓冲区的能力,在一步步构造类型时就会用到这个能力。在这个例子中,我们可以在构造实例的时候,简单用一个 <code>String</code> 并往它上面追加片段,不过这里我采用一个 <code>parts: [String]</code> 来代替,之后再将它组合起来</p></li><li><p>实现 <code>appendLiteral(_ string: String)</code> 逐个追加文本到 <code>parts</code> 里</p></li><li><p>实现 <code>appendInterpolation(user: String)</code> 在遇到 <code>\(user: xxx)</code> 时逐个追加 markdown 表示的用户配置链接</p></li><li><p>实现 <code>appendInterpolation(issue: Int)</code> 逐个追加用 markdown 表示的 issue 链接</p></li><li><p>然后在 <code>GitHubComment</code> 上实现 <code>init(stringInterpolation: StringInterpolation)</code> 将 <code>parts</code> 构造成一个评论</p></li></ul><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">GitHubComment</span>: <span class="title">ExpressibleByStringInterpolation</span> </span>{</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">StringInterpolation</span>: <span class="title">StringInterpolationProtocol</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> parts: [<span class="type">String</span>]</span><br><span class="line"> <span class="keyword">init</span>(literalCapacity: <span class="type">Int</span>, interpolationCount: <span class="type">Int</span>) {</span><br><span class="line"> <span class="keyword">self</span>.parts = []</span><br><span class="line"> <span class="comment">// - literalCapacity 文本片段的字符数 (L)</span></span><br><span class="line"> <span class="comment">// - interpolationCount 插值片段数 (I)</span></span><br><span class="line"> <span class="comment">// 我们预计通常结构会是像 "LILILIL"</span></span><br><span class="line"> <span class="comment">// — e.g. "Hello \(world, .color(.blue))!" — 因此是 2n+1</span></span><br><span class="line"> <span class="keyword">self</span>.parts.reserveCapacity(<span class="number">2</span>*interpolationCount+<span class="number">1</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendLiteral</span><span class="params">(<span class="number">_</span> literal: String)</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.parts.append(literal)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendInterpolation</span><span class="params">(user name: String)</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.parts.append(<span class="string">"[\(name)](https://github.com/\(name))"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendInterpolation</span><span class="params">(issue number: Int)</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.parts.append(<span class="string">"[#\(number)](issues/\(number))"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">init</span>(stringInterpolation: <span class="type">StringInterpolation</span>) {</span><br><span class="line"> <span class="keyword">self</span>.markdown = stringInterpolation.parts.joined()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这就完事了!我们成功了!</p><p>注意,因为那些我们实现了的 <code>appendInterpolation</code> 方法签名,我们允许使用 <code>Hello \(user: "alisoftware")</code> 但不能使用 <code>Hello \(user: 123)</code>,因为 <code>appendInterpolation(user:)</code> 期望一个 <code>String</code> 作为形参。类似的是,在你的字符串中 <code>\(issue: 123)</code> 只能允许一个 <code>Int</code> 因为 <code>appendInterpolation(issue:)</code> 采用一个 <code>Int</code> 作为形参。</p><p>实际上,如果你尝试在你的 <code>StringInterpolation</code> 子类中用不支持的插值,编译器会给你提示报错:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> comment: <span class="type">GitHubComment</span> = <span class="string">"""</span></span><br><span class="line"><span class="string">See \(issue: "bob") where \(username: "alisoftware") explains the steps to reproduce.</span></span><br><span class="line"><span class="string">"""</span></span><br><span class="line"><span class="comment">// ^~~~~ ^~~~~~~~~</span></span><br><span class="line"><span class="comment">// 错误: 无法转换 ‘String’ 类型的值到期望的形参类型 ‘Int’</span></span><br><span class="line"><span class="comment">// 错误: 调用 (have 'username:', expected 'user:')实参标签不正确</span></span><br></pre></td></tr></table></figure><h2 id="这仅仅只是个开始"><a href="#这仅仅只是个开始" class="headerlink" title="这仅仅只是个开始"></a>这仅仅只是个开始</h2><p>这个新的设计打开了一大串脑洞让你去实现自己的 <code>ExpressibleByStringInterpolation</code> 类型。这些想法包括:</p><ul><li>创建一个 <code>HTML</code> 类型并遵循,你就可以用插值写 HTML </li><li>创建一个 <code>SQLStatement</code> 类型并遵循,你就可以写更简单的 SQL 语句</li><li>用字符串插值支持更多自定义格式,比如在你的插值字符串中用格式化 <code>Double</code> 或者 <code>Date</code> 值</li><li>创建一个 <code>RegEX</code> 类型并遵循,你就可以用花里胡哨的语法写正则表达式</li><li>创建一个 <code>AttributedString</code> 类型并遵循,就可以用字符串插值构建 <code>NSAttributedString</code> </li></ul><p>带来新的字符串插值设计的 <a href="https://github.com/brentdax" target="_blank" rel="noopener">Brent Royal-Gordon</a> 和 <a href="https://github.com/milseman" target="_blank" rel="noopener">Michael Ilseman</a>,提供了更多例子在这个 <a href="https://gist.github.com/brentdax/0b46ce25b7da1049e61b4669352094b6" target="_blank" rel="noopener">要点列表</a> 中。</p><p>我个人尝试了一下支持 <code>NSAttributedString</code> 的实现,并想 <a href="http://alisoftware.github.io/swift/2018/12/16/swift5-stringinterpolation-part2/" target="_blank" rel="noopener">在专门的一篇文章里分享它的初步实现</a>,因为我发现它非常优雅。我们下一篇文章再见!</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Olivier Halligon,<a href="http://alisoftware.github.io/swift/2018/12/15/swift5-stringinterpolation-part1/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-12-15<br>译者:<a href="https://nemocdz.github.io/" target="_blank" rel="noopener">Nemocdz</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p> <code>StringInterpolation</code> 协议最初的设计效率低下又不易扩展,为了在后续的版本中能够将其彻底重构,Swift 4 中将该协议标记为废弃。即将在 Swift 5 中亮相的 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md" target="_blank" rel="noopener">SE-0228</a> 提案介绍了一种新的 <code>StringInterpolation</code> 设计,使得 String 有了更大的潜能。</p>
</summary>
<category term="Crunchy Development" scheme="https://swift.gg/categories/Crunchy-Development/"/>
<category term="Swift 进阶" scheme="https://swift.gg/tags/Swift-%E8%BF%9B%E9%98%B6/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
</entry>
<entry>
<title>在 iOS 11 中使用 Core Bluetooth</title>
<link href="https://swift.gg/2019/04/15/core-bluetooth/"/>
<id>https://swift.gg/2019/04/15/core-bluetooth/</id>
<published>2019-04-15T05:00:00.000Z</published>
<updated>2022-08-26T13:33:55.036Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Andrew Jaffee,<a href="https://appcoda.com/core-bluetooth/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-04-17<br>译者:<a href="https://github.com/dzyding" target="_blank" rel="noopener">灰s</a>;校对:<a href="https://github.com/Cee" target="_blank" rel="noopener">Cee</a>,<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>作为 iOS 开发,我们十分清楚人们都喜欢互通性。我们喜欢通过无线设备与其他人进行沟通这一点是显而易见的。最近,我们开始希望能够与那些曾经被认为是独立的普通设备进行<em>通信</em>。我们开始喜欢,甚至是期望,部分无线设备可以收集并且分析自己的数据(通常称为“可穿戴设备”)。许多设备已经成为我们生活里的一部分,还为还有一个专门的术语来描述它:“Internet of Things” 或者 “IoT”(物联网)。现在地球上有数十亿的无线通讯设备。在这篇教程中,我们将聚焦 IoT 其中的一部分:蓝牙。 </p><p>我将说明蓝牙技术背后的基本概念,以及: </p><ul><li>展示如何精通蓝牙方向的软件开发,从而为你提供巨大的职业机遇 </li><li>提醒你必须去确认在发布一个使用蓝牙技术的应用时是否需要通过“资格审查” </li><li>给你提供 Apple 的<a href="https://developer.apple.com/documentation/corebluetooth" target="_blank" rel="noopener"><strong><em>Core Bluetooth</em></strong></a> 框架概述 (<a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/AboutCoreBluetooth/Introduction.html#//apple_ref/doc/uid/TP40013257-CH1-SW1" target="_blank" rel="noopener"><strong>也可以参阅这里</strong></a>) </li><li>最后,带领你使用 Swift 4 并通过 <em>Core Bluetooth</em> 和一个蓝牙设备来开发一款用于监控心率的 iOS 应用程序</li></ul><blockquote><p>提示:注意跟随阅读文章中包含的超链接。对于开发者这是重要的资料,它确保你完全理解蓝牙的工作方式以及苹果是如何支持蓝牙这种技术的。</p></blockquote><a id="more"></a><h2 id="蓝牙-一项迅速发展的技术"><a href="#蓝牙-一项迅速发展的技术" class="headerlink" title="蓝牙 - 一项迅速发展的技术"></a>蓝牙 - 一项迅速发展的技术</h2><p>在一篇文章中不可能说清楚如何为整个物联网开发软件,但实际上,对所有这些无线设备进行数据分析是很有启发性的 - 实际上是很不可思议的。连接着的东西无处不在并且可以预测这个小东西的增长速度将是惊人的。如果你观察一下我们今天讨论的内容,在“短程段”中,使用如蓝牙和无线网的技术,然后添加上“广域类别”中,使用如电话的技术(比如: CDMA),<a href="https://www.ericsson.com/en/mobility-report/internet-of-things-forecast" target="_blank" rel="noopener"><strong>你将看到</strong></a> ~ 2014 年的 125 亿设备迅速增加到 2022 年预计的 300 亿。 </p><p>蓝牙是一种短距离无线通讯技术的标准化规范。<a href="https://www.bluetooth.com/zh-cn" target="_blank" rel="noopener"><strong>Bluetooth Special Interest Group(蓝牙技术联盟)</strong></a> 管理和保护这种短程无线技术背后的研发、发展还有知识产权。SIG 确保关于蓝牙的制造商,开发者和销售者他们的硬件和软件都是基于标准化规范。 </p><p>根据 Bluetooth SIG 报道,<a href="https://www.bluetooth.com/bluetooth-technology" target="_blank" rel="noopener"><strong>“今年有将近 40 亿台设备使用蓝牙进行连接。蓝牙将连接手机、平板电脑、个人电脑,蓝牙将会将我们彼此连接。”</strong></a>。一家对短程通讯技术进行深度投资的公司 Ellisys 对此表示认同,并 <a href="https://globenewswire.com/news-release/2018/02/22/1379920/0/en/Ellisys-Increases-Support-for-Bluetooth-Mesh-Networking-on-Protocol-Solutions.html" target="_blank" rel="noopener"><strong>“预估 2018 年将有近 40 亿台新的蓝牙设备上市”</strong></a>。请记住,<em>仅在今年</em>就有 40 亿<em>新</em>蓝牙设备上市。 </p><p>根据这个趋势,一家收集“市场和消费数据”的公司 Statista 认为全球的蓝牙设备 <a href="https://www.statista.com/statistics/283638/installed-base-forecast-bluetooth-enabled-devices-2012-2018/" target="_blank" rel="noopener"><strong>将从 2012 年的 35 亿增长到 2018 年预估的 100 亿</strong></a>。</p><h2 id="对于你的职业生涯,蓝牙意味着什么"><a href="#对于你的职业生涯,蓝牙意味着什么" class="headerlink" title="对于你的职业生涯,蓝牙意味着什么"></a>对于你的职业生涯,蓝牙意味着什么</h2><p>Dogtown Media 有限责任公司,一家 iOS 端“物联网蓝牙应用”精品开发商,该公司声称 <a href="http://www.dogtownmedia.com/app-development-services/internet-of-things-bluetooth-app-development/" target="_blank" rel="noopener"><strong>“根据麦肯锡全球研究所(McKinsey Global Institute)的专家预测,在未来 9 年内,物联网将对全球经济产生超过 6 万亿美元的影响”</strong></a> 。这对于像你我这样的 iOS 开发意味着什么?Dogtown 说 <a href="http://www.dogtownmedia.com/app-development-services/internet-of-things-bluetooth-app-development/" target="_blank" rel="noopener"><strong>“未来几年,对那些有远见的初创企业和创业者来说,将是令人兴奋的、多产的,而且非常有利可图的。”</strong></a><br>翻译:作为一个有前瞻性或者想创业的青年,应该学习使用蓝牙来进行应用程序的开发,因为在这个迅速扩大的市场,你的下个任务或者岗位有很大可能需要这个技能。 </p><p><strong>免责声明</strong>: </p><blockquote><ul><li><em>我与 Dogtown Media, LLC 之间没有任何的从属关系。在搜索到这篇文章的期间,我发现了这家公司的网站,看到他们专门从事 iOS 端的蓝牙开发。</em> </li><li><em>我是 Bluetooth SIG 的一名 “Adopter” 级别成员。</em> </li></ul></blockquote><h2 id="在提交你使用-Core-Bluetooth-开发的应用程序被审核之前"><a href="#在提交你使用-Core-Bluetooth-开发的应用程序被审核之前" class="headerlink" title="在提交你使用 Core Bluetooth 开发的应用程序被审核之前"></a>在提交你使用 Core Bluetooth 开发的应用程序被审核之前</h2><p>在蓝牙技术刚展露头角之际,我经常看到开发者们找一些参考资料,然后立即投入到涉及无线设备的应用开发中,并提交蓝牙应用到 Apple 的 AppStore 中。我想说:别那么快,伙计。 </p><p>Bluetooth SIG 规定,<a href="https://www.bluetooth.com/develop-with-bluetooth/qualification-listing" target="_blank" rel="noopener"><strong>“所有使用蓝牙技术的产品必须完成 Bluetooth Qualification Process(蓝牙资格审核)。”</strong></a> 我听到有人说,“市面上有太多基于蓝牙的应用;没有人会注意到我的”。呃,并不是这样。蓝牙技术有 <a href="https://www.bluetooth.com/about-us/governing-documents" target="_blank" rel="noopener"><strong>版权,专利,并且授权</strong></a> 给应用开发者。如果你想让你的应用程序被聚焦并且展示你集成了蓝牙技术的事实,请记住: </p><blockquote><p><em>Bluetooth</em> 商标 - 包括 BLUETOOTH 文字商标,图形商标(符文 B 和椭圆形设计),还有组合商标(蓝牙文字商标和设计)- 这些都被 Bluetooth SIG 所拥有。只有 Bluetooth SIG 的成员并且拥有对应资格和申报过的产品才可以展示,相关功能或者使用任何商标。为了保护这些商标,Bluetooth SIG 管理了一套执行程序,监控市场并进行审核,以确保会员使用商标的行为符合蓝牙品牌指南,并确保最终发布的产品与已通过资格审查程序的商品和服务相对应。</p></blockquote><p>来看一下 Bluetooth SIG 的 <a href="https://www.bluetooth.com/develop-with-bluetooth/qualification-listing" target="_blank" rel="noopener"><strong>资质 FAQ</strong></a>:</p><blockquote><p>如果我没有给我的产品申请相应的资质会怎么样? </p><p>如果你没有给你的产品申请相应的资质,你将成为执法行动的对象。请阅读这里的 <a href="https://www.bluetooth.com/develop-with-bluetooth/qualification-listing" target="_blank" rel="noopener"><strong>更新策略</strong></a>,其中我们概述了升级计划。如果没有采取纠正措施,您的 Bluetooth SIG 会员资质可能被暂停或撤销。</p></blockquote><p>别傻了,别去冒险。最重要的一点是,我们所有人都应该努力追求最高的诚信和诚实,在应该给予信任的时候给予信任,并促进遵守标准,使协同工作成为规范,而不是例外。数千个人贡献了数千个小时的工作和数百万美元用于发展蓝牙的标准和 <a href="http://www.ipwatchdog.com/2015/05/10/evolution-of-technology-bluetooth-the-once-and-future-king/id=57473/" target="_blank" rel="noopener"><strong>多项专利</strong></a>,从而创造了一套明显有用的知识财产。</p><h2 id="别让我吓着你"><a href="#别让我吓着你" class="headerlink" title="别让我吓着你"></a>别让我吓着你</h2><p>人们常常被「商标」、「专利」、「版权」、「资质」、「会员」、等严厉的词语所吓倒,尤其是 “强制执行。”不要开始担心使用蓝牙进行开发的事。<em>加入 Bluetooth SIG!它是免费的!</em> <a href="https://www.bluetooth.com/develop-with-bluetooth/join" target="_blank" rel="noopener"><strong>点击这里</strong></a>,然后:</p><blockquote><p>首先成为一个 Adopter 级别的会员。使用蓝牙技术开发一款产品,会员资格是必须的,Adopter 级别会员拥有以下这些福利:<br>• 根据 <a href="https://www.bluetooth.com/~/media/downloads/pcla%20esign%20version%20version%2011.ashx?la=en" target="_blank" rel="noopener"><strong>Bluetooth Patent/Copyright License Agreement(蓝牙专利/版权许可协议)</strong></a> 使用蓝牙技术生产产品的许可<br>• 根据 <a href="https://www.bluetooth.com/~/media/files/membership/btla.ashx?la=en" target="_blank" rel="noopener"><strong>Bluetooth Trademark License Agreement(蓝牙商标许可协议)</strong></a> 在符合条件的产品上使用蓝牙商标的许可<br>• 能够与数以万计的 Bluetooth SIG 成员建立网络,并在各种各样的行业中合作 — 从芯片制造商到应用程序的开发者,设备制造商和服务提供商<br>• 能够参加 <a href="https://www.bluetooth.com/specifications/working-groups/working-groups-committees" target="_blank" rel="noopener"><strong>SIG 专家组、研究小组和工作组中的子小组</strong></a><br>• 访问诸如 Profile Tuning Suite(PTS)之类的工具,提供协议和协同测试……</p></blockquote><h2 id="成为-Bluetooth-SIG-的一员"><a href="#成为-Bluetooth-SIG-的一员" class="headerlink" title="成为 Bluetooth SIG 的一员"></a>成为 Bluetooth SIG 的一员</h2><p>成为 SIG 的一员 <a href="https://www.bluetooth.com/develop-with-bluetooth/build" target="_blank" rel="noopener"><strong>会包含很多好处</strong></a>。你可以免费使用教育工具包、培训视频、网络研讨会、开发人员论坛、开发人员支持服务、白皮书、产品测试工具,并帮助确保您的应用程序满足国际监管要求(主要是关于 <a href="https://www.fda.gov/MedicalDevices/DigitalHealth/WirelessMedicalDevices/default.htm" target="_blank" rel="noopener"><strong>射频排放</strong></a>)。 </p><p>你只要成为会员就会得到一些曝光。我的公司是它的一个成员,所以在 Bluetooth SIG’s Member Directory 中可以 <a href="https://www.bluetooth.com/develop-with-bluetooth/join/member-directory?q=microIT%20Infrastructure,%20LLC" target="_blank" rel="noopener"><strong>被看到</strong></a>:<br><img src="https://appcoda.com/wp-content/uploads/2018/04/membership.png" alt> </p><p>一旦你开发了一款应用,使其通过 SIG 认证,并获得 Apple App Store 的许可,那么你的产品同时也会被 SIG 公开上市,这时你将获得更多的曝光。 </p><h2 id="对应用程序进行资格认证既简单又便宜"><a href="#对应用程序进行资格认证既简单又便宜" class="headerlink" title="对应用程序进行资格认证既简单又便宜"></a>对应用程序进行资格认证既简单又便宜</h2><p>当你对自己基于 Core Bluetooth 开发的应用程序感到满意,并准备将其提交到 Apple App Store 进行审核,请停下,然后前往 Bluetooth SIG 的网页对你的应用程序进行 <a href="https://www.bluetooth.com/develop-with-bluetooth/qualification-listing" target="_blank" rel="noopener"><strong>认证</strong></a>。SIG 将为您提供一个整洁的 <a href="https://launchstudio.bluetooth.com/" target="_blank" rel="noopener"><strong>“Launch Studio”</strong></a>,它是您用来完成 Bluetooth Qualification Process 的在线工具。” </p><p>对于大多数应用程序,比如我将在本教程中介绍的 “GATT - based Profile Client(app),”认证和上市的费用是 100 美元。花一些精力来确保您的代码符合 Bluetooth 规范和做一些测试,将是非常值得的。最后,可以给你的应用程序印上蓝牙的商标。这个 <a href="https://www.bluetooth.com/develop-with-bluetooth/marketing-branding" target="_blank" rel="noopener"><strong>商标</strong></a> “在全球范围内都是可识别的,消费者认知度高达92%。” </p><p>请不要担心 100 美元的问题。你更有可能获得一份拥有丰厚薪水或者时薪的工作,并为公司处理这些蓝牙的合规问题。</p><h2 id="理解-Core-Bluetooth"><a href="#理解-Core-Bluetooth" class="headerlink" title="理解 Core Bluetooth"></a>理解 Core Bluetooth</h2><p>大多数情况下,使用蓝牙设备是非常简单的。开发与蓝牙通讯的软件却有可能非常复杂。这就是为什么 Apple 创造了 <a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothOverview/CoreBluetoothOverview.html#//apple_ref/doc/uid/TP40013257-CH2-SW1" target="_blank" rel="noopener"><strong><em>Core Bluetooth</em></strong> <strong>框架</strong></a>: </p><blockquote><p>Core Bluetooth 框架让您的 iOS 和 Mac 应用程序与蓝牙低能耗设备通信。例如,您的应用程序可以发现、搜索低能量的外围设备还有与之交互,比如心率监视器、数字恒温器,甚至其他 iOS 设备。 </p></blockquote><blockquote><p>该框架是蓝牙 4.0 规范中关于使用低能耗设备的抽象。就是说,它为你,也就是开发者,隐藏了规范中很多底层的细节,使你更容易开发与低能耗设备进行交互的应用程序。因为该框架是基于标准规范的,所有规范中的很多概念和术语被采用了…… </p></blockquote><p>请注意是“低能量设备”。当使用 <em>Core Bluetooth</em> 我们并不是处理如无线扬声器这样的经典蓝牙设备。与这类设备的通讯会很快的耗尽电池能量。<em>Core Bluetooth</em> 是针对“Bluetooth Low Energy”(BLE)的 API,也称为“Bluetooth 4.0”。BLE 使用的电力要少得多,因为它的设计目的是通信少量的数据。BLE 设备的一个很好的例子是心率监测器(HRM)。它几乎每秒钟只发送几个字节的数据。这就是为什么人们可以带着一个 HRM 或者带着他们的 iPhone 跑一个小时,记录跑步期间心率的变化,而看不到电池电量的巨大消耗。注意,随着本文的进行,像 BLE 这种首字母缩略词的数量正在增加。 </p><p>为了我们能够一起流畅的讨论 <em>Core Bluetooth</em> 你需要学习一个新的词汇表。 </p><p>分别从 <a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothOverview/CoreBluetoothOverview.html#//apple_ref/doc/uid/TP40013257-CH2-SW17" target="_blank" rel="noopener"><strong>客户端/服务端和生产者/消费者模型</strong></a> 的角度考虑 BLE 协议。<br><img src="https://appcoda.com/wp-content/uploads/2018/04/central-peripheral.png" alt> </p><h2 id="The-Peripheral(外围设备)"><a href="#The-Peripheral(外围设备)" class="headerlink" title="The Peripheral(外围设备)"></a>The Peripheral(外围设备)</h2><p><em>外围设备</em>是硬件/软件的一部分,就像 HRM。大多数 HRM 设备搜集或/和计算数据,如每分钟心跳、HRM 的电池电量水平、以及所谓的“RR-Interval”。设备传输这些数据到另一个需要它们的实体或实体组。外围设备是<em>服务者</em>和<em>生产者</em>。市场上比较流行的 HRM 有 <a href="https://www.wahoofitness.com/devices/heart-rate-monitors/wahoo-tickr-heart-rate-strap" target="_blank" rel="noopener"><strong>Wahoo TICKR,</strong></a><a href="https://www.polar.com/us-en/products/accessories/h10_heart_rate_sensor" target="_blank" rel="noopener"><strong>Polar H7,</strong></a>和 <a href="https://www.scosche.com/rhythm-plus-heart-rate-monitor-armband" target="_blank" rel="noopener"><strong>Scosche Rhythm+</strong></a>。<br><img src="https://appcoda.com/wp-content/uploads/2018/04/heart-rate-monitor-device.png" alt> </p><p>我将通过编写连接到这三种设备的 Swift 4 代码来展示 BLE 等标准的重要性。 </p><p><strong><em>Core Bluetooth 视角</em></strong> </p><p>来自 <a href="https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate" target="_blank" rel="noopener"><strong>苹果的文档</strong></a>: </p><blockquote><p>CBPeripheralDelegate </p><p><a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral" target="_blank" rel="noopener"><code>CBPeripheral</code></a> 对象的代理必须遵守 <a href="https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate" target="_blank" rel="noopener"><code>CBPeripheralDelegate</code></a> 协议。代理使用这个协议的方法来对一个远程外围设备的服务和属性,进行发现、探索、还有交互方面的监控。这个协议里面没有必须遵守的方法。 </p></blockquote><h2 id="The-Central(中央设备)"><a href="#The-Central(中央设备)" class="headerlink" title="The Central(中央设备)"></a>The Central(中央设备)</h2><p><em>中央设备</em>是硬件/软件的一部分,就像 iPhone、iPad、MacBook、<br>iMac 等。这些设备可以使用应用程序扫描像 HRM 这样的蓝牙外围设备。中央设备是一个<em>客户</em>以及 <em>消费者</em>。它们与 HRM 是连通的,所以它们可以使用从外围设备中取出的像每分钟心跳、电池的电量水平、还有“RR-Interval”这样的数据。中央设备接收这些数据,可以对数据执行增值计算,或者只是通过用户界面显示数据,或者是存储数据以供将来分析、展示,或者是聚合和数据分析(就像统计分析需要足够的数据来确定重要的和有意义的趋势),或其他类似的操作。 </p><p><strong><em>Core Bluetooth 视角</em></strong> </p><p>来自 <a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate" target="_blank" rel="noopener"><strong>苹果的文档</strong></a>: </p><blockquote><p><a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate" target="_blank" rel="noopener"><code>CBCentralManagerDelegate</code></a> 协议定义了方法,<a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanager" target="_blank" rel="noopener"><code>CBCentralManager</code></a> 对象的代理必须遵守它。协议中的可选方法允许代理来监控对外围设备的发现、连接、还有检索。唯一必须实现的方法表明中央设备的可用性,并且当中央设备的状态发生更新时被调用。</p></blockquote><h2 id="通过广播找到外围设备"><a href="#通过广播找到外围设备" class="headerlink" title="通过广播找到外围设备"></a>通过广播找到外围设备</h2><p>如果你的 iPhone 或 iPad 找不到这些外设从而不能连接到它们,那么 HRM 之类的外设就没什么用了。因此,它们不断通过无线频段发送着数据的小片段(包),说着类似这样的话:“嘿,我是 Scosche Rhythm+ 心率检测器;我能提供类似我的穿戴者每分钟心率的功能;我能提供类似我的电池电量水平的信息。”当一个对心率感兴趣的<em>中央设备</em>通过<em>扫描</em>找到了这个<em>外围设备</em>,中央设备将连接到它并且它会停止广播。 </p><p>你可能已经使用过 iPhone -> <em>设置</em> -> <em>蓝牙</em> 来开启或关闭蓝牙(包括传统的和 BLE)。当切换到开启,你可以看到你的 iPhone 扫描设备并与它们建立连接,就像下面我所截的两张图,搜索,并且将我的 iPhone 连接到一个 Scosche Rhythm+ HRM:<br><img src="https://appcoda.com/wp-content/uploads/2018/04/bluetooth-found-ryhthm.png" alt> </p><p>依照 <a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothOverview/CoreBluetoothOverview.html#//apple_ref/doc/uid/TP40013257-CH2-SW17" target="_blank" rel="noopener"><strong>苹果</strong></a> 的说法: </p><blockquote><p>外围设备以广播包的形式广播一些数据。一个广播包是一个相对较小的数据束,其中可能包含外围设备所能提供的有用信息,比如外围设备的名字还有主要功能。例如,数字恒温器可能会广播它能提供房间的当前温度。在 BLE 中,广播是外围设备展示其存在的主要方式。另一方面,中央设备可以扫描和监听任何外围设备,只要这些设备的广播信息是它感兴趣的…… </p></blockquote><p>在这篇教程中,过一会我会向你展示怎样使用 Swift 4 来编码进行外围设备的扫描并连接它们。</p><h2 id="外围设备的各种服务"><a href="#外围设备的各种服务" class="headerlink" title="外围设备的各种服务"></a>外围设备的各种服务</h2><p>服务可能不是你认为的那样。服务<em>描述</em>外围设备提供的主要特性或功能。但它并不是一种具体的测量方法,如每分钟心跳数,而是一种描述从外围设备可以得到的与心脏相关的测量方法的分类。 </p><p>依照 <a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothOverview/CoreBluetoothOverview.html#//apple_ref/doc/uid/TP40013257-CH2-SW17" target="_blank" rel="noopener"><strong>苹果</strong></a> 的说法: </p><blockquote><p>服务是一个数据和相关行为的集合,用于实现设备(或设备的一部分)的功能或特性。比如,心率检测器的一项服务可能是公开来自监测器的心率传感器的心率数据。</p></blockquote><p>具体定义一个蓝牙“服务”,我们应该看看 Bluetooth SIG 的 <a href="https://www.bluetooth.com/specifications/gatt/services" target="_blank" rel="noopener"><strong>“GATT Services(服务)”</strong></a> 列表,这里 GATT 代表 <a href="https://www.bluetooth.com/specifications/gatt" target="_blank" rel="noopener"><strong>“Generic Attributes(通用属性)”</strong></a>。 </p><p>向下滚动服务 <a href="https://www.bluetooth.com/specifications/gatt/services" target="_blank" rel="noopener"><strong>列表</strong></a>,直到你在 <strong>Name(名字)</strong> 列中看到 “Heart Rate”。注意, <strong>Uniform Type Identifier (统一类型标识符)</strong> 对应的是 “org.bluetooth.service.heart_rate”,<strong>Assigned Number(指定编码)</strong> 则是 0x180D。请注意在后面的代码中我们将使用 0x180D 这个值。 </p><p>点击 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“Heart Rate(心率)”</strong></a>,你将打开一个网页,上面用粗体字写着 <strong>Name: Heart Rate</strong>。请注意 <strong>Summary(摘要)</strong> ,“HEART RATE Service(心率服务)公开心率和其他与心率传感器相关的数据,用于健身应用。”向下滚动页面就会发现 <strong>Heart Rate</strong> <em>service</em> 本身并不会提供每分钟跳动的实际心率。这个服务是一个其他数据片段的集合,它们被称为 <em>characteristics(特征)</em>。最后,你会得到一个特征来提供重要数据:心率。 </p><p><strong><em>Core Bluetooth 视角</em></strong> </p><p>来自 <a href="https://developer.apple.com/documentation/corebluetooth/cbservice" target="_blank" rel="noopener"><strong>苹果的文档</strong></a>: </p><blockquote><p><a href="https://developer.apple.com/documentation/corebluetooth/cbservice" target="_blank" rel="noopener"><code>CBService</code></a> 和它的子类 <a href="https://developer.apple.com/documentation/corebluetooth/cbmutableservice" target="_blank" rel="noopener"><code>CBMutableService</code></a> 代表一个外围设备的服务 - 为实现设备(或设备的一部分)的功能或特性而收集的数据和相关行为。<code>CBService</code> 对象特指远程外围设备(使用 <a href="https://developer.apple.com/documentation/corebluetooth/cbperipheral" target="_blank" rel="noopener"><code>CBPeripheral</code></a> 对象来表示)的服务。服务组可能是主要的,也有可能是次要的,可能会包含一个特征组的代码,也有可能会包含一个服务组(代表其他的服务组)。 </p></blockquote><h2 id="外围设备服务的特征"><a href="#外围设备服务的特征" class="headerlink" title="外围设备服务的特征"></a>外围设备服务的特征</h2><p>外围设备的服务常常被分解成更细化但相关的信息。特征通常是我们找到重要信息、真实<em>数据</em>的地方。再次查看 <a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothOverview/CoreBluetoothOverview.html#//apple_ref/doc/uid/TP40013257-CH2-SW17" target="_blank" rel="noopener"><strong>苹果</strong></a> 的说明: </p><blockquote><p>服务本身是由特征或包含的服务(这里指别的服务)组成。特征更详细的提供了外围设备的服务信息。例如,刚才描述的心率服务,可能包含一个描述设备的心率传感器所在目标身体位置的特征和另一个传递心率测量数据的特征。 </p></blockquote><p>让我们继续使用 HRM 作为例子。请返回那个用粗体字写着 <strong>Name: Heart Rate(名字:心率)</strong> 的 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>界面</strong></a>。向下滚动直到你看到 <strong>Service Characteristics(服务特征)</strong>。那是一个包含大量元数据(<em>关于</em>信息的数据)的大表格。请找到 <strong>Heart Rate Measurement(心率测量)</strong> 并点击 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml" target="_blank" rel="noopener"><strong>org.bluetooth.characteristic.heart_rate_measurement</strong></a> 然后审查。稍后我会对这个界面进行解释。 </p><p><strong><em>Core Bluetooth 视角</em></strong><br>来自 <a href="https://developer.apple.com/documentation/corebluetooth/cbcharacteristic" target="_blank" rel="noopener"><strong>苹果的文档</strong></a>: </p><blockquote><p><a href="https://developer.apple.com/documentation/corebluetooth/cbcharacteristic" target="_blank" rel="noopener"><code>CBCharacteristic</code></a> 和它的子类 <a href="https://developer.apple.com/documentation/corebluetooth/cbmutablecharacteristic" target="_blank" rel="noopener"><code>CBMutableCharacteristic</code></a> 代表关于外围设备服务的详细信息。<code>CBCharacteristic</code> 对象特指远程外围设备(远程外围设备使用 <code>CBPeripheral</code> 对象表示)服务的特征。一个特征包含一个单一的值以及任意个描述符来描述这个值。特征的属性描述了如何使用这个特征的值以及如何访问这些描述符。</p></blockquote><h2 id="GATT-规范"><a href="#GATT-规范" class="headerlink" title="GATT 规范"></a>GATT 规范</h2><p>当你使用 <em>Core Bluetooth</em> 开发一款需要与蓝牙外围设备交互的应用程序时,你首先应该前往 Bluetooth SIG 的首页。 </p><p>让我们一起回顾我曾经的经历,那会我在开发一个应用程序,用 HRM 做了各种各样非常好玩的功能。查看 <a href="https://www.bluetooth.com/specifications/gatt" target="_blank" rel="noopener"><strong>GATT Specifications(GATT 技术指标)</strong></a> 部分,然后在 <a href="https://www.bluetooth.com/specifications/gatt/services" target="_blank" rel="noopener"><strong>GATT Services(GATT 服务)</strong></a> 下面找到你需要的外围设备服务。 </p><p>在本文介绍的 HRM 示例中,首先在 <a href="https://www.bluetooth.com/specifications/gatt/services" target="_blank" rel="noopener"><strong>GATT Services(GATT 服务)</strong></a> 界面的 <strong>Name(名字)</strong> 列中找到 “Heart Rate”(也就是一个超链接)项。点击 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“Heart Rate(心率)”</strong></a> 链接并且查看完整的网站。请记住 <strong>Assigned Number(分配符)</strong>(0x180D)然后滑动到底部的 <strong>Service Characteristics(服务特征)</strong> 表。仔细的查看表格并且找到有兴趣的特征。 </p><p>在这个例子中,阅读 <strong>Heart Rate Measurement(心率测量)</strong> 和 <strong>Body Sensor Location(传感器所在身体部位)</strong> 分区,然后点击各自的详细链接,<a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml" target="_blank" rel="noopener"><strong>org.bluetooth.characteristic.heart_rate_measurement</strong></a> 和 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml" target="_blank" rel="noopener"><strong>org.bluetooth.characteristic.body_sensor_location</strong></a>。 </p><p>在 <strong>Heart Rate Measurement(心率测量)</strong> 以及 <strong>Body Sensor Location(传感器所在身体部位)</strong> 界面中,分别记住它们的 <strong>Assigned Number(分配符)</strong>,(0x2A37)和(0x2A38),然后查看界面中的所有信息,以便了解将被发送到该 HRM 应用程序中的蓝牙编码数据结构该如何解译。编写代码时必须把蓝牙编码数据转换成人类可读的格式。 </p><p>随着本教程的深入,我将向你介绍更多细节,特别是当我向你展示,我用来与 BLE HRM 通信的应用程序代码。 </p><p>如果你 <a href="https://www.bluetooth.com/develop-with-bluetooth/join" target="_blank" rel="noopener"><strong>加入</strong></a> Bluetooth SIG ,你可以获得更多关于使用服务和特征进行编程的详细信息。 </p><h2 id="编写-Core-Bluetooth-代码"><a href="#编写-Core-Bluetooth-代码" class="headerlink" title="编写 Core Bluetooth 代码"></a>编写 Core Bluetooth 代码</h2><p>在这次讨论中,我将假设你了解 iOS 应用程序开发的基础知识,包括 Swift 编程语言和 Xcode <em>Single View App</em>(单视图应用程序)模板。测试应用程序的用户界面(UI),包括 Auto Layout(自动布局),代码如下所示,非常简单。 </p><p>我将用一系列步骤来描述代码 — 这些步骤在下面的代码中<em>同样</em>会被解释。因此,在阅读本节中的步骤时,请参阅下面代码中对应的步骤。整个过程基本上是线性的。请记住,其中一些步骤表示回调 — 正在调用的委托方法。 </p><p>在编写应用程序时,我会将 <a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/AboutCoreBluetooth/Introduction.html#//apple_ref/doc/uid/TP40013257-CH1-SW1" target="_blank" rel="noopener"><strong><em>Core Bluetooth</em></strong></a> 组件分解成协议或类 — 例如,将核心功能从 UI 中分离出来。但这段代码的目的是向您展示 <em>Core Bluetooth</em> 如何在最少的干扰下工作。我的注释很简单,而且有实际意义。在一个页面中你只会看到重要部分。 </p><h2 id="示例应用程序样式"><a href="#示例应用程序样式" class="headerlink" title="示例应用程序样式"></a>示例应用程序样式</h2><p>针对这篇文章我所开发的应用程序 UI 极其简单。当应用程序被启动,它开始扫描并尝试匹配一个 HRM。扫描的过程通过 <code>UIActivityIndicatorView</code> 类在屏幕上显示并旋转来表明。当没有匹配上任一 HRM 时,通过一个红色正方形的 <code>UIView</code> 来表明。一旦发现一个 HRM 并初步链接,<code>UIActivityIndicatorView</code> 停止旋转并隐藏,并且红色 <code>UIView</code> 转变为绿色。当 HRM 完全链接并被访问,我会显示 HRM 的品牌型号和穿戴者放置在身体上的预定位置。此时我会开始读取并且显示穿戴者每分钟的心率,大约每秒更新。大多数 HRM 都是每秒发送一次每分钟心率值。我人为地设计了一个心率数字的脉冲动画让应用看起来更有吸引力,但是你看到的是我<em>真实</em>的心率。当 HRM 断开链接,我清空所有的信息文本,将正方形 <code>UIView</code> 转变为红色,显示 <code>UIActivityIndicatorView</code> 并开始旋转,同时再次开始扫描 HRM。 </p><p>以下是我的应用程序在与三个不同品牌的 HRM 匹配运行时的样式 — Scosche Rhythm+,Wahoo TICKR,还有Polar H7: </p><p><img src="https://appcoda.com/wp-content/uploads/2018/04/hrm-demo-1024x433.png" alt> </p><p>Rhythm+ 使用红外光“看”我的静脉以确定心率。TICKR 和 H7 使用电极检测告诉我心跳的电脉冲。</p><h2 id="逐步了解我的代码"><a href="#逐步了解我的代码" class="headerlink" title="逐步了解我的代码"></a>逐步了解我的代码</h2><p>你可以在下一段找到完整的源代码。在这里,我将向你介绍实现步骤。 </p><p><strong>Step 0.00 :</strong> 我必须导入 <code>CoreBluetooth</code> 框架。 </p><p><strong>Step 0.0 :</strong> 指定 GATT 中的 <strong>Assigned Numbers(分配符)</strong> 为常量。我这样做让蓝牙规范的标识符更具可读性和可维护性,针对 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“心率”</strong></a> 服务,其 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml" target="_blank" rel="noopener"><strong>“心率测量”</strong></a> 特征,还有其 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml" target="_blank" rel="noopener"><strong>“身体传感器位置”</strong></a> 特征。 </p><p><strong>Step 0.1 :</strong> 创建一个 <code>UIViewController</code> 的子类 <code>HeartRateMonitorViewController</code>。使 <code>HeartRateMonitorViewController</code> 遵守 <a href="https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate" target="_blank" rel="noopener"><code>**CBCentralManagerDelegate**</code></a> 和 <a href="https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate" target="_blank" rel="noopener"><code>**CBPeripheralDelegate**</code></a> 协议。我使用协议和委托的设计模式,正如我在 AppCoda 文章中 <a href="https://appcoda.com/protocol-oriented-programming/" target="_blank" rel="noopener"><strong>这里</strong></a> 还有 <a href="https://appcoda.com/swift-delegate/" target="_blank" rel="noopener"><strong>这里</strong></a> 分别描述的那样。我们将实现来自两个协议的方法。我们将调用一些 <em>Core Bluetooth</em> 的方法,一些方法将由 <em>Core Bluetooth</em> 为我们调用,以响应我们自己的调用。 </p><p><strong>Step 0.2 :</strong> 我们在 <code>HeartRateMonitorViewController</code> 类中定义实例变量,它们代表 <code>CBCentralManager</code> 和 <code>CBPeripheral</code> 类,所以它们在应用程序的生命周期内都是<em>持续存在</em>的。 </p><p><strong>Step 1 :</strong> 我们为进程在后台创建一个并发队列。我希望 <em>Core Bluetooth</em> 的运行发生在后台。我希望 UI 保持响应。说不定,在一个更复杂的应用程序中,HRM 可能会运行数小时,为用户收集心率数据。用户可能希望使用其他应用程序特性,例如,修改应用程序设置,或者,如果用户正在跑步,并且希望使用 <code>Core Location</code> 来跟踪跑步的路线。因此,在心率数据正在收集和显示的同时,用户可以收集和/或查看他们的地理位置。 </p><p><strong>Step 2 :</strong> 创建用于扫描、连接、管理和从外围设备收集数据的控制中心。这是<em>必要</em>的一步。缺少了控制中心 <em>Core Bluetooth</em> 将无法工作。另一个必要的:由于 <code>HeartRateMonitorViewController</code> 采用了 <code>CBCentralManagerDelegate</code>,我们将 <code>centralManager</code> 的委托属性设置成 <code>HeartRateMonitorViewController</code>(<code>self</code>)。同时我们还为控制中心指定了 <code>DispatchQueue</code>。 </p><p><strong>Step 3.1 :</strong> <code>centralManagerDidUpdateState</code> 方法的调用基于设备的蓝牙状态。理想情况下,我们应该考虑一个场景,在该场景中,用户无意(或故意)在 <code>Settings(设置)</code> 应用程序中关闭蓝牙。我们<em>只能</em>在蓝牙为 <code>.poweredOn</code> 状态时才能扫描外围设备。 </p><p><strong>Step 3.2 :</strong> 控制中心应该扫描感兴趣的外围设备,但<em>前提</em>是设备(如iPhone)开启了蓝牙。还记得上面标题为“通过广播找到外围设备”的部分吗?我们就是这样处理这个调用的。我们的监听<em>只</em>针对正在广播 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>心率</strong></a> 服务(0x180D)的 HRM。我们可以通过添加特定服务的 <code>CBUUIDs</code> 到 <code>serviceUUIDs</code> 数组参数(标记为 <code>withServices</code>),从而达到监听并且连接更多外围设备的目的。例如,在一些健康相关的应用程序中,我们可以监听并连接到 HRM <em>和</em>血压监测器或者 BPM(尽管我们需要再创建一个 <code>CBPeripheral</code> 类的实例变量)。注意,如果我们做了这个调用: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">centralManager?.scanForPeripherals(withServices: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p>我们可以监听范围内<em>所有</em>蓝牙设备的广播。在一些蓝牙功能类的应用程序中它可能有用。 </p><p><strong>Step 4.1 :</strong> 找到这个应用程序可以连接哪些<em>感兴趣的</em>外围设备(HRM)。这个 <code>didDiscover</code> 方法告诉我们,在扫描时,控制中心已经发现了正在广播的 HRM。 </p><p><strong>Step 4.2 :</strong> 我们<em>必须</em>在类的实例变量中保存刚刚发现的外围设备的引用,它将持续存在。如果我们仅仅只是使用了一个局部变量,我们会倒霉的。 </p><p><strong>Step 4.3 :</strong> 因为 <code>HeartRateMonitorViewController</code> 采用了 <code>CBPeripheralDelegate</code> 协议,所以 <code>peripheralHeartRateMonitor</code> 对象必须将它的 <code>delegate</code> 属性设置为 <code>HeartRateMonitorViewController</code>(<code>self</code>)。 </p><p><strong>Step 5 :</strong> 我们在 <code>didDiscover</code> 中告诉控制中心停止扫描以便保护电池寿命。当已经连接的 HRM 或外围设备断开连接时,我们可以再次开启扫描。 </p><p><strong>Step 6 :</strong> 此时还在 <code>didDiscover</code> 中,我们连接到被发现的感兴趣的外围设备,一个 HRM。 </p><p><strong>Step 7 :</strong> <code>didConnect</code> 方法<em>仅仅</em>“当成功与一个外围设备连接时调用。”请注意“成功”这个词。如果你发现一个外围设备但不能连接,那么你需要进行一些调试。请注意我更新了 UI 用来显示我连接了那个外围设备,并表明我已经停止扫描,以及其他一些事情。 </p><p><strong>Step 8 :</strong> 此时还在 <code>didConnect</code> 方法中,我们在外围设备上寻找感兴趣的服务。具体来说,我们希望找到 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>Heart Rate(心率)</strong></a>(0x180D)服务。 </p><p><strong>Step 9 :</strong> 当 <code>didDiscoverServices</code> 方法被调用的时候,说明在我们所连接的外围设备上发现了 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“Heart Rate(心率)”</strong></a> 服务。请记住我们需要寻找感兴趣的<em>特征</em>。这里我对 <strong>Heart Rate(心率)</strong> 服务的所有特征进行了一次循环以找到我接下来要用的那个。如果你前往 Bluetooth SIG 网页中 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“Heart Rate(心率)”</strong></a> 服务对应的页面,滚动到下面标记为 <strong>Service Characteristics(服务特征)</strong> 的分区,就可以查看那三个可用的特征。 </p><p><strong>Step 10 :</strong> <code>didDiscoverCharacteristicsFor service</code> 方法证明我们已经发现了感兴趣的服务中所有的特征。 </p><p><strong>Step 11 :</strong> 首先,我订阅了一个通知 - “read” - 关于感兴趣的 <strong>Body Sensor Location(传感器所在身体部位)</strong> 特征。前往 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“Heart Rate(心率)”</strong></a> 服务的页面,你会发现这个特征被标记为“Read Mandatory。”调用 <code>peripheral.readValue</code> 将会引起 <code>peripheral:didUpdateValueForCharacteristic:error:</code> 方法稍后被调用,所以我可以将这个特征解析成人类语言。其次,我订阅了一个定期通知 — “notify” — 关于感兴趣的 <strong>Heart Rate Measurement(心率测量)</strong> 特征。前往 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml" target="_blank" rel="noopener"><strong>“Heart Rate(心率)”</strong></a> 服务的页面,你会发现这个特征被标记为“Notify Mandatory。”调用 <code>peripheral.setNotifyValue</code> 将会引起 <code>peripheral:didUpdateValueForCharacteristic:error:</code> 方法稍后被调用,并且是几乎<em>每一秒钟</em>触发一次,所以我可以将这个特征解析成人类语言。 </p><p><strong>Step 12 :</strong> 因为我对特征 <strong>Body Sensor Location(传感器所在身体部位)</strong> (0x2A38)订阅了读取值,并且对特征 <strong>Heart Rate Measurement(心率测量)</strong> (0x2A37)订阅了定期获取通知,所以如果它们发送值或者定期更新,我将分别获得这两个二进制值。 </p><p><strong>Step 13 :</strong> 将 BLE <strong>Heart Rate Measurement(心率测量)</strong> 的数据解译成人们可读的格式。前往 GATT 规范的 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml" target="_blank" rel="noopener"><strong>页面</strong></a> 找到这个特征。第一个字节是关于其余数据的元数据 (<strong>标记</strong>)。规范告诉我看第一个字节的最低有效位,<strong>Heart Rate Value Format bit(心率值的标识位)</strong>。如果是 0(zero),每分钟的心跳数将以 <code>UINT8</code> 格式在第二字节。我从来没有遇到过一个 HRM 使用第二个字节以外的任何字节,我在这里演示的三个 HRM 也不例外。这就是为什么我忽略了 <strong>Heart Rate Value Format bit(心率值的标识位)</strong> 值为 1(one)的用例。我看过所有被提到的实现,但从来没有能够测试这些实现。对于我无法重现的情况,我不会发表任何看法。 </p><p><strong>Step 14 :</strong> 将 BLE <strong>Body Sensor Location(传感器所在身体部位)</strong> 的数据解译成人们可读的格式。前往 GATT 规范的 <a href="https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml" target="_blank" rel="noopener"><strong>页面</strong></a> 找到这个特征。这个特征非常简单。将值 1、2、3、4、5、6 或 7 存储在 8 位中。形成的文本字符串与这些值以解译为目的的展示是一样的。 </p><p><strong>Step 15 :</strong> 当一个外围设备从控制中心断开时,采取适当的行动。我更新我的 UI 以及…… </p><p><strong>Step 16 :</strong> 开始扫描,为了发现一个正在广播 <strong>Heart Rate(心率)</strong> 服务(0x180D)的外围设备。 </p><h2 id="我的源代码"><a href="#我的源代码" class="headerlink" title="我的源代码"></a>我的源代码</h2><p>这里是对于我们刚刚所讨论的实现,完整的源代码:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> UIKit</span><br><span class="line"></span><br><span class="line"><span class="comment">// STEP 0.00: 必须导入 CoreBluetooth framework</span></span><br><span class="line"><span class="keyword">import</span> CoreBluetooth</span><br><span class="line"></span><br><span class="line"><span class="comment">// STEP 0.0: 指定 GATT 中的 "Assigned Numbers" 为常量,这样它们会拥有更好的可读性和可维护性</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// MARK: - Core Bluetooth 服务 ID</span></span><br><span class="line"><span class="keyword">let</span> <span class="type">BLE_Heart_Rate_Service_CBUUID</span> = <span class="type">CBUUID</span>(string: <span class="string">"0x180D"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// MARK: - Core Bluetooth 特征 ID</span></span><br><span class="line"><span class="keyword">let</span> <span class="type">BLE_Heart_Rate_Measurement_Characteristic_CBUUID</span> = <span class="type">CBUUID</span>(string: <span class="string">"0x2A37"</span>)</span><br><span class="line"><span class="keyword">let</span> <span class="type">BLE_Body_Sensor_Location_Characteristic_CBUUID</span> = <span class="type">CBUUID</span>(string: <span class="string">"0x2A38"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// STEP 0.1: 这个类同时采用了控制中心和外围设备的委托协议,所以必须遵守这些协议的要求</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HeartRateMonitorViewController</span>: <span class="title">UIViewController</span>, <span class="title">CBCentralManagerDelegate</span>, <span class="title">CBPeripheralDelegate</span> </span>{</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// MARK: - Core Bluetooth 类的成员变量</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 0.2: 分别创建 CBCentralManager 和 CBPeripheral 的实例变量</span></span><br><span class="line"> <span class="comment">// 所以它们在应用程序的生命周期里持续存在</span></span><br><span class="line"> <span class="keyword">var</span> centralManager: <span class="type">CBCentralManager?</span></span><br><span class="line"> <span class="keyword">var</span> peripheralHeartRateMonitor: <span class="type">CBPeripheral?</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// MARK: - UI outlets / 成员变量</span></span><br><span class="line"> </span><br><span class="line"> <span class="meta">@IBOutlet</span> <span class="keyword">weak</span> <span class="keyword">var</span> connectingActivityIndicator: <span class="type">UIActivityIndicatorView!</span></span><br><span class="line"> <span class="meta">@IBOutlet</span> <span class="keyword">weak</span> <span class="keyword">var</span> connectionStatusView: <span class="type">UIView!</span></span><br><span class="line"> <span class="meta">@IBOutlet</span> <span class="keyword">weak</span> <span class="keyword">var</span> brandNameTextField: <span class="type">UITextField!</span></span><br><span class="line"> <span class="meta">@IBOutlet</span> <span class="keyword">weak</span> <span class="keyword">var</span> sensorLocationTextField: <span class="type">UITextField!</span></span><br><span class="line"> <span class="meta">@IBOutlet</span> <span class="keyword">weak</span> <span class="keyword">var</span> beatsPerMinuteLabel: <span class="type">UILabel!</span></span><br><span class="line"> <span class="meta">@IBOutlet</span> <span class="keyword">weak</span> <span class="keyword">var</span> bluetoothOffLabel: <span class="type">UILabel!</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 设置 HealthKit </span></span><br><span class="line"> <span class="keyword">let</span> healthKitInterface = <span class="type">HealthKitInterface</span>()</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// MARK: - UIViewController delegate</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">viewDidLoad</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.viewDidLoad()</span><br><span class="line"> <span class="comment">// 在视图加载完成以后,通常是通过一个 nib,做所有附加的设置。</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 最初,我们在进行扫描并且没有产生连接</span></span><br><span class="line"> connectingActivityIndicator.backgroundColor = <span class="type">UIColor</span>.white</span><br><span class="line"> connectingActivityIndicator.startAnimating()</span><br><span class="line"> connectionStatusView.backgroundColor = <span class="type">UIColor</span>.red</span><br><span class="line"> brandNameTextField.text = <span class="string">"----"</span></span><br><span class="line"> sensorLocationTextField.text = <span class="string">"----"</span></span><br><span class="line"> beatsPerMinuteLabel.text = <span class="string">"---"</span></span><br><span class="line"> <span class="comment">// 以防 Bluetooth 被关闭</span></span><br><span class="line"> bluetoothOffLabel.alpha = <span class="number">0.0</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 1: 为控制中心在后台创建一个并发队列</span></span><br><span class="line"> <span class="keyword">let</span> centralQueue: <span class="type">DispatchQueue</span> = <span class="type">DispatchQueue</span>(label: <span class="string">"com.iosbrain.centralQueueName"</span>, attributes: .concurrent)</span><br><span class="line"> <span class="comment">// STEP 2: 创建用于扫描、连接、管理和从外围设备收集数据的控制中心。</span></span><br><span class="line"> centralManager = <span class="type">CBCentralManager</span>(delegate: <span class="keyword">self</span>, queue: centralQueue)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 从 HKHealthStore 读取心率数据</span></span><br><span class="line"> <span class="comment">// healthKitInterface.readHeartRateData()</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 从 HKHealthStore 读取性别类型</span></span><br><span class="line"> <span class="comment">// healthKitInterface.readGenderType()</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">didReceiveMemoryWarning</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.didReceiveMemoryWarning()</span><br><span class="line"> <span class="comment">// 处理任何可以重新创建的资源</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// MARK: - CBCentralManagerDelegate methods</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// STEP 3.1: 这个方法的调用基于设备的蓝牙状态; </span></span><br><span class="line"> <span class="comment">// 仅在 Bluetooth 为 .poweredOn 时才可以扫描外围设备</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">centralManagerDidUpdateState</span><span class="params">(<span class="number">_</span> central: CBCentralManager)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">switch</span> central.state {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">case</span> .unknown:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Bluetooth status is UNKNOWN"</span>)</span><br><span class="line"> bluetoothOffLabel.alpha = <span class="number">1.0</span></span><br><span class="line"> <span class="keyword">case</span> .resetting:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Bluetooth status is RESETTING"</span>)</span><br><span class="line"> bluetoothOffLabel.alpha = <span class="number">1.0</span></span><br><span class="line"> <span class="keyword">case</span> .unsupported:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Bluetooth status is UNSUPPORTED"</span>)</span><br><span class="line"> bluetoothOffLabel.alpha = <span class="number">1.0</span></span><br><span class="line"> <span class="keyword">case</span> .unauthorized:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Bluetooth status is UNAUTHORIZED"</span>)</span><br><span class="line"> bluetoothOffLabel.alpha = <span class="number">1.0</span></span><br><span class="line"> <span class="keyword">case</span> .poweredOff:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Bluetooth status is POWERED OFF"</span>)</span><br><span class="line"> bluetoothOffLabel.alpha = <span class="number">1.0</span></span><br><span class="line"> <span class="keyword">case</span> .poweredOn:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Bluetooth status is POWERED ON"</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="type">DispatchQueue</span>.main.async { () -> <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">self</span>.bluetoothOffLabel.alpha = <span class="number">0.0</span></span><br><span class="line"> <span class="keyword">self</span>.connectingActivityIndicator.startAnimating()</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 3.2: 扫描我们感兴趣的外围设备</span></span><br><span class="line"> centralManager?.scanForPeripherals(withServices: [<span class="type">BLE_Heart_Rate_Service_CBUUID</span>])</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END switch</span></span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func centralManagerDidUpdateState</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 4.1: 找到这个应用程序可以连接哪些感兴趣的外围设备</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">centralManager</span><span class="params">(<span class="number">_</span> central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : <span class="keyword">Any</span>], rssi RSSI: NSNumber)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">print</span>(peripheral.name!)</span><br><span class="line"> decodePeripheralState(peripheralState: peripheral.state)</span><br><span class="line"> <span class="comment">// STEP 4.2: 必须储存一个外围设备的引用到类的实例变量中</span></span><br><span class="line"> peripheralHeartRateMonitor = peripheral</span><br><span class="line"> <span class="comment">// STEP 4.3: 因为 HeartRateMonitorViewController 采用了 CBPeripheralDelegate 协议,</span></span><br><span class="line"> <span class="comment">// 所以 peripheralHeartRateMonitor 必须设置他的 </span></span><br><span class="line"> <span class="comment">// delegate 属性为 HeartRateMonitorViewController (self)</span></span><br><span class="line"> peripheralHeartRateMonitor?.delegate = <span class="keyword">self</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 5: 停止扫描以保护电池的寿命;当断开链接的时候再次扫描。</span></span><br><span class="line"> centralManager?.stopScan()</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 6: 与已经发现的,感兴趣的外围设备建立连接</span></span><br><span class="line"> centralManager?.connect(peripheralHeartRateMonitor!)</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func centralManager(... didDiscover peripheral</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 7: “当一个与外围设备的连接被成功创建时调用。”</span></span><br><span class="line"> <span class="comment">// 只有当我们知道与外围设备的连接建立成功之后才能前往下一步</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">centralManager</span><span class="params">(<span class="number">_</span> central: CBCentralManager, didConnect peripheral: CBPeripheral)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="type">DispatchQueue</span>.main.async { () -> <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">self</span>.brandNameTextField.text = peripheral.name!</span><br><span class="line"> <span class="keyword">self</span>.connectionStatusView.backgroundColor = <span class="type">UIColor</span>.green</span><br><span class="line"> <span class="keyword">self</span>.beatsPerMinuteLabel.text = <span class="string">"---"</span></span><br><span class="line"> <span class="keyword">self</span>.sensorLocationTextField.text = <span class="string">"----"</span></span><br><span class="line"> <span class="keyword">self</span>.connectingActivityIndicator.stopAnimating()</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 8: 在外围设备上寻找感兴趣的服务</span></span><br><span class="line"> peripheralHeartRateMonitor?.discoverServices([<span class="type">BLE_Heart_Rate_Service_CBUUID</span>])</span><br><span class="line"></span><br><span class="line"> } <span class="comment">// END func centralManager(... didConnect peripheral</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 15: 当一个外围设备断开连接,使用适当的方法</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">centralManager</span><span class="params">(<span class="number">_</span> central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// print("Disconnected!")</span></span><br><span class="line"> </span><br><span class="line"> <span class="type">DispatchQueue</span>.main.async { () -> <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">self</span>.brandNameTextField.text = <span class="string">"----"</span></span><br><span class="line"> <span class="keyword">self</span>.connectionStatusView.backgroundColor = <span class="type">UIColor</span>.red</span><br><span class="line"> <span class="keyword">self</span>.beatsPerMinuteLabel.text = <span class="string">"---"</span></span><br><span class="line"> <span class="keyword">self</span>.sensorLocationTextField.text = <span class="string">"----"</span></span><br><span class="line"> <span class="keyword">self</span>.connectingActivityIndicator.startAnimating()</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 16: 在这个用例中,开始扫描相同或其他的外设,只要它们是 HRM,就可以重新联机</span></span><br><span class="line"> centralManager?.scanForPeripherals(withServices: [<span class="type">BLE_Heart_Rate_Service_CBUUID</span>])</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func centralManager(... didDisconnectPeripheral peripheral</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// MARK: - CBPeripheralDelegate methods</span></span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">peripheral</span><span class="params">(<span class="number">_</span> peripheral: CBPeripheral, didDiscoverServices error: Error?)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> service <span class="keyword">in</span> peripheral.services! {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> service.uuid == <span class="type">BLE_Heart_Rate_Service_CBUUID</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Service: \(service)"</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 9: 在感兴趣的服务中寻找感兴趣的特征</span></span><br><span class="line"> peripheral.discoverCharacteristics(<span class="literal">nil</span>, <span class="keyword">for</span>: service)</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func peripheral(... didDiscoverServices</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 10: 从感兴趣的服务中,确认我们所发现感兴趣的特征</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">peripheral</span><span class="params">(<span class="number">_</span> peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> characteristic <span class="keyword">in</span> service.characteristics! {</span><br><span class="line"> <span class="built_in">print</span>(characteristic)</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> characteristic.uuid == <span class="type">BLE_Body_Sensor_Location_Characteristic_CBUUID</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 11: 订阅关于感兴趣特征的单次通知;</span></span><br><span class="line"> <span class="comment">// “当你使用这个方法去读取特征的值时,外围设备将会调用…… </span></span><br><span class="line"> <span class="comment">// peripheral:didUpdateValueForCharacteristic:error:”</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// Read Mandatory</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> peripheral.readValue(<span class="keyword">for</span>: characteristic)</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> characteristic.uuid == <span class="type">BLE_Heart_Rate_Measurement_Characteristic_CBUUID</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">// STEP 11: 订阅关于感兴趣特征的持续通知;</span></span><br><span class="line"> <span class="comment">// “当你启用特征值的通知时,外围设备调用……</span></span><br><span class="line"> <span class="comment">// peripheral(_:didUpdateValueFor:error:)” </span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// Notify Mandatory</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> peripheral.setNotifyValue(<span class="literal">true</span>, <span class="keyword">for</span>: characteristic)</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END for</span></span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func peripheral(... didDiscoverCharacteristicsFor service</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 12: 每当一个特征值定期更新或者发布一次时,我们都会收到通知;</span></span><br><span class="line"> <span class="comment">// 阅读并解译我们订阅的特征值</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">peripheral</span><span class="params">(<span class="number">_</span> peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> characteristic.uuid == <span class="type">BLE_Heart_Rate_Measurement_Characteristic_CBUUID</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 13: 通常我们需要将 BLE 的数据解析成人类可读的格式</span></span><br><span class="line"> <span class="keyword">let</span> heartRate = deriveBeatsPerMinute(using: characteristic)</span><br><span class="line"> </span><br><span class="line"> <span class="type">DispatchQueue</span>.main.async { () -> <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> </span><br><span class="line"> <span class="type">UIView</span>.animate(withDuration: <span class="number">1.0</span>, animations: {</span><br><span class="line"> <span class="keyword">self</span>.beatsPerMinuteLabel.alpha = <span class="number">1.0</span></span><br><span class="line"> <span class="keyword">self</span>.beatsPerMinuteLabel.text = <span class="type">String</span>(heartRate)</span><br><span class="line"> }, completion: { (<span class="literal">true</span>) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">self</span>.beatsPerMinuteLabel.alpha = <span class="number">0.0</span></span><br><span class="line"> })</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END DispatchQueue.main.async...</span></span><br><span class="line"></span><br><span class="line"> } <span class="comment">// END if characteristic.uuid ==...</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> characteristic.uuid == <span class="type">BLE_Body_Sensor_Location_Characteristic_CBUUID</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// STEP 14: 通常我们需要将 BLE 的数据解析成人类可读的格式</span></span><br><span class="line"> <span class="keyword">let</span> sensorLocation = readSensorLocation(using: characteristic)</span><br><span class="line"></span><br><span class="line"> <span class="type">DispatchQueue</span>.main.async { () -> <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">self</span>.sensorLocationTextField.text = sensorLocation</span><br><span class="line"> }</span><br><span class="line"> } <span class="comment">// END if characteristic.uuid ==...</span></span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func peripheral(... didUpdateValueFor characteristic</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// MARK: - Utilities</span></span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">deriveBeatsPerMinute</span><span class="params">(using heartRateMeasurementCharacteristic: CBCharacteristic)</span></span> -> <span class="type">Int</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> heartRateValue = heartRateMeasurementCharacteristic.value!</span><br><span class="line"> <span class="comment">// 转换为无符号 8 位整数数组</span></span><br><span class="line"> <span class="keyword">let</span> buffer = [<span class="type">UInt8</span>](heartRateValue)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// UInt8: “一个 8 位无符号整数类型。”</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 在缓冲区的第一个字节(8 位)是标记(元数据,用于管理包中其余部分);</span></span><br><span class="line"> <span class="comment">// 如果最低有效位(LSB)是 0,心率(bpm)则是 UInt8 格式,</span></span><br><span class="line"> <span class="comment">// 如果 LSB 是 1,BPM 则是 UInt16</span></span><br><span class="line"> <span class="keyword">if</span> ((buffer[<span class="number">0</span>] &amp; <span class="number">0x01</span>) == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 第二个字节:“心率的格式被设置为 UINT8”</span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"BPM is UInt8"</span>)</span><br><span class="line"> <span class="comment">// 将心率写入 HKHealthStore</span></span><br><span class="line"> <span class="comment">// healthKitInterface.writeHeartRateData(heartRate: Int(buffer[1]))</span></span><br><span class="line"> <span class="keyword">return</span> <span class="type">Int</span>(buffer[<span class="number">1</span>])</span><br><span class="line"> } <span class="keyword">else</span> { <span class="comment">// 我从来没有看到过这个用例,所以我把它留给理论学家去争论</span></span><br><span class="line"> <span class="comment">// 第二个和第三个字节:“心率的格式被设置为 UINT16”</span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"BPM is UInt16"</span>)</span><br><span class="line"> <span class="keyword">return</span> -<span class="number">1</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func deriveBeatsPerMinute</span></span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">readSensorLocation</span><span class="params">(using sensorLocationCharacteristic: CBCharacteristic)</span></span> -> <span class="type">String</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> sensorLocationValue = sensorLocationCharacteristic.value!</span><br><span class="line"> <span class="comment">// 转换为无符号 8 位整数数组</span></span><br><span class="line"> <span class="keyword">let</span> buffer = [<span class="type">UInt8</span>](sensorLocationValue)</span><br><span class="line"> <span class="keyword">var</span> sensorLocation = <span class="string">""</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 只看 8 位</span></span><br><span class="line"> <span class="keyword">if</span> buffer[<span class="number">0</span>] == <span class="number">1</span></span><br><span class="line"> {</span><br><span class="line"> sensorLocation = <span class="string">"Chest"</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> buffer[<span class="number">0</span>] == <span class="number">2</span></span><br><span class="line"> {</span><br><span class="line"> sensorLocation = <span class="string">"Wrist"</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> sensorLocation = <span class="string">"N/A"</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> sensorLocation</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func readSensorLocation</span></span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">decodePeripheralState</span><span class="params">(peripheralState: CBPeripheralState)</span></span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">switch</span> peripheralState {</span><br><span class="line"> <span class="keyword">case</span> .disconnected:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Peripheral state: disconnected"</span>)</span><br><span class="line"> <span class="keyword">case</span> .connected:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Peripheral state: connected"</span>)</span><br><span class="line"> <span class="keyword">case</span> .connecting:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Peripheral state: connecting"</span>)</span><br><span class="line"> <span class="keyword">case</span> .disconnecting:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Peripheral state: disconnecting"</span>)</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> } <span class="comment">// END func decodePeripheralState(peripheralState</span></span><br><span class="line"></span><br><span class="line">} <span class="comment">// END class HeartRateMonitorViewController</span></span><br></pre></td></tr></table></figure></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我希望你喜欢这篇教程。买或者借一个 BLE 设备,然后使用我的代码或自己编写代码来连接它。遵循教程中所有我提供的超链接并且阅读它们。查阅 Bluetooth SIG 的 <a href="https://www.bluetooth.com/" target="_blank" rel="noopener"><strong>网页</strong></a> 以及苹果的 <a href="https://developer.apple.com/documentation/corebluetooth" target="_blank" rel="noopener"><strong><em>Core Bluetooth</em></strong></a>(<a href="https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/AboutCoreBluetooth/Introduction.html#//apple_ref/doc/uid/TP40013257-CH1-SW1" target="_blank" rel="noopener"><strong>这里</strong></a> 也可以看到)框架文档,你一定可以对蓝牙技术有一个概览。 </p><p>感谢阅读。记得享受你的工作。不要忘记,当你的简历上面有蓝牙的经验将是你的职业生涯的一大亮点。 </p><p>作为参考,你可以 <a href="https://github.com/appcoda/HealthKit-and-Bluetooth-HRM" target="_blank" rel="noopener"><strong>在 GitHub 上面查看完整的源代码</strong></a>。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
详细描述了使用 Core Bluetooth 的全流程。包括注册成为 Bluetooth SIG 的会员,以及 GATT 规范的查阅。
</summary>
<category term="appcoda" scheme="https://swift.gg/categories/appcoda/"/>
<category term="教程" scheme="https://swift.gg/tags/%E6%95%99%E7%A8%8B/"/>
</entry>
<entry>
<title>Void</title>
<link href="https://swift.gg/2019/02/25/void/"/>
<id>https://swift.gg/2019/02/25/void/</id>
<published>2019-02-25T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.035Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Mattt,<a href="https://nshipster.com/void/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-10-31<br>译者:<a href="https://github.com/zhongWJ" target="_blank" rel="noopener">zhongWJ</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>从 <a href="https://nshipster.com/nil/" target="_blank" rel="noopener">我们第一篇关于 Objective-C 中的 <code>nil</code> 的文章</a> 到 <a href="https://nshipster.com/never/" target="_blank" rel="noopener">最近对 Swift 中 <code>Never</code> 类型的一瞥</a>,“不存在”一直是 NSHipster 讨论的话题。但今天的文章可能是它们当中充斥着最多如 <a href="https://en.wikipedia.org/wiki/Horror_vacui" target="_blank" rel="noopener">恐怖留白</a> 般细节的 —— 因为我们将目光聚焦在了 Swift 中的 <code>Void</code> 上。</p><a id="more"></a><p><code>Void</code> 是什么?在 Swift 中,它只不过是一个空元组。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typealias</span> <span class="type">Void</span> = ()</span><br></pre></td></tr></table></figure><p>我们使用 <code>Void</code> 时才会开始关注它。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> void: <span class="type">Void</span> = ()</span><br><span class="line">void. <span class="comment">// 没有代码补全提示</span></span><br></pre></td></tr></table></figure><p><code>Void</code> 类型的值没有成员:既没有成员方法,也没有成员变量,甚至连名字都没有。它并不比 <code>nil</code> 多些什么。对于一个空容器,Xcode 不会给我们任何代码补全提示。</p><h2 id="为“不存在”而生之物"><a href="#为“不存在”而生之物" class="headerlink" title="为“不存在”而生之物"></a>为“不存在”而生之物</h2><p>在标准库中,<code>Void</code> 类型最显著和奇特的用法是在 <code>ExpressibleByNilLiteral</code> 协议中。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">ExpressibleByNilLiteral</span> </span>{</span><br><span class="line"> <span class="keyword">init</span>(nilLiteral: ())</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>遵从 <code>ExpressibleByNilLiteral</code> 协议的类型可以用 <code>nil</code> 字面量来初始化。大多数类型并不遵从这个协议,因为用 <code>Optional</code> 来表示值可能不存在会更容易理解。但偶尔你也会碰到 <code>ExpressibleByNilLiteral</code>。</p><p><code>ExpressibleByNilLiteral</code> 的指定构造方法不接收任何实际参数。(假设接收了,那结果会怎么样?)然而,该协议的指定构造方法不能仅仅只是一个空构造方法 <code>init()</code>,因为很多类型用它作为默认构造方法。</p><p>你可以将指定构造方法改为一个返回 <code>nil</code> 的类型方法(Type Method)来尝试解决这个问题,但一些强制内部可见的状态在构造方法外就不能使用了。在这里我们使用一种更好的解决方案,给构造方法增加一个带 <code>Void</code> 参数的 <code>nilLiteral</code> 标签。这巧妙的利用已有的功能来实现非常规的结果。</p><h2 id="如何比较“不存在”之物"><a href="#如何比较“不存在”之物" class="headerlink" title="如何比较“不存在”之物"></a>如何比较“不存在”之物</h2><p>元组以及元类型(例如 <code>Int.Type</code>,<code>Int.self</code> 返回结果),函数类型(例如 <code>(String) -> Bool</code>),existential 类型(例如 <code>Encodable & Decodable</code>)组成了非正式类型。与包含 swift 大部分的正式类型或命名类型不同,非正式类型是相对其他类型来定义的。</p><p>非正式类型不能被扩展。<code>Void</code> 是一个空元组,而由于元组是非正式类型,所以你不能给 <code>Void</code> 添加方法、属性或者遵从协议。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Void</span> </span>{} <span class="comment">// 非正式类型 `Void` 不能被扩展</span></span><br></pre></td></tr></table></figure><p><code>Void</code> 不遵从 <code>Equatable</code>协议,因为它不能这么做。然而当我们调用等于操作符(<code>==</code>)时,它如我们期望的一样运行正确。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">void == void <span class="comment">// true</span></span><br></pre></td></tr></table></figure><p>下面这个全局函数定义在所有正式协议之外,它实现了这个看似矛盾的行为。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> == <span class="params">(lhs: <span class="params">()</span></span></span>, rhs: ()) -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>小于操作符(<code><</code>)也被同样处理,用这种方式来替代 <code>Comparable</code> 协议及其衍生出的其他比较操作符。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> < (lhs: (), rhs: ()) -> <span class="title">Bool</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>Swift 标准库为大小最多为 6 的元组提供了比较函数的实现。然而这是一种 hack 方式。Swift 核心团队在许多时候都显露过想要给元组增加对 <code>Equatable</code> 协议的支持的兴趣,但在实现的时候,并没有讨论过正式的提议。</p></blockquote><h2 id="壳中之鬼"><a href="#壳中之鬼" class="headerlink" title="壳中之鬼"></a>壳中之鬼</h2><p>作为非正式类型,<code>Void</code> 不能被扩展。但 <code>Void</code> 毕竟是一个类型,所以能被当作泛型约束来使用。</p><p>例如,考虑以下单个值的泛型容器:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Wrapper</span><<span class="title">Value</span>> </span>{</span><br><span class="line"> <span class="keyword">let</span> value: <span class="type">Value</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当泛型容器所包装的值的类型本身遵循 <code>Equatable</code> 协议时,利用 Swift 4.1 的杀手锏特性 <a href="https://swift.org/blog/conditional-conformance/" target="_blank" rel="noopener">条件遵循</a>,我们首先可以扩展 <code>Wrapper</code> 让其支持 <code>Equatable</code> 协议。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Wrapper</span>: <span class="title">Equatable</span> <span class="title">where</span> <span class="title">Value</span>: <span class="title">Equatable</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="function"><span class="keyword">func</span> ==<span class="params">(lhs: Wrapper<Value>, rhs: Wrapper<Value>)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">return</span> lhs.value == rhs.value</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>利用同之前一样的技巧,我们可以实现一个接受 <code>Wrapper<Void></code> 参数的 <code>==</code> 全局函数,来达到和 <code>Equatable</code> 协议几乎一样的效果。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> ==<span class="params">(lhs: Wrapper<Void>, rhs: Wrapper<Void>)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在这种情况下,我们就可以比较两个包装了 <code>Void</code> 值的 <code>Wrapper</code>。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">Wrapper</span>(value: void) == <span class="type">Wrapper</span>(value: void) <span class="comment">// true</span></span><br></pre></td></tr></table></figure><p>然而,当我们尝试将这样一个包装值赋值给一个变量时,编译器会生成诡异的错误。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> wrapperOfVoid = <span class="type">Wrapper</span><<span class="type">Void</span>>(value: void)</span><br><span class="line"><span class="comment">// 👻 错误: 不能赋值:</span></span><br><span class="line"><span class="comment">// 由于找不到对应符号,无法销毁 wrapperOfVoid</span></span><br></pre></td></tr></table></figure><p><code>Void</code> 的可怕之处反过来再次自我否定。</p><h2 id="幽灵类型"><a href="#幽灵类型" class="headerlink" title="幽灵类型"></a>幽灵类型</h2><p>即使你不敢提及它的非正式名字,你依然逃不过 <code>Void</code> 的掌心。</p><p>任何没有显式声明返回值的函数会隐式的返回一个 <code>Void</code>。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doSomething</span><span class="params">()</span></span> { ... }</span><br><span class="line"></span><br><span class="line"><span class="comment">// 等同于</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doSomething</span><span class="params">()</span></span> -> <span class="type">Void</span> { ... }</span><br></pre></td></tr></table></figure><p>这个行为很奇怪,但不是特别有用。并且当你将一个返回 <code>Void</code> 类型的函数的返回值赋值给一个变量时,编译器会生成一个警告。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">doSomething() <span class="comment">// 没有警告</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> result = doSomething()</span><br><span class="line"><span class="comment">// ⚠️ 常量 `result` 指向的是一个 `Void` 类型的值,这种行为的结果不可预测</span></span><br></pre></td></tr></table></figure><p>你可以显式指定变量类型为 <code>Void</code> 来消除警告。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> result: <span class="type">Void</span> = doSomething() <span class="comment">// ()</span></span><br></pre></td></tr></table></figure><blockquote><p>相反的,当函数的返回值类型为非 <code>Void</code> 时,你如果不将返回值赋值给其他变量,编译器也会产生警告。更多详情可以参考 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0047-nonvoid-warn.md" target="_blank" rel="noopener">SE-0047 “默认当非 <code>Void</code> 函数返回结果未使用时告警”</a>。</p></blockquote><h2 id="试着从-Void-恢复过来"><a href="#试着从-Void-恢复过来" class="headerlink" title="试着从 Void 恢复过来"></a>试着从 Void 恢复过来</h2><p>如果你斜视 <code>Void?</code>,时间足够长,你可能会将它和 <code>Bool</code> 弄混。这两种类型类似,都仅有两种状态:<code>true</code> / <code>.some(())</code> 以及 <code>false</code> / <code>.none</code>。</p><p>但类似并不意味着一样。它们两最明显的不同是,<code>Bool</code> 遵循 <code>ExpressibleByBooleanLiteral</code> 协议,而 <code>Void</code> 不是也不能遵循 <code>ExpressibleByBooleanLiteral</code> 协议,和它不能遵循 <code>Equatable</code> 协议的原因一样。所以你不能这样做:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">(<span class="literal">true</span> <span class="keyword">as</span> <span class="type">Void?</span>) <span class="comment">// 错误</span></span><br></pre></td></tr></table></figure><blockquote><p><code>Void</code> 可能是 Swift 中最令人毛骨悚的类型了。但是当给 <code>Bool</code> 起一个 <code>Booooooool</code> 别名时, 就和 <code>Void</code> 不相上下了。</p></blockquote><p>但 <code>Void?</code> 硬坳的话是能够表现的像 <code>Bool</code> 一样。比如下面这个随机抛出错误的函数:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Failure</span>: <span class="title">Error</span> </span>{}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">failsRandomly</span><span class="params">()</span></span> <span class="keyword">throws</span> {</span><br><span class="line"> <span class="keyword">if</span> <span class="type">Bool</span>.random() {</span><br><span class="line"> <span class="keyword">throw</span> <span class="type">Failure</span>()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>正确方式是,在一个 <code>do / catch</code> 代码块中用 <code>try</code> 表达式来调用这个函数。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">do</span> {</span><br><span class="line"> <span class="keyword">try</span> failsRandomly()</span><br><span class="line"> <span class="comment">// 成功执行</span></span><br><span class="line">} <span class="keyword">catch</span> {</span><br><span class="line"> <span class="comment">// 失败执行</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>failsRandomly()</code> 隐式返回 <code>Void</code>,利用这一事实可以达到同样效果,虽然不正确但表面上可行。<code>try?</code> 表达式会处理可能抛出异常的语句,将结果包装为一个可选类型值。对于 <code>failsRandomly()</code> 这种情况而言,结果是 <code>Void?</code>。假如 <code>Void?</code> 有 <code>.some</code> 值(即,<code>!= nil</code>),这意味着函数没有出错直接返回。如果 <code>success</code> 是 <code>nil</code>,那我们就知道函数生成了一个错误。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> success: <span class="type">Void?</span> = <span class="keyword">try</span>? failsRandomly()</span><br><span class="line"><span class="keyword">if</span> success != <span class="literal">nil</span> {</span><br><span class="line"> <span class="comment">// 成功执行</span></span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 失败执行</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>很多人可能不喜欢 <code>do / catch</code> 代码块,但你不得不承认,相比这里的代码,<code>do / catch</code> 代码块更加优雅。</p><p>在某些特殊场景下,这种变通方式可能会很有用。例如为了保存每一次自评估闭包执行的副作用,你可以在类上使用静态属性:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">var</span> oneTimeSideEffect: <span class="type">Void?</span> = {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">try</span>? data.write(to: fileURL)</span><br><span class="line">}()</span><br></pre></td></tr></table></figure><p>虽然这样可行,但更好的办法是使用 <code>Error</code> 和 <code>Bool</code> 类型。</p><h2 id="夜晚才会响(”Clang”)的东西"><a href="#夜晚才会响(”Clang”)的东西" class="headerlink" title="夜晚才会响(”Clang”)的东西"></a>夜晚才会响(”Clang”)的东西</h2><p>当读到这么令人发寒的描述时,如果你开始打寒颤了,你可以引导 <code>Void</code> 类型的坏死能量来召唤巨大的热量给自己的精神加热:</p><p>也就是说,通过以下代码让 <code>lldb-rpc-server</code> 全力开启 CPU(译者注:编译器会卡死):</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Optional</span>: <span class="title">ExpressibleByBooleanLiteral</span> <span class="title">where</span> <span class="title">Wrapped</span> == <span class="title">Void</span> </span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">typealias</span> <span class="type">BooleanLiteralType</span> = <span class="type">Bool</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">init</span>(booleanLiteral value: <span class="type">Bool</span>) {</span><br><span class="line"> <span class="keyword">if</span> value {</span><br><span class="line"> <span class="keyword">self</span>.<span class="keyword">init</span>(())!</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">self</span>.<span class="keyword">init</span>(nilLiteral: ())!</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> pseudoBool: <span class="type">Void?</span> = <span class="literal">true</span> <span class="comment">// 我们永远都不会发现是这里导致的</span></span><br></pre></td></tr></table></figure><p>按照洛夫克拉夫特式恐怖小说的传统,<code>Void</code> 有一个计算机无法处理的物理结构;我们简单地见证了它如何使一个进程无可救药的疯狂。</p><h2 id="徒有其表的胜利"><a href="#徒有其表的胜利" class="headerlink" title="徒有其表的胜利"></a>徒有其表的胜利</h2><p>我们用一段熟悉的代码来结束这段神奇的学习之旅:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">Result</span><<span class="title">Value</span>, <span class="title">Error</span>> </span>{</span><br><span class="line"> <span class="keyword">case</span> success(<span class="type">Value</span>)</span><br><span class="line"> <span class="keyword">case</span> failure(<span class="type">Error</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果你还记得之前 <a href="https://nshipster.com/never" target="_blank" rel="noopener">我们关于 <code>Never</code> 类型的文章</a>,你应该知道,将 <code>Result</code> 的 <code>Error</code> 类型设为 <code>Never</code> 可以让它表示某些总会成功的操作。</p><p>类似的,操作成功但不会生成有意义的结果,用 <code>Void</code> 作为 <code>Value</code> 类型可以表示。</p><p>例如,应用可能会通过简单的网络请求定时“ping”服务器来实现一个 <a href="https://en.wikipedia.org/wiki/The_Tell-Tale_Heart" target="_blank" rel="noopener">心跳</a>。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ping</span><span class="params">(<span class="number">_</span> url: URL, completion: <span class="params">(Result<Void, Error>)</span></span></span> -> <span class="type">Void</span>) {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>根据 HTTP 语义,一个虚拟 <code>/ping</code> 终端正确的状态码应该是 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204" target="_blank" rel="noopener">204 No Content</a>。</p></blockquote><p>在请求的回调中,通过下面的调用来表示成功:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">completion(.success(()))</span><br></pre></td></tr></table></figure><p>假如你觉得括号太多了(其实又有什么问题呢?),给 <code>Result</code> 加一个关键的扩展可以让事情更简单点:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Result</span> <span class="title">where</span> <span class="title">Value</span> == <span class="title">Void</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> success: <span class="type">Result</span> {</span><br><span class="line"> <span class="keyword">return</span> .success(())</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>有付出就有收获。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">completion(.success)</span><br></pre></td></tr></table></figure><p><br></p><p>虽然这看起来像一次纯理论甚至抽象的练习,但对 <code>Void</code> 的探究能让我们对 Swift 这门编程语言的基础有一个更深刻的认知。</p><p>在 Swift 还没有面世很久之前,元组在编程语言中扮演着重要角色。它们可以表示参数列表和枚举关联值,依场景不同而扮演不同角色。但在某些情况下,这个模型崩溃了。编程语言依然没有调和好这些不同结构之间的差异。</p><p>依据 Swift 神话,<code>Void</code> 将会是那些老神(译者注:旧的编程语言)的典范:它是一个真正的单例,你压根一丁点儿都不会注意到它的作用和影响;编译器也会忽略它。</p><p>可能这一切都只是我们理解力的边缘发明,是我们对这门语言前景担忧的一种表现。总之,当你凝视 <code>Void</code> 时,<code>Void</code> 也在凝视着你。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
本文介绍了 Swift 中 Void 的
</summary>
<category term="Swift" scheme="https://swift.gg/categories/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/categories/Swift/NSHipster/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/tags/NSHipster/"/>
</entry>
<entry>
<title>Swift 5 字符串插值之美</title>
<link href="https://swift.gg/2019/02/21/the-beauty-of-swift-5-string-interpolation/"/>
<id>https://swift.gg/2019/02/21/the-beauty-of-swift-5-string-interpolation/</id>
<published>2019-02-21T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.035Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Erica Sadun,<a href="https://ericasadun.com/2018/12/12/the-beauty-of-swift-5-string-interpolation/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-12-12<br>译者:<a href="https://www.roczhang.com/" target="_blank" rel="noopener">RocZhang</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>感谢提案 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md" target="_blank" rel="noopener">SE-0228</a>,让我们能够精确控制字符串插值的打印方式。感谢 Brent 带给我们这个非常棒的功能。让我来分享一些例子。</p><a id="more"></a><p>回想一下在我们要打印可选值的时候,会这样写:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="string">"There's \(value1) and \(value2)"</span></span><br></pre></td></tr></table></figure><p>但这样写会立即得到一个警告:</p><p><img src="/img/articles/the-beauty-of-swift-5-string-interpolation/Screen-Shot-2018-12-12-at-2.38.51-PM.pngw=1065&ssl=11550726099.018731" alt></p><p>我们可以点击修复按钮来消除这些警告,得到如下的代码。但我们仍然会看到一个类似于这样的输出:“There’s Optional(23) and nil”。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="string">"There's \(String(describing: value1)) and \(String(describing: value2))"</span></span><br></pre></td></tr></table></figure><p>现在我们可以通过下面这种方式去掉输出中的“Optional”,直接打印出“There’s 23 and nil”:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">String</span>.<span class="title">StringInterpolation</span> </span>{</span><br><span class="line"> <span class="comment">/// 提供 `Optional` 字符串插值</span></span><br><span class="line"> <span class="comment">/// 而不必强制使用 `String(describing:)`</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendInterpolation</span><span class="params">(<span class="number">_</span> value: T?, <span class="keyword">default</span> defaultValue: String)</span></span> {</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> value = value {</span><br><span class="line"> appendInterpolation(value)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> appendLiteral(defaultValue)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// There's 23 and nil</span></span><br><span class="line"><span class="string">"There's \(value1, default: "</span><span class="literal">nil</span><span class="string">") and \(value2, default: "</span><span class="literal">nil</span><span class="string">")"</span></span><br></pre></td></tr></table></figure><p>我们也可以创建一组样式,从而使可选值能够保持一致的输出展示方式:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">String</span>.<span class="title">StringInterpolation</span> </span>{</span><br><span class="line"> <span class="comment">/// 可选值插值样式</span></span><br><span class="line"> <span class="keyword">public</span> <span class="class"><span class="keyword">enum</span> <span class="title">OptionalStyle</span> </span>{</span><br><span class="line"> <span class="comment">/// 有值和没有值两种情况下都包含单词 `Optional`</span></span><br><span class="line"> <span class="keyword">case</span> descriptive</span><br><span class="line"> <span class="comment">/// 有值和没有值两种情况下都去除单词 `Optional`</span></span><br><span class="line"> <span class="keyword">case</span> stripped</span><br><span class="line"> <span class="comment">/// 使用系统的插值方式,在有值时包含单词 `Optional`,没有值时则不包含</span></span><br><span class="line"> <span class="keyword">case</span> `<span class="keyword">default</span>`</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 使用提供的 `optStyle` 样式来插入可选值</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendInterpolation</span><span class="params">(<span class="number">_</span> value: T?, optStyle style: String.StringInterpolation.OptionalStyle)</span></span> {</span><br><span class="line"> <span class="keyword">switch</span> style {</span><br><span class="line"> <span class="comment">// 有值和没有值两种情况下都包含单词 `Optional`</span></span><br><span class="line"> <span class="keyword">case</span> .descriptive:</span><br><span class="line"> <span class="keyword">if</span> value == <span class="literal">nil</span> {</span><br><span class="line"> appendLiteral(<span class="string">"Optional(nil)"</span>)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> appendLiteral(<span class="type">String</span>(describing: value))</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 有值和没有值两种情况下都去除单词 `Optional`</span></span><br><span class="line"> <span class="keyword">case</span> .stripped:</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> value = value {</span><br><span class="line"> appendInterpolation(value)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> appendLiteral(<span class="string">"nil"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 使用系统的插值方式,在有值时包含单词 `Optional`,没有值时则不包含</span></span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> appendLiteral(<span class="type">String</span>(describing: value))</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 使用 `stripped` 样式来对可选值进行插值</span></span><br><span class="line"> <span class="comment">/// 有值和没有值两种情况下都省略单词 `Optional`</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendInterpolation</span><span class="params">(describing value: T?)</span></span> {</span><br><span class="line"> appendInterpolation(value, optStyle: .stripped)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// "There's Optional(23) and nil"</span></span><br><span class="line"><span class="string">"There's \(value1, optStyle: .default) and \(value2, optStyle: .default)"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// "There's Optional(23) and Optional(nil)"</span></span><br><span class="line"><span class="string">"There's \(value1, optStyle: .descriptive) and \(value2, optStyle: .descriptive)"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// "There's 23 and nil"</span></span><br><span class="line"><span class="string">"There's \(describing: value1) and \(describing: value2)"</span></span><br></pre></td></tr></table></figure><p>插值不仅仅用于调整可选值的输出方式,在其他方面也很有用。比如你想控制输出是否带有特定的字符,就不需要写一个带有空字符串的三元表达式:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 成功时包含(感谢 Nate Cook)</span></span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">String</span>.<span class="title">StringInterpolation</span> </span>{</span><br><span class="line"> <span class="comment">/// 只有 `condition` 的返回值为 `true` 才进行插值</span></span><br><span class="line"> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">appendInterpolation</span><span class="params">(<span class="keyword">if</span> condition: @autoclosure <span class="params">()</span></span></span> -> <span class="type">Bool</span>, <span class="number">_</span> literal: <span class="type">StringLiteralType</span>) {</span><br><span class="line"> <span class="keyword">guard</span> condition() <span class="keyword">else</span> { <span class="keyword">return</span> }</span><br><span class="line"> appendLiteral(literal)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 旧写法</span></span><br><span class="line"><span class="string">"Cheese Sandwich \(isStarred ? "</span>(*)<span class="string">" : "</span><span class="string">")"</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 新写法</span></span><br><span class="line"><span class="string">"Cheese Sandwich \(if: isStarred, "</span>(*)<span class="string">")"</span></span><br></pre></td></tr></table></figure><p>我们还可以用字符串插值来做更多有趣的事情。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
本文对 Swift 5 中 SE-0228 对 String Interpolation 相关功能的改进进行了举例说明,展示了一些控制字符串插值的方式。
</summary>
<category term="Erica Sadun" scheme="https://swift.gg/categories/Erica-Sadun/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
</entry>
<entry>
<title>协议中的私有属性</title>
<link href="https://swift.gg/2019/02/18/protocols-private-properties/"/>
<id>https://swift.gg/2019/02/18/protocols-private-properties/</id>
<published>2019-02-18T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.035Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Olivier Halligon,<a href="http://alisoftware.github.io/swift/protocols/2018/09/02/protocols-private-properties/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09-02<br>译者:<a href="https://github.com/dzyding" target="_blank" rel="noopener">灰s</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://linusling.com" target="_blank" rel="noopener">小铁匠Linus</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>在 Swift 中,协议中声明的属性没有访问控制的能力。如果协议中列出了某个属性,则必须使遵守协议的类型显式声明这些属性。 </p><p>不过有些时候,尽管你会在协议中声明一些属性,但你是要利用这些属性来提供你的实现,并不希望这些属性在类型的外部被使用。让我们看看如何解决这个问题。 </p><a id="more"></a><h2 id="一个简单的例子"><a href="#一个简单的例子" class="headerlink" title="一个简单的例子"></a>一个简单的例子</h2><p>假设你需要创建一个专门的对象来管理你的视图控制器(ViewControllers)导航,比如一个协调器(Coordinator)。 </p><p>每个协调器都有一个根控制器 <code>UINavigationController</code>,并共享一些通用的功能,比如在它上面推进(push)和弹出(pop)其他 ViewController。所以最初它看起来可能是这样 <a href="#foot1" id="1"><sup>[1]</sup></a>: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Coordinator.swift</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">Coordinator</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> navigationController: <span class="type">UINavigationController</span> { <span class="keyword">get</span> }</span><br><span class="line"> <span class="keyword">var</span> childCoordinator: <span class="type">Coordinator?</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">push</span><span class="params">(viewController: UIViewController, animated: Bool)</span></span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">present</span><span class="params">(childViewController: UIViewController, animated: Bool)</span></span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">pop</span><span class="params">(animated: Bool)</span></span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Coordinator</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">push</span><span class="params">(viewController: UIViewController, animated: Bool = <span class="literal">true</span>)</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.navigationController.pushViewController(viewController, animated: animated)</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">present</span><span class="params">(childCoordinator: Coordinator, animated: Bool)</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.navigationController.present(childCoordinator.navigationController, animated: animated) { [<span class="keyword">weak</span> <span class="keyword">self</span>] <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">self</span>?.childCoordinator = childCoordinator</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">pop</span><span class="params">(animated: Bool = <span class="literal">true</span>)</span></span> {</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> childCoordinator = <span class="keyword">self</span>.childCoordinator {</span><br><span class="line"> <span class="keyword">self</span>.dismissViewController(animated: animated) { [<span class="keyword">weak</span> <span class="keyword">self</span>] <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">self</span>?.childCoordinator = <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">self</span>.navigationController.popViewController(animated: animated)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当我们想要声明一个新的 <code>Coordinator</code> 对象时,会像这样做:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// MainCoordinator.swift</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MainCoordinator</span>: <span class="title">Coordinator</span> </span>{</span><br><span class="line"> <span class="keyword">let</span> navigationController: <span class="type">UINavigationController</span> = <span class="type">UINavigationController</span>()</span><br><span class="line"> <span class="keyword">var</span> childCoordinator: <span class="type">Coordinator?</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">showTutorialPage1</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">let</span> vc = makeTutorialPage(<span class="number">1</span>, coordinator: <span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">self</span>.push(viewController: vc)</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">showTutorialPage2</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">let</span> vc = makeTutorialPage(<span class="number">2</span>, coordinator: <span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">self</span>.push(viewController: vc)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">makeTutorialPage</span><span class="params">(<span class="number">_</span> num: Int, coordinator: Coordinator)</span></span> -> <span class="type">UIViewController</span> { … }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="问题:泄漏实现细节"><a href="#问题:泄漏实现细节" class="headerlink" title="问题:泄漏实现细节"></a>问题:泄漏实现细节</h2><p>这个解决方案在 <code>protocol</code> 的可见性上有两个问题: </p><ul><li>每当我们想要声明一个新的 <code>Coordinator</code> 对象,都必须显式的声明一个 <code>let navigationController: UINavigationController</code> 属性和一个 <code>var childCoordinator: Coordinator?</code> 属性。<strong>虽然,在遵守协议的类型现实中,我们并没有显式的使用他们</strong> - 但它们就在那里,因为我们需要它们作为默认的实现来供 <code>protocol Coordinator</code> 正常工作。 </li><li>我们必须声明的这两个属性具有与 <code>MainCoordinator</code> 相同的可见性(在本例中为隐式 <code>internal(内部)</code> 访问控制级别),因为这是 <code>protocol Coordinator</code> 的必备条件。这使得它们对外部可见,就像在编码时可以使用 <code>MainCoordinator</code>。 </li></ul><p>所以问题是我们每次都要声明一些属性——即使它只是一些实现细节,而且这些实现细节会通过外部接口被泄漏,从而允许类的访问者做一些本不应该被允许的事,例如: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> mainCoord = <span class="type">MainCoordinator</span>()</span><br><span class="line"><span class="comment">// 访问者不应该被允许直接访问 navigationController ,但是他们可以</span></span><br><span class="line">mainCoord.navigationController.dismissViewController(animated: <span class="literal">true</span>)</span><br><span class="line"><span class="comment">// 他们也不应该被允许做这样的事情</span></span><br><span class="line">mainCoord.childCoordinator = mainCoord</span><br></pre></td></tr></table></figure><p>也许你会认为,既然我们不希望它们是可见的,那么可以直接在第一段代码的 <code>protocol</code> 中不声明这两个属性。但是如果我们这样做,将无法通过 <code>extension Coordinator</code> 来提供默认的实现,因为默认的实现需要这两个属性存在以便它们的代码被编译。 </p><p>你可能希望 Swift 允许在协议中申明这些属性为 <code>fileprivate</code>,但是在 Swift 4 中,你不能在 <code>protocols</code> 中使用任何访问控制的关键字。 </p><p>所以我们如何才能解决这个“既要提供用到这个属性的默认实现,有不让这些属性对外暴露”的问题呢?</p><h2 id="一个解决方案"><a href="#一个解决方案" class="headerlink" title="一个解决方案"></a>一个解决方案</h2><p>实现这一点的一个技巧是将这些属性隐藏在中间对象中,并在该对象中将对应的属性声明为 <code>fileprivate</code>。 </p><p>通过这种方式,尽管我们依旧在对应类型的公共接口中声明了属性,但是接口的访问者却不能访问该对象的内部属性。而我们对于协议的默认实现却能够访问它们 —— 只要它们在同一个文件中被声明就行了(因为它们是 <code>fileprivate</code> )。 </p><p>看起来就像这样: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Coordinator.swift</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CoordinatorComponents</span> </span>{</span><br><span class="line"> <span class="keyword">fileprivate</span> <span class="keyword">let</span> navigationController: <span class="type">UINavigationController</span> = <span class="type">UINavigationController</span>()</span><br><span class="line"> <span class="keyword">fileprivate</span> <span class="keyword">var</span> childCoordinator: <span class="type">Coordinator?</span> = <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">protocol</span> <span class="title">Coordinator</span>: <span class="title">AnyObject</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> coordinatorComponents: <span class="type">CoordinatorComponents</span> { <span class="keyword">get</span> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">push</span><span class="params">(viewController: UIViewController, animated: Bool)</span></span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">present</span><span class="params">(childCoordinator: Coordinator, animated: Bool)</span></span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">pop</span><span class="params">(animated: Bool)</span></span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Coordinator</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">push</span><span class="params">(viewController: UIViewController, animated: Bool = <span class="literal">true</span>)</span></span> {</span><br><span class="line"> <span class="keyword">self</span>.coordinatorComponents.navigationController.pushViewController(viewController, animated: animated)</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">present</span><span class="params">(childCoordinator: Coordinator, animated: Bool = <span class="literal">true</span>)</span></span> {</span><br><span class="line"> <span class="keyword">let</span> childVC = childCoordinator.coordinatorComponents.navigationController</span><br><span class="line"> <span class="keyword">self</span>.coordinatorComponents.navigationController.present(childVC, animated: animated) { [<span class="keyword">weak</span> <span class="keyword">self</span>] <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">self</span>?.coordinatorComponents.childCoordinator = childCoordinator <span class="comment">// retain the child strongly</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">pop</span><span class="params">(animated: Bool = <span class="literal">true</span>)</span></span> {</span><br><span class="line"> <span class="keyword">let</span> privateAPI = <span class="keyword">self</span>.coordinatorComponents</span><br><span class="line"> <span class="keyword">if</span> privateAPI.childCoordinator != <span class="literal">nil</span> {</span><br><span class="line"> privateAPI.navigationController.dismiss(animated: animated) { [<span class="keyword">weak</span> privateAPI] <span class="keyword">in</span></span><br><span class="line"> privateAPI?.childCoordinator = <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> privateAPI.navigationController.popViewController(animated: animated)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在,遵守协议的 <code>MainCoordinator</code> 类型: </p><ul><li>仅需要声明一个 <code>let coordinatorComponents = CoordinatorComponents()</code> 属性,并不用知道 <code>CoordinatorComponents</code> 类型的内部有些什么(隐藏了实现细节)。 </li><li>在 <code>MainCoordinator.swift</code> 文件中,不能访问 <code>coordinatorComponents</code> 的任何属性,因为它们被声明为 <code>fileprivate</code>。 </li></ul><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MainCoordinator</span>: <span class="title">Coordinator</span> </span>{</span><br><span class="line"> <span class="keyword">let</span> coordinatorComponents = <span class="type">CoordinatorComponents</span>()</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">showTutorialPage1</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">let</span> vc = makeTutorialPage(<span class="number">1</span>, coordinator: <span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">self</span>.push(viewController: vc)</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">showTutorialPage2</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">let</span> vc = makeTutorialPage(<span class="number">2</span>, coordinator: <span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">self</span>.push(viewController: vc)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">makeTutorialPage</span><span class="params">(<span class="number">_</span> num: Int, coordinator: Coordinator)</span></span> -> <span class="type">UIViewController</span> { … }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然,你仍然需要在遵守协议的类型中声明 <code>let coordinatorComponents</code> 来提供存储,这个声明必须是可见的(不能是 <code>private</code>),因为这是遵守 <code>protocol Coordinator</code> 所要求的一部分。但是: </p><ul><li>只需要声明 1 个属性,取代之前的 2 个(在更复杂的情况下会有更多)。 </li><li>更重要的是,即使它可以从遵守协议的类型的实现中访问,也可以从外部接口访问,你却不能对它做任何事情。 </li></ul><p>当然,你仍然可以访问 <code>myMainCoordinator.coordinatorComponents</code>,但是不能使用它做任何事情,因为它所有的属性都是 <code>fileprivate</code> ! </p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>Swift 可能无法提供你想要的所有功能。你可能希望有朝一日 <code>protocols</code> 允许对它声明需要的属性和方法使用访问控制关键字,或者通过某种方式将它们在公共 API 中隐藏。 </p><p>但与此同时,掌握这些技巧和变通方法可以使你的公共 API 更好、更安全,避免泄露实现细节或者访问在实现之外不应该被修改的属性,同时仍然使用 <a href="http://alisoftware.github.io/swift/protocol/2015/11/08/mixins-over-inheritance/" target="_blank" rel="noopener">Mixin pattern</a> 并提供默认实现。 </p><hr><p><a id="foot1" href="#1"><sup>[1]</sup></a>.这是一个简化的例子;不要将注意力集中在 Coordinator 的实现 - 它不是这个例子的重点,更应该关注的是需要在协议中声明公开可访问的属性。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
通过另一种方式来控制协议中属性和方法的访问权限
</summary>
<category term="Olivier Halligon" scheme="https://swift.gg/categories/Olivier-Halligon/"/>
<category term="Swift" scheme="https://swift.gg/categories/Olivier-Halligon/Swift/"/>
<category term="协议" scheme="https://swift.gg/tags/%E5%8D%8F%E8%AE%AE/"/>
</entry>
<entry>
<title>SwiftSyntax</title>
<link href="https://swift.gg/2019/01/25/nshipster-swiftsyntax/"/>
<id>https://swift.gg/2019/01/25/nshipster-swiftsyntax/</id>
<published>2019-01-25T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Mattt,<a href="https://nshipster.com/swiftsyntax/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-10-22<br>译者:<a href="https://dingtz.com/" target="_blank" rel="noopener">jojotov</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p><a href="https://github.com/apple/swift-syntax" target="_blank" rel="noopener">SwiftSyntax</a> 是一个可以分析、生成以及转换 Swift 源代码的 Swift 库。它是基于 <a href="https://github.com/apple/swift/tree/master/lib/Syntax" target="_blank" rel="noopener">libSyntax</a> 库开发的,并于 <a href="https://github.com/apple/swift-syntax/commit/909d336aefacdcbdd45ec6130471644c1ae929f5" target="_blank" rel="noopener">2017 年 8 月</a> 从 Swift 语言的主仓库中分离出来,单独建立了一个仓库。</p><a id="more"></a><p>总的来说,这些库都是为了给结构化编辑(structured editing)提供安全、正确且直观的工具。关于结构化编辑,在 <a href="https://github.com/apple/swift/blob/master/lib/Syntax/README.md#swift-syntax-and-structured-editing-library" target="_blank" rel="noopener">thusly</a> 中有具体的描述:</p><blockquote><p>什么是结构化编辑?结构化编辑是一种编辑的策略,它对源代码的<em>结构</em>更加敏感,而源代码的表示(例如字符或者字节)则没那么重要。这可以细化为以下几个部分:替换标识符,将对全局方法的调用转为对方法的调用,或者根据已定的规则识别并格式化整个源文件。</p></blockquote><p>在写这篇文章时,SwiftSyntax 仍处于在开发中并进行 API 调整的阶段。不过目前你已经可以使用它对 Swift 代码进行一些编程工作。</p><p>目前,<a href="https://github.com/apple/swift/tree/master/lib/Migrator" target="_blank" rel="noopener">Swift Migrator</a> 已经在使用 SwiftSyntax 了,并且在对内和对外层面上,对 SwiftSyntax 的接入也在不断地努力着。</p><h2 id="SwiftSyntax-如何工作?"><a href="#SwiftSyntax-如何工作?" class="headerlink" title="SwiftSyntax 如何工作?"></a>SwiftSyntax 如何工作?</h2><p>为了明白 SwiftSyntax 如何工作,我们首先要回头看看 Swift 编译器的架构:</p><p><img src="/img/articles/nshipster-swiftsyntax/swift-compilation-diagram-8af7d0078f72cdaa8f50430e608f15a9d4214f5772439d2fd6904bb5a8a53c60.png1548390462.3512783" alt></p><p>Swift 编译器的主要职责是把 Swift 代码转换为可执行的机器代码。整个过程可以划分为几个离散的步骤,一开始,<a href="https://github.com/apple/swift/tree/master/lib/Parse" target="_blank" rel="noopener">语法分析器</a> 会生成一个抽象语法树(AST)。之后,语义分析器会进行工作并生成一个通过类型检查的 AST。至此步骤,代码会降级到 <a href="https://github.com/apple/swift/blob/master/docs/SIL.rst" target="_blank" rel="noopener">Swift 中间层语言</a>;随后 SIL 会继续转换并优化自身,降级为 <a href="http://llvm.org/docs/LangRef.html" target="_blank" rel="noopener">LLVM IR</a>,并最终编译为机器代码。</p><p>对于我们的讨论来说,最重要的关键点是 SwiftSyntax 的操作目标是编译过程第一步所生成的 AST。但也由于这样,SwiftSyntax 无法告知你任何关于代码的语义或类型信息。</p><p>与 SwiftSyntax 相反,一些如 <a href="https://github.com/apple/swift/tree/master/tools/SourceKit" target="_blank" rel="noopener">SourceKit</a> 之类的工具,操作的目标为更容易理解的 Swift 代码。这可以帮助此类工具实现一些编辑器相关的特性,例如代码补全或者文件之间的跳转。虽然 SwiftSyntax 不能像 SourceKit 一样实现跳转或者补全的功能,但在语法层面上也有很多应用场景,例如代码格式化和语法高亮。</p><h3 id="揭秘-AST"><a href="#揭秘-AST" class="headerlink" title="揭秘 AST"></a>揭秘 AST</h3><p>抽象语法树在抽象层面上比较难以理解。因此我们先生成一个示例来一睹其貌。</p><p>留意一下如下的一行 Swift 代码,它声明了一个名为 <code>one()</code> 的函数,函数返回值为 <code>1</code>:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">one</span><span class="params">()</span></span> -> <span class="type">Int</span> { <span class="keyword">return</span> <span class="number">1</span> }</span><br></pre></td></tr></table></figure><p>在命令行中对此文件运行 <code>swiftc</code> 命令并传入 <code>-frontend -emit-syntax</code> 参数:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> xcrun swiftc -frontend -emit-syntax ./One.swift</span></span><br></pre></td></tr></table></figure><p>运行的结果为一串 JSON 格式的 AST。当你用 JSON 格式来展示时,AST 的结构会表现的更加清晰:</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"SourceFile"</span>,</span><br><span class="line"> <span class="attr">"layout"</span>: [{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"CodeBlockItemList"</span>,</span><br><span class="line"> <span class="attr">"layout"</span>: [{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"CodeBlockItem"</span>,</span><br><span class="line"> <span class="attr">"layout"</span>: [{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"FunctionDecl"</span>,</span><br><span class="line"> <span class="attr">"layout"</span>: [<span class="literal">null</span>, <span class="literal">null</span>, {</span><br><span class="line"> <span class="attr">"tokenKind"</span>: {</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"kw_func"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"leadingTrivia"</span>: [],</span><br><span class="line"> <span class="attr">"trailingTrivia"</span>: [{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"Space"</span>,</span><br><span class="line"> <span class="attr">"value"</span>: <span class="number">1</span></span><br><span class="line"> }],</span><br><span class="line"> <span class="attr">"presence"</span>: <span class="string">"Present"</span></span><br><span class="line"> }, {</span><br><span class="line"> <span class="attr">"tokenKind"</span>: {</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"identifier"</span>,</span><br><span class="line"> <span class="attr">"text"</span>: <span class="string">"one"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"leadingTrivia"</span>: [],</span><br><span class="line"> <span class="attr">"trailingTrivia"</span>: [],</span><br><span class="line"> <span class="attr">"presence"</span>: <span class="string">"Present"</span></span><br><span class="line"> }, ...</span><br></pre></td></tr></table></figure><p>Python 中的 <code>json.tool</code> 模块提供了便捷地格式化 JSON 的能力。且几乎所有的 macOS 系统都已经集成了此模块,因此每个人都可以使用它。举个例子,你可以使用如下的命令对编译的输出结果使用 <code>json.tool</code> 格式化:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> xcrun swiftc -frontend -emit-syntax ./One.swift | python -m json.tool</span></span><br></pre></td></tr></table></figure><p>在最外层,可以看到 <code>SourceFile</code>,它由 <code>CodeBlockItemList</code> 以及 <code>CodeBlockItemList</code> 内部的 <code>CodeBlockItem</code> 这几个部分组成。对于这个示例来说,仅有一个 <code>CodeBlockItem</code> 对应函数的定义(<code>FunctionDecl</code>),其自身包含了几个子组件如函数签名、参数闭包和返回闭包。</p><p>术语 trivia 用于描述任何没有实际语法意义的东西,例如空格。每个标记符(Token)可以有一个或多个行前和行尾的 trivia。例如,在返回的闭包(<code>-> Int</code>)中的 <code>Int</code> 后的空格可以用如下的行尾 trivia 表示:</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"kind"</span>: <span class="string">"Space"</span>,</span><br><span class="line"> <span class="attr">"value"</span>: <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="处理文件系统限制"><a href="#处理文件系统限制" class="headerlink" title="处理文件系统限制"></a>处理文件系统限制</h3><p>SwiftSyntax 通过代理系统的 <code>swiftc</code> 调用来生成抽象语法树。但是,这也限制了代码必须放在某个文件才能进行处理,而我们却经常需要对以字符串表示的代码进行处理。</p><p>为了解决这个限制,其中一种办法是把代码写入一个临时文件并传入到编译器中。</p><p><a href="https://nshipster.com/nstemporarydirectory/" target="_blank" rel="noopener">我们曾经尝试过写入临时文件</a>,但目前,有更好的 API 可以帮助我们完成这项工作,它由 <a href="https://github.com/apple/swift-package-manager" target="_blank" rel="noopener">Swift Package Manager</a> 本身提供。在你的 <code>Package.swift</code> 文件中,添加如下的包依赖关系,并把 <code>Utility</code> 依赖添加到正确的 target 中:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line">.package(url: <span class="string">"https://github.com/apple/swift-package-manager.git"</span>, from: <span class="string">"0.3.0"</span>),</span><br></pre></td></tr></table></figure><p>现在,你可以像下面这样引入 <code>Basic</code> 模块并使用 <code>TemporaryFile</code> API:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> Basic</span><br><span class="line"><span class="keyword">import</span> Foundation</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> code: <span class="type">String</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> tempfile = <span class="keyword">try</span> <span class="type">TemporaryFile</span>(deleteOnClose: <span class="literal">true</span>)</span><br><span class="line"><span class="keyword">defer</span> { tempfile.fileHandle.closeFile() }</span><br><span class="line">tempfile.fileHandle.write(code.data(using: .utf8)!)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> url = <span class="type">URL</span>(fileURLWithPath: tempfile.path.asString)</span><br><span class="line"><span class="keyword">let</span> sourceFile = <span class="keyword">try</span> <span class="type">SyntaxTreeParser</span>.parse(url)</span><br></pre></td></tr></table></figure><h2 id="我们可以用-SwiftSyntax-做什么"><a href="#我们可以用-SwiftSyntax-做什么" class="headerlink" title="我们可以用 SwiftSyntax 做什么"></a>我们可以用 SwiftSyntax 做什么</h2><p>现在我们对 SwiftSyntax 如何工作已经有了足够的理解,是时候讨论一下几个使用它的方式了!</p><h3 id="编写-Swift-代码:地狱模式"><a href="#编写-Swift-代码:地狱模式" class="headerlink" title="编写 Swift 代码:地狱模式"></a>编写 Swift 代码:地狱模式</h3><p>我们第一个想到,但却是最没有实际意义的 SwiftSyntax 用例就是让编写 Swift 代码的难度提升几个数量级。</p><p>利用 SwiftSyntax 中的 <code>SyntaxFactory</code> APIs,我们可以生成完整的 Swift 代码。不幸的是,编写这样的代码并不像闲庭散步般轻松。</p><p>留意一下如下的示例代码:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftSyntax</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> structKeyword = <span class="type">SyntaxFactory</span>.makeStructKeyword(trailingTrivia: .spaces(<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> identifier = <span class="type">SyntaxFactory</span>.makeIdentifier(<span class="string">"Example"</span>, trailingTrivia: .spaces(<span class="number">1</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> leftBrace = <span class="type">SyntaxFactory</span>.makeLeftBraceToken()</span><br><span class="line"><span class="keyword">let</span> rightBrace = <span class="type">SyntaxFactory</span>.makeRightBraceToken(leadingTrivia: .newlines(<span class="number">1</span>))</span><br><span class="line"><span class="keyword">let</span> members = <span class="type">MemberDeclBlockSyntax</span> { builder <span class="keyword">in</span></span><br><span class="line"> builder.useLeftBrace(leftBrace)</span><br><span class="line"> builder.useRightBrace(rightBrace)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> structureDeclaration = <span class="type">StructDeclSyntax</span> { builder <span class="keyword">in</span></span><br><span class="line"> builder.useStructKeyword(structKeyword)</span><br><span class="line"> builder.useIdentifier(identifier)</span><br><span class="line"> builder.useMembers(members)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(structureDeclaration)</span><br></pre></td></tr></table></figure><p><em>唷。</em>那最后这段代码让我们得到了什么呢?</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Example</span> </span>{</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><em>令人窒息的操作。</em></p><p>这绝不是为了取代 <a href="https://nshipster.com/swift-gyb/" target="_blank" rel="noopener">GYB</a> 来用于每天的代码生成。(事实上,<a href="https://github.com/apple/swift/blob/master/lib/Syntax/SyntaxKind.cpp.gyb" target="_blank" rel="noopener">libSyntax</a> 和 <a href="https://github.com/apple/swift-syntax/blob/master/Sources/SwiftSyntax/SyntaxKind.swift.gyb" target="_blank" rel="noopener">SwiftSyntax</a> 都使用了 <code>gyb</code> 来生成接口。</p><p>但这个接口在某些特殊的问题上却格外有用。例如,你或许会使用 SwiftSyntax 来实现一个 Swift 编译器的 <a href="https://en.wikipedia.org/wiki/Fuzzing" target="_blank" rel="noopener">模糊测试</a>,使用它可以随机生成一个表面有效却实际上非常复杂的程序,以此来进行压力测试。</p><h2 id="重写-Swift-代码"><a href="#重写-Swift-代码" class="headerlink" title="重写 Swift 代码"></a>重写 Swift 代码</h2><p><a href="https://github.com/apple/swift-syntax#example" target="_blank" rel="noopener">在 SwiftSyntax 的 README 中有一个示例</a> 展示了如何编写一个程序来遍历源文件中的整型并把他们的值加 1。</p><p>通过这个,你应该已经推断得出如何使用它来创建一个典型的 <code>swift-format</code> 工具。</p><p>但现在,我们先考虑一个相当<em>没有</em>效率——并且可能在万圣节(🎃)这种需要捣蛋的场景才合适的用例,源代码重写:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftSyntax</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ZalgoRewriter</span>: <span class="title">SyntaxRewriter</span> </span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">visit</span><span class="params">(<span class="number">_</span> token: TokenSyntax)</span></span> -> <span class="type">Syntax</span> {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">case</span> <span class="keyword">let</span> .stringLiteral(text) = token.tokenKind <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> token</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> token.withKind(.stringLiteral(zalgo(text)))</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><a href="https://gist.github.com/mattt/b46ab5027f1ee6ab1a45583a41240033" target="_blank" rel="noopener"><code>zalgo</code></a> 函数是用来做什么的?可能不知道会更好……</p><p>不管怎样,在你的源代码中运行这个重写器,可以把所有的文本字符串转换为像下面一样的效果:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Before 👋😄</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">"Hello, world!"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// After 🦑😵</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">"H͞͏̟̂ͩel̵ͬ͆͜ĺ͎̪̣͠ơ̡̼͓̋͝, w͎̽̇ͪ͢ǒ̩͔̲̕͝r̷̡̠͓̉͂l̘̳̆ͯ̊d!"</span>)</span><br></pre></td></tr></table></figure><p><em>鬼魅一般,对吧?</em></p><h2 id="高亮-Swift-代码"><a href="#高亮-Swift-代码" class="headerlink" title="高亮 Swift 代码"></a>高亮 Swift 代码</h2><p>让我们用一个真正实用的东西来总结我们对 SwiftSyntax 的探究:一个 Swift 语法高亮工具。</p><p>从语法高亮工具的意义上来说,它可以把源代码按某种方式格式化为显示更为友好的 HTML。</p><p><a href="https://github.com/NSHipster/nshipster.com" target="_blank" rel="noopener">NSHipster 通过 Jekyll 搭建</a>,并使用了 Ruby 的库 <a href="https://github.com/jneen/rouge" target="_blank" rel="noopener">Rouge</a> 来渲染你在每篇文章中看到的示例代码。尽管如此,由于 Swift 的复杂语法和过快迭代,渲染出来的 HTML 并不是 100% 正确。</p><p>不同于 <a href="https://github.com/jneen/rouge/blob/master/lib/rouge/lexers/swift.rb" target="_blank" rel="noopener">处理一堆麻烦的正则表达式</a>,我们可以构造一个 <a href="https://github.com/NSHipster/SwiftSyntaxHighlighter" target="_blank" rel="noopener">语法高亮器</a> 来放大 SwiftSyntax 对语言的理解的优势。</p><p>根据这个核心目的,实现的方法可以很直接:实现一个 <code>SyntaxRewriter</code> 的子类并重写 <code>visit(_:)</code> 方法,这个方法会在遍历源文件的每个标识符时被调用。通过判断每种不同的标识符类型,你可以把相应的可高亮标识符映射为 HTML 标记。</p><p>例如,数字文本可以用类名是 <code>m</code> 开头的 <code><span></code> 元素来表示(<code>mf</code> 表示浮点型,<code>mi</code> 表示整型)。如下是对应的在 <code>SyntaxRewriter</code> 子类中的代码:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> SwiftSyntax</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SwiftSyntaxHighlighter</span>: <span class="title">SyntaxRewriter</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> html: <span class="type">String</span> = <span class="string">""</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">visit</span><span class="params">(<span class="number">_</span> token: TokenSyntax)</span></span> -> <span class="type">Syntax</span> {</span><br><span class="line"> <span class="keyword">switch</span> token.tokenKind {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">case</span> .floatingLiteral(<span class="keyword">let</span> string):</span><br><span class="line"> html += <span class="string">"<span class=\"mf\">\(string)</span>"</span></span><br><span class="line"> <span class="keyword">case</span> .integerLiteral(<span class="keyword">let</span> string):</span><br><span class="line"> <span class="keyword">if</span> string.hasPrefix(<span class="string">"0b"</span>) {</span><br><span class="line"> html += <span class="string">"<span class=\"mb\">\(string)</span>"</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> string.hasPrefix(<span class="string">"0o"</span>) {</span><br><span class="line"> html += <span class="string">"<span class=\"mo\">\(string)</span>"</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> string.hasPrefix(<span class="string">"0x"</span>) {</span><br><span class="line"> html += <span class="string">"<span class=\"mh\">\(string)</span>"</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> html += <span class="string">"<span class=\"mi\">\(string)</span>"</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> token</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>尽管 <code>SyntaxRewritere</code> 针对每一种不同类型的语法元素,都已经实现了 <code>visit(:)</code> 方法,但我发现使用一个 <code>switch</code> 语句可以更简单地处理所有工作。(在 <code>default</code> 分支中打印出无法处理的标记符,可以更好地帮助我们找到那些没有处理的情况)。这不是最优雅的实现,但鉴于我对 SwiftSyntax 不足的理解,这是个较好的开端。</p><p>不管怎样,在几个小时的开发工作后,我已经可以在 Swift 大量的语法特性中,生成出比较理想的渲染过的输出。</p><p><img src="/img/articles/nshipster-swiftsyntax/swiftsyntaxhightlighter-example-output-829aa64ab4bdf73a2e3070aab017e21e3db37ca0ee35079f0e89e22594806df0.png1548390462.5352607" alt></p><p>这个项目需要一个库和命令行工具的支持。快去 <a href="https://github.com/NSHipster/SwiftSyntaxHighlighter" target="_blank" rel="noopener">尝试一下 </a>然后让我知道你的想法吧!</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Mattt,<a href="https://nshipster.com/swiftsyntax/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-10-22<br>译者:<a href="https://dingtz.com/" target="_blank" rel="noopener">jojotov</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p><a href="https://github.com/apple/swift-syntax" target="_blank" rel="noopener">SwiftSyntax</a> 是一个可以分析、生成以及转换 Swift 源代码的 Swift 库。它是基于 <a href="https://github.com/apple/swift/tree/master/lib/Syntax" target="_blank" rel="noopener">libSyntax</a> 库开发的,并于 <a href="https://github.com/apple/swift-syntax/commit/909d336aefacdcbdd45ec6130471644c1ae929f5" target="_blank" rel="noopener">2017 年 8 月</a> 从 Swift 语言的主仓库中分离出来,单独建立了一个仓库。</p>
</summary>
<category term="Swift" scheme="https://swift.gg/categories/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/categories/Swift/NSHipster/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/tags/NSHipster/"/>
</entry>
<entry>
<title>以流的形式执行 Multipart 请求</title>
<link href="https://swift.gg/2019/01/21/streaming-multipart-requests/"/>
<id>https://swift.gg/2019/01/21/streaming-multipart-requests/</id>
<published>2019-01-21T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Soroush Khanlou,<a href="http://khanlou.com/2018/11/streaming-multipart-requests/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-14<br>译者:<a href="https://www.jianshu.com/u/076cc5e18bb8" target="_blank" rel="noopener">郑一一</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>Foundation 框架中的 URL 类提供了非常全面的功能,此后还在 iOS 7 中新增了 URLSession 类。尽管如此,基础库中仍然缺少 multipart 文件上传的功能。</p><a id="more"></a><h2 id="什么是-multipart-请求?"><a href="#什么是-multipart-请求?" class="headerlink" title="什么是 multipart 请求?"></a>什么是 multipart 请求?</h2><p>Multipart 编码实际上就是在网络中上传大型文件的方法。在浏览器中,有时候你会选择一个文件作为表单提交内容的一部分。这个文件便是以 multipart 请求的方式实现上传的。</p><p>乍一看,multipart 请求和一般请求差不多。不同之处是 multipart 请求额外为 <code>HTTP</code> 请求体指定了唯一编码。同 JSON 编码(<code>{"key": "value"}</code>)或者 URL 字符编码 (<code>key=value</code>) 相比,multipart 编码干的事略微有所不同。因为 multipart 请求体实际上只是一串字节流,接收端实体在解析数据时,需要知道字节流中各个部分之间的界限。所以 multipart 请求需要使用 “boundaries” 来解决这个问题。在请求首部的 <code>Content-Type</code> 中,可以定义 boundary:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Accept: application/json</span><br><span class="line">Content-Type: multipart/form-data; boundary=khanlou.comNczcJGcxe</span><br></pre></td></tr></table></figure><p>Boundary 的具体内容并不重要,唯一需要注意的是:在请求体中,boundary 是不能重复出现(这样才能体现 boundary 的作用)。你可以使用 UUID 作为 boundary。</p><p>请求的每一部分可以是普通数据(比如图片)或者元数据(一般是文本,对应一个名字,组成一个键值对)。如果数据是图片的话,那它看起来应该是这样的:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">--<boundary></span><br><span class="line">Content-Disposition: form-data; name=<name>; filename=<filename.jpg></span><br><span class="line">Content-Type: image/jpeg</span><br><span class="line"></span><br><span class="line"><image data></span><br></pre></td></tr></table></figure><p>如果是普通文本,则是这样:</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">--<boundary></span><br><span class="line">Content-Disposition: form-data; name=<name></span><br><span class="line">Content-Type: text/plain</span><br><span class="line"></span><br><span class="line"><some text></span><br></pre></td></tr></table></figure><p>请求结尾会有一个带着两个连字符的 boundary,<code>--<boundary>--</code>。(此处需要注意,所有新行必须是回车换行。)</p><p>以上就是关于 multipart 请求的所有内容,并不是特别复杂。事实上,当在写第一个有关 multipart 编码的客户端实现时,我有些抵触阅读 multipart/form-data 的 <a href="https://tools.ietf.org/html/rfc7578" target="_blank" rel="noopener">RFC</a>。可是在开始阅读之后,我对这个协议的理解更深了。整个文档可读性很强,很轻易就能直达知识的源头。</p><p>我在开源的 <a href="https://github.com/backchannel/BackchannelSDK-iOS" target="_blank" rel="noopener">Backchannel SDK</a> 实现了上述功能。<a href="https://github.com/backchannel/BackchannelSDK-iOS/blob/master/Source/Image%20Chooser/BAKUploadAttachmentRequest.m" target="_blank" rel="noopener"><code>BAKUploadAttachmentRequest</code></a> 和 <a href="https://github.com/backchannel/BackchannelSDK-iOS/blob/master/Source/Image%20Chooser/BAKMultipartRequestBuilder.m" target="_blank" rel="noopener"><code>BAKMultipartRequestBuilder</code></a> 类包含了处理 mulitipart 的方法。在这个项目中,仅仅包含了处理单个文件的情况,并且没有包括元数据。但是作为范例,依旧很好地展示了 mulitipart 请求是如何构建的。可以通过添加额外的实现代码,来支持元数据和多文件的功能。</p><p>无论是使用一个请求上传多个文件,还是多个请求分别对应上传一个文件,来实现多文件上传功能,都会碰到一个问题。这个问题就是,如果你尝试一次性上传很多文件的话,app 将会闪退。这是因为使用 <a href="https://github.com/backchannel/BackchannelSDK-iOS/blob/master/Source/Image%20Chooser/BAKMultipartRequestBuilder.m#L66-L70" target="_blank" rel="noopener">该版本的代码</a>,加载的数据会直接进入内存,在内存暴涨的情况下,即使使用当下性能最强的旗舰手机也会有闪退发生。</p><h2 id="将硬盘中数据以流的形式读取"><a href="#将硬盘中数据以流的形式读取" class="headerlink" title="将硬盘中数据以流的形式读取"></a>将硬盘中数据以流的形式读取</h2><p>最常见的解决方法是将硬盘中的数据以流的形式读取出来。其核心思想是文件的字节数据会一直保存在硬盘里,直到被读取并发往网络。内存中只保留了很小一部分的镜像数据。</p><p>目前,我想出两种方法可以解决这个问题。第一个方法,把 multipart 请求体中的所有数据写到硬盘的一个新文件中,并使用 URLSession 的 <code>uploadTask(with request: URLRequest, fromFile fileURL: URL)</code> 方法将文件转化为流。这个方法可以奏效,但我并不想为每一个请求新建一个新文件保存到硬盘中。因为这意味着在请求发出后,还需要删除这个文件。</p><p>第二种方法是将内存和硬盘的数据合并在一起,并通过统一的接口向网络输出数据。</p><p>如果你觉得第二种方法听起来像是 <a href="http://khanlou.com/2015/10/clustering/" target="_blank" rel="noopener">类簇</a>,恭喜你,完全正确。很多常用 Cocoa 类都允许创建子类,并实现一些父类方法,使其和父类表现一致。回想一下 <code>NSArray</code> 的 <code>-count</code> 属性和 <code>-objectAtIndex:</code> 方法。因为 <code>NSArray</code> 的所有其它方法都是基于 <code>-count</code> 属性和 <code>-objectAtIndex:</code> 方法实现的,你可以非常轻易地创建优化版本的 <code>NSArray</code> 子类。</p><p>你可以创建一个 <code>NSData</code> 子类,它无需真正从硬盘读取数据,而只是创建一个指针直接指向硬盘中的数据。这样做的好处是是不需要把数据载入内存中进行读取。这种方法称为内存映射,基于 Unix 方法 <code>mmap</code>。你可以通过 <code>.mappedIfSafe</code> 或者 <code>alwaysMapped</code> 选项,来使用 <code>NSData</code> 的这项特性。因为 <code>NSData</code> 是一个类簇,我们将创建一个 <code>ConcatenatedData</code> 子类(就像 <code>FlattenCollection</code> 在 Swift 中的工作方式),该子类会将多个 <code>NSData</code> 对象视作一个连续的 <code>NSData</code>。完成创建以后,我们就做好所有准备来解决这个问题啦。</p><p>通过查看 <code>NSData</code> 所有原生方法,可以发现,需要实现的是 <code>-count</code> 和 <code>-bytes</code>。实现 <code>-count</code> 并不难,我们可以把所有 <code>NSData</code> 对象的大小相加得到;但在实现 <code>-bytes</code> 时则会有个问题。 <code>-bytes</code> 需要返回一个指向一段连续缓冲区的指针,而目前我们并没有这个指针。</p><p>在基础库中,提供了 <code>NSInputStream</code> 类用于处理不连续的数据。非常幸运,<code>NSInputStream</code> 同样是一个类簇。我们可以创建一个子类,将多条流合并。在使用子类时,感觉上就像是一条流。通过使用 <code>+inputStreamWithData:</code> 和 <code>+inputStreamWithURL:</code> 方法,可以轻易地创建一条输入流,用来代表硬盘中的文件和内存中的数据(比如 boundaries)。</p><p>通过阅读最好的第三方网络库源代码,你会发现 <a href="https://github.com/AFNetworking/AFNetworking" target="_blank" rel="noopener">AFNetworking</a> 采用了<a href="https://github.com/AFNetworking/AFNetworking/blob/009e3bb6673edc183c4f2baf552ad7cccba94d58/AFNetworking/AFURLRequestSerialization.m#L896-L927" target="_blank" rel="noopener">这种方法</a>。(<a href="https://github.com/Alamofire/Alamofire" target="_blank" rel="noopener">Alamofire</a>,Swift 版本的 AFNetworking,则采用了第一种方法,<a href="https://github.com/Alamofire/Alamofire/blob/ff16ce9e87aeb0ee1f30b28789db1fff01e8fb02/Source/MultipartFormData.swift#L432-L455" target="_blank" rel="noopener">将数据全部加载到内存中</a>,但如果数据量太大,就会写到硬盘的一个文件中。)</p><h2 id="将所有部分拼接起来"><a href="#将所有部分拼接起来" class="headerlink" title="将所有部分拼接起来"></a>将所有部分拼接起来</h2><p>你可以在 <a href="https://gist.github.com/khanlou/8cc2e3cb23ec8d03b1fc187f5922e244" target="_blank" rel="noopener">这里</a> 看看我的串行输入流的实现(是用 Objective-C 实现的,以后我可能还会写一个 Swift 版本的)。</p><p>通过 <code>SKSerialInputStream</code> 类,可以将流组合在一起。下面展示了前缀和后缀属性:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">MultipartComponent</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> prefixData: <span class="type">Data</span> {</span><br><span class="line"> <span class="keyword">let</span> string = <span class="string">"""</span></span><br><span class="line"><span class="string"> \(self.boundary)</span></span><br><span class="line"><span class="string"> Content-Disposition: form-data; name="\(self.name); filename="\(self.filename)"</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> <span class="keyword">return</span> string.data(using: .utf8)</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">var</span> postfixData: <span class="type">Data</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"\r\n"</span>.data(using: .utf8)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>将元数据和文件的 <code>dataStream</code> 组合在一起,得到一条输入流:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">MultipartComponent</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> inputStream: <span class="type">NSInputStream</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> streams = [</span><br><span class="line"> <span class="type">NSInputStream</span>(data: prefixData),</span><br><span class="line"> <span class="keyword">self</span>.fileDataStream,</span><br><span class="line"> <span class="type">NSInputStream</span>(data: postfixData),</span><br><span class="line"> ]</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> <span class="type">SKSerialInputStream</span>(inputStreams: streams)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>创建好每一部分输入流之后,就可以把所有流组合在一起,得到一条完整输入流。此外,在请求结尾还需要添加一个 boundary:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">RequestBuilder</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> bodyInputStream: <span class="type">NSInputStream</span> {</span><br><span class="line"> <span class="keyword">let</span> stream = parts</span><br><span class="line"> .<span class="built_in">map</span>({ $<span class="number">0</span>.inputStream })</span><br><span class="line"> + [<span class="type">NSInputStream</span>(data: <span class="string">"--\(self.boundary)--"</span>.data(using: .utf8))]</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> <span class="type">SKSerialInputStream</span>(inputStreams: streams)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后,将 <code>bodyInputStream</code> 赋值给 URL 请求的 <code>httpBodyStream</code> 属性:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> urlRequest = <span class="type">URLRequest</span>(url: url)</span><br><span class="line"></span><br><span class="line">urlRequest.httpBodyStream = requestBuilder.bodyInputStream;</span><br></pre></td></tr></table></figure><p>注意,<code>httpBodyStream</code> 和 <code>httpBody</code> 两个属性是互斥的——两个属性不会同时生效。设置 <code>httpBodyStream</code> 会使得 <code>Data</code> 版本 <code>httpBody</code> 失效,反之亦然。</p><p>流文件上传的关键是能够将多条输入流合并成一条流。<code>SKSerialInputStream</code> 类完成了整个工作。尽管说子类化 <code>NSInputStream</code> 有一些困难,可一旦解决这个问题,我们就离成功不远啦。</p><h2 id="子类化过程中需要注意的问题"><a href="#子类化过程中需要注意的问题" class="headerlink" title="子类化过程中需要注意的问题"></a>子类化过程中需要注意的问题</h2><p>子类化 <code>NSInputStream</code> 的过程不会太轻松,甚至可以说很困难。你必须实现 9 个方法。其中的 7 个方法,父类只有一些微不足道的默认实现。而在文档中只提到了 9 个方法中的 3 个,所以你还得实现 6 个 <code>NSStream</code> (<code>NSInputStream</code> 的父类)的方法,其中有 2 个是 run loop 方法,并允许空实现。在这之前,你还需要额外 <a href="http://blog.bjhomer.com/2011/04/subclassing-nsinputstream.html" target="_blank" rel="noopener">实现 3 个私有方法</a>,不过现在不必实现了。此外,还需要定义 3 个<code>只读</code>属性:<code>streamStatus</code>,<code>streamError</code>,<code>delegate</code>。</p><p>在处理完上述子类化相关的细节后,接下来的挑战是创建一个 <code>NSInputStream</code> 子类,其行为应该和 API 使用者所期望的保持一致。然而,这个类状态的重度耦合是不容易被人发现的。</p><p>有一些状态需要保证行为一致。举个例子,<code>hasBytesAvailable</code> 是不同于其它状态的,但还是存在细微的联系。在我最近发现的一个 bug 里,<code>hasBytesAvailable</code> 属性会返回 <code>self.currentIndex != self.inputStreams.count</code>,但是这会造成一个 bug,流会一直处于开启的状态,并最终造成请求超时。修复这个 bug 的办法是改为返回 <code>YES</code>,但我一直没有找到这个 bug 的根源所在。</p><p>另外一个状态 <code>streamStatus</code>,存在许多可能的值,其中比较重要的两个值是 <code>NSStreamStatusOpen</code> 和 <code>NSStreamStatusClosed</code>。</p><p>最后一个比较有意思的状态是字节数,从 <code>read</code> 方法中返回值。这个属性除了会返回正整型数之外,还会返回 -1,-1 代表有错误产生,需要进一步检查非空属性 <code>streamError</code> 来获取更多信息。字节数还可以返回 0,根据文档描述,这是标明流结尾的另外一种方式。</p><p>文档并不会告诉你哪些状态的组合是有意义的。比如说流产生一个 <code>streamError</code>,但状态却是 <code>NSStreamStatusClosed</code>,而不是 <code>NSStreamStatusError</code>,在这种情况下是否会有问题?想要管理好所有的状态非常难,不过到最后终究还是能解决的。</p><p>对于 <code>SKSerialStream</code> 类,是否可以在所有情况下都能正常工作,我还不是特别有信心。但看起来,<code>SKSerialStream</code> 通过使用 URLSession 能很好地支持上传 multipart 数据。如果你在使用这份代码的时候发现任何问题,请务必联系我,我们可以一起不断优化这个类。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Soroush Khanlou,<a href="http://khanlou.com/2018/11/streaming-multipart-requests/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-14<br>译者:<a href="https://www.jianshu.com/u/076cc5e18bb8" target="_blank" rel="noopener">郑一一</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p>Foundation 框架中的 URL 类提供了非常全面的功能,此后还在 iOS 7 中新增了 URLSession 类。尽管如此,基础库中仍然缺少 multipart 文件上传的功能。</p>
</summary>
<category term="Khanlou" scheme="https://swift.gg/categories/Khanlou/"/>
<category term="iOS 开发" scheme="https://swift.gg/tags/iOS-%E5%BC%80%E5%8F%91/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="Objective-C" scheme="https://swift.gg/tags/Objective-C/"/>
</entry>
<entry>
<title>Language Server Protocol</title>
<link href="https://swift.gg/2019/01/15/nshipster-language-server-protocol/"/>
<id>https://swift.gg/2019/01/15/nshipster-language-server-protocol/</id>
<published>2019-01-15T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Mattt,<a href="https://nshipster.com/language-server-protocol/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-19<br>译者:<a href="https://github.com/mobilefellow" target="_blank" rel="noopener">雨谨</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>上个月,苹果公司 <a href="https://forums.swift.org/t/new-lsp-language-service-supporting-swift-and-c-family-languages-for-any-editor-and-platform/17024" target="_blank" rel="noopener">在 Swift.org 论坛上宣布</a>,正在着手为 Swift 和 C 语言支持 <a href="https://microsoft.github.io/language-server-protocol/" target="_blank" rel="noopener">Language Server Protocol</a>(语言服务器协议,LSP)。</p><blockquote><p>对于苹果公司而言,为所有 Swift 开发者 —— 包括非苹果平台上的 —— 提供高质量的工具支持非常重要。我们希望与开源社区合作,将精力集中在构建 Xcode 和其他编辑器、其他平台可以共享的公共基础设施上。为实现这一目标,[……],我们决定支持 LSP。</p><p><em>Argyrios Kyrtzidis,2018 年 10 月 15 日</em></p></blockquote><p><strong>这可能是苹果自 2014 年将 Swift 作为开源软件发布以来,为 Swift 做出的最重要的决定。</strong> 这对于 APP 开发者来说是一件大事,对于其他平台上的 Swift 开发者来说更是一件大事。</p><p>为了理解其中的原因,本周的文章将研究 Language Server Protocol 解决了什么问题,它是如何工作的,以及它的长期影响可能是什么。</p><a id="more"></a><blockquote><p><strong>更新</strong>:sourcekit-lsp 项目现在已经可以 <a href="https://github.com/apple/sourcekit-lsp" target="_blank" rel="noopener">在 GitHub 上访问</a> 了。</p></blockquote><hr><p>想象这样一个矩阵,每一行表示不同的编程语言(Swift、JavaScript、Ruby、Python 等),每一列表示不同的代码编辑器(Xcode、Visual Studio、Vim、Atom 等),这样每个单元格表示特定编辑器对一种语言的支持级别。</p><p><img src="/img/articles/nshipster-language-server-protocol/lsp-languages-times-editors-b9a398af0dea85f2ad6dcf5412fbcb451a43bc90091d5e3ab3b1140da9926b3e.svg1547520866.1069534" alt="lsp-languages-times-editors.svg"></p><p>然后,你就发现各种组合形成了一种支离破碎的兼容。有些编辑器和部分语言深度集成,但除此之外几乎什么都干不了;其他编辑器则比较通用,对很多语言都提供了基本的支持。(IDE 这个术语通常用来描述前者。)</p><p>举个奇葩的例子:<em>你不用 Xcode 来开发 APP,却偏用来干其他事情。</em></p><p>为了更好地支持某一特定的语言,编辑器必须编写一些集成代码(integration code)—— 要么直接写在项目里,要么通过插件。由于不同语言和编辑器的实现机制不一样,因此比方说 Vim 改进了对 Ruby 支持,但这并不能让它更好地支持 Python,也不能让 Ruby 在 Atom 上运行地更好。最终的结果是:大量精力浪费在了不同技术的兼容上。</p><p>我们上面描述的情况通常被称为 <em>M × N 问题</em>,即最终的集成方案数量为编译器数量 M 与语言数量 N 的乘积。Language Server Protocol 所做的事情就是将 M × N 问题变成 <em>M + N 问题</em>。</p><p>编辑器不必实现对每种语言的支持,只需支持 LSP 即可。之后,它就能同等程度地支持所有支持 LSP 的语言。</p><p><img src="/img/articles/nshipster-language-server-protocol/lsp-languages-plus-editors-904f780fa4a21e89b5b00bfe5fca39795dd54c1c4c67acf3f0fe095aaf09064d.svg1547520866.4280558" alt="lsp-languages-plus-editors.svg"></p><blockquote><p>Tomohiro Matsuyama 在 2010 年写的 <a href="https://tkf.github.io/2013/06/04/Emacs-is-dead.html" target="_blank" rel="noopener">“Emacs は死んだ” (<em>“Emacs 已死”</em>)</a> 这篇文章就对这种问题做出了一个很好的论述。Matsuyama 描述了 Emacs 脚本语言的局限性(不支持多线程、底层 API 过少、用户基数太小),他认为编写插件的首选方法应该是与外部程序进行交互,而不是原生实现。</p></blockquote><p>Language Server Protocol 为支持的语言提供了一套通用的功能集,包括:</p><ul><li>语法高亮(Syntax Highlighting)</li><li>自动格式化(Automatic Formatting)</li><li>自动补全(Autocomplete)</li><li>语法(Syntax)</li><li>工具提示(Tooltips)</li><li>内联诊断(Inline Diagnostics)</li><li>跳转到定义(Jump to Definition)</li><li>项目内查找引用(Find References in Project)</li><li>高级文本和符号搜索(Advanced Text and Symbol Search)</li></ul><p>各种工具和编辑器可以将精力用于提升可用性和提供更高级的功能,而不是为每种新技术再造个轮子。</p><h2 id="Language-Server-Protocol-的工作原理"><a href="#Language-Server-Protocol-的工作原理" class="headerlink" title="Language Server Protocol 的工作原理"></a>Language Server Protocol 的工作原理</h2><p>如果你是一个 iOS 程序员,那么一定很熟悉 <em>server</em> 和 <em>protocol</em> 这两个术语在 Web 应用程序的 HTTP + JSON 通信场景下的含义。实际上 Language Server Protocol 差不多也是这么工作的。</p><p>对于 LSP,<em>client</em> 是指编辑器 —— 或者更宽泛一点,是指工具,<em>server</em> 是指本地独立进程里运行的一个外部程序。</p><p>至于名字中包含 <em>protocol</em>,是因为 LSP 类似于一个精简版的 HTTP:</p><ul><li>每个消息都由报头部分和内容部分组成。</li><li>报头部分包含一个必填的 <code>Content-Length</code> 字段,用于说明内容部分的大小(以字节为单位),以及一个可选的 <code>Content-Type</code> 字段(默认值为 <code>application/vscode-jsonrpc; charset=utf-8</code>)。</li><li>内容部分使用 <a href="https://www.jsonrpc.org/specification" target="_blank" rel="noopener">JSON-RPC</a> 描述请求、响应和通知的结构。</li></ul><p>每当工具中发生了什么事情,比如用户需要跳转到符号的定义,工具就会向 server 发送一个请求。server 接收到该请求,然后返回适当的响应。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Parent</span> </span>{}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Child</span>: <span class="title">Parent</span> </span>{}</span><br></pre></td></tr></table></figure><p><img src="/img/articles/nshipster-language-server-protocol/lsp-jump-to-definition-f76ae15d897ab30706c101e7300cd299ad97f6b910ed79ce4890351c2805ae56.gif1547520866.6389997" alt="lsp-jump-to-definition.gif"></p><p>以下是 LSP 如何在幕后实现这种交互:</p><p>首先,当用户打开 Swift 代码时,若 Swift language server 并未运行,编辑器将在一个独立进程中启动它,并执行一些额外的配置。</p><p>当用户执行 “跳转到定义(jump to definition)” 指令时,编辑器向 Swift language server 发送以下请求:</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"jsonrpc"</span>: <span class="string">"2.0"</span>,</span><br><span class="line"> <span class="attr">"id"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">"method"</span>: <span class="string">"textDocument/definition"</span>,</span><br><span class="line"> <span class="attr">"params"</span>: {</span><br><span class="line"> <span class="attr">"textDocument"</span>: {</span><br><span class="line"> <span class="attr">"uri"</span>: <span class="string">"file:///Users/NSHipster/Example.swift"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"position"</span>: {</span><br><span class="line"> <span class="attr">"line"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">"character"</span>: <span class="number">13</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>收到这个请求后,Swift language server 使用 <a href="https://github.com/apple/swift/tree/master/tools/SourceKit" target="_blank" rel="noopener">SourceKit</a> 等编译器工具来标识相应的代码实体,并在代码的上一行找到其声明的位置。然后 language server 用以下消息进行响应:</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"jsonrpc"</span>: <span class="string">"2.0"</span>,</span><br><span class="line"> <span class="attr">"id"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">"result"</span>: {</span><br><span class="line"> <span class="attr">"uri"</span>: <span class="string">"file:///Users/NSHipster/Example.swift"</span>,</span><br><span class="line"> <span class="attr">"range"</span>: {</span><br><span class="line"> <span class="attr">"start"</span>: {</span><br><span class="line"> <span class="attr">"line"</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="attr">"character"</span>: <span class="number">6</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"end"</span>: {</span><br><span class="line"> <span class="attr">"line"</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="attr">"character"</span>: <span class="number">12</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后,编辑器导航到文件(在本例中,该文件已经打开),将光标移动到该范围,并高亮显示出来。</p><p>这种方法的美妙之处在于,编辑器完成所有这些操作时,除了 .swift 文件与 Swift 代码相关以外,对 Swift 编程语言一无所知。编辑器需要做的就是与 language server 对话并更新 UI。而且编辑器知道如何做到这一点后,就可以遵循相同的过程,与任何带有 language server 的语言所编写的代码进行交互。</p><h2 id="Clang-LLVM-里的-Language-Server-Protocol"><a href="#Clang-LLVM-里的-Language-Server-Protocol" class="headerlink" title="Clang / LLVM 里的 Language Server Protocol"></a>Clang / LLVM 里的 Language Server Protocol</h2><p>如果你觉得之前的 <em>M + N</em> 图有点眼熟,那可能是因为 LLVM 也采用了同样的方法。</p><p>LLVM 的核心是中间表示(intermediate representation,IR)。LLVM 所支持的语言使用 <em>编译器前端(compiler frontend)</em> 生成 IR,再使用 <em>编译器后端(compiler backend)</em> 将 IR 生成所支持平台的机器码。</p><p><img src="/img/articles/nshipster-language-server-protocol/lsp-llvm-ir-34a10847cbe6519370c1b5e92def8f82b2ebde71aa2440c88880283bd5cbaf0a.svg1547520866.867885" alt="lsp-llvm-ir.svg"></p><blockquote><p>如果你想了解 Swift 代码编译的更多细节,请查看 <a href="https://nshipster.com/swiftsyntax/" target="_blank" rel="noopener">我们关于 SwiftSyntax 的文章</a>。</p></blockquote><p><a href="https://clang.llvm.org" target="_blank" rel="noopener">Clang</a> 是 C 语言的 LLVM 编译器前端。Swift 与 Objective-C 的互操作性(inter-operability)就是靠它实现的。在最近的 5.0.0 版本中,Clang 添加了一个名为 <a href="https://clang.llvm.org/extra/clangd.html" target="_blank" rel="noopener">Clangd</a> 的新工具,它是 LLVM 对 Language Server Protocol 的实现。</p><p>2018 年 4 月,<a href="http://lists.llvm.org/pipermail/cfe-dev/2018-April/057668.html" target="_blank" rel="noopener">苹果公司向 LLVM 邮件组宣布</a>,将把开发的重心从 <a href="https://clang.llvm.org/doxygen/group__CINDEX.html" target="_blank" rel="noopener">libclang</a> 转向 Clangd,以其作为创建交互工具的主要方式。</p><p>现在你可能会想,<em>“那又怎样?”</em> 苹果公司是 LLVM 项目最重要的支持者之一,该项目创始人 Chris Lattner 已经在苹果公司工作了十多年。苹果公司决定从不透明的 Clang 工具切换到另一个,似乎是一个实现细节了(可以这么说)。</p><p>这个官宣很有趣的一点是,Clangd 似乎完全是在苹果以外开发的,谷歌和其他公司也做出了重大贡献。这个官宣标志着未来工具开发方向的重大转变 —— 六个月后 Swift.org 论坛将证实这一点。</p><h2 id="苹果支持-Language-Server-Protocol-的潜在影响"><a href="#苹果支持-Language-Server-Protocol-的潜在影响" class="headerlink" title="苹果支持 Language Server Protocol 的潜在影响"></a>苹果支持 Language Server Protocol 的潜在影响</h2><p>根据苹果公司 10 月份发布的 LSP 公告,我们预计在未来几周内(撰写本文时,最早 11 月中旬)将看到该项目的首批代码。</p><p>要感受这些发展的全部影响还需要一些时间,但请相信我:你的耐心是值得的。我相信以下是 LSP 在未来几个月和几年将会发生的一些事情。</p><h3 id="Swift-变成一种更加通用的编程语言"><a href="#Swift-变成一种更加通用的编程语言" class="headerlink" title="Swift 变成一种更加通用的编程语言"></a>Swift 变成一种更加通用的编程语言</h3><p>虽然 Swift 主要用于 APP 开发,但它从一开始就被设计成一种功能强大的通用编程语言。在 <a href="https://www.tensorflow.org/swift/" target="_blank" rel="noopener">Swift for TensorFlow</a>、<br><a href="https://github.com/apple/swift-nio" target="_blank" rel="noopener">SwiftNIO</a> 和其他项目中,我们正开始看到 Swift 承诺的在 App Store 之外的使用。</p><p>到目前为止,阻碍 Swift 被主流采用的最大因素之一是它对 Xcode 的依赖。</p><p>人们会有很多质疑,当有那么多优秀的、门槛低很多的替代方案可选的情况下,为什么还要让 Web 开发者或机器学习工程师仅仅为了尝试 Swift 而去下载 Xcode?支持 Language Server Protocol 可以让苹果生态圈以外的人更容易地使用他们熟悉工具去感受 Swift。</p><h3 id="Xcode-变得更好"><a href="#Xcode-变得更好" class="headerlink" title="Xcode 变得更好"></a>Xcode 变得更好</h3><p>支持 LSP 不仅仅是让 Swift 在其他编辑器中运行地更好,Xcode 也将受益匪浅。</p><p>看看苹果 Swift 项目负责人 Ted Kremenek 的 <a href="https://forums.swift.org/t/new-lsp-language-service-supporting-swift-and-c-family-languages-for-any-editor-and-platform/17024/29" target="_blank" rel="noopener">这篇论坛帖子</a>:</p><blockquote><p>@akyrtzi 所描述的 LSP 服务将比今天的 SourceKit 更强大。</p></blockquote><p>LSP 对 Xcode 团队来说是一个机遇,让他们使用一种新的方法实现 Swift 集成,并可以将其应用在语言和工具自 1.0 版本发布以来四年中的所有改进上。</p><h3 id="Xcode(最终)变得更强大"><a href="#Xcode(最终)变得更强大" class="headerlink" title="Xcode(最终)变得更强大"></a>Xcode(最终)变得更强大</h3><p>LSP 的好处并不限于 Swift 和 Objective-C,<a href="https://forums.swift.org/t/new-lsp-language-service-supporting-swift-and-c-family-languages-for-any-editor-and-platform/17024/33" target="_blank" rel="noopener">Argyrios 在那个帖子的另一个留言中指出</a>:</p><blockquote><p>Xcode 使用我们新的 LSP 服务,这意味着它也可以使用其他 LSP 服务,我们对此很感兴趣,不过目前暂无具体计划。</p></blockquote><p>目前的工作重点是改进 Swift。但是,一旦实现了这一点,就应该能相对简单地将这些优化转移到支持 LSP 的其他语言中。</p><hr><p>软件的架构反映了创建它的组织的结构和价值。在某种程度上,反之亦然。</p><p>通过让 Xcode 支持开放的 Language Server Protocol 标准,苹果正在履行其在苹果生态系统之外的平台上实现 Swift 成功的承诺。我认为这是可行的:工具(或缺少工具)通常是技术获得人心的关键决定因素。但或许更重要的是,我认为这一决定表明,公司内部(至少是一小部分)对合作和透明度的意愿有所增强。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Mattt,<a href="https://nshipster.com/language-server-protocol/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-19<br>译者:<a href="https://github.com/mobilefellow" target="_blank" rel="noopener">雨谨</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p>上个月,苹果公司 <a href="https://forums.swift.org/t/new-lsp-language-service-supporting-swift-and-c-family-languages-for-any-editor-and-platform/17024" target="_blank" rel="noopener">在 Swift.org 论坛上宣布</a>,正在着手为 Swift 和 C 语言支持 <a href="https://microsoft.github.io/language-server-protocol/" target="_blank" rel="noopener">Language Server Protocol</a>(语言服务器协议,LSP)。</p>
<blockquote>
<p>对于苹果公司而言,为所有 Swift 开发者 —— 包括非苹果平台上的 —— 提供高质量的工具支持非常重要。我们希望与开源社区合作,将精力集中在构建 Xcode 和其他编辑器、其他平台可以共享的公共基础设施上。为实现这一目标,[……],我们决定支持 LSP。</p>
<p><em>Argyrios Kyrtzidis,2018 年 10 月 15 日</em></p>
</blockquote>
<p><strong>这可能是苹果自 2014 年将 Swift 作为开源软件发布以来,为 Swift 做出的最重要的决定。</strong> 这对于 APP 开发者来说是一件大事,对于其他平台上的 Swift 开发者来说更是一件大事。</p>
<p>为了理解其中的原因,本周的文章将研究 Language Server Protocol 解决了什么问题,它是如何工作的,以及它的长期影响可能是什么。</p>
</summary>
<category term="Swift" scheme="https://swift.gg/categories/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/categories/Swift/NSHipster/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/tags/NSHipster/"/>
</entry>
<entry>
<title>Swift 傻瓜技巧 #6:有动画或无动画</title>
<link href="https://swift.gg/2019/01/07/stupid-swift-tricks-6-animations/"/>
<id>https://swift.gg/2019/01/07/stupid-swift-tricks-6-animations/</id>
<published>2019-01-07T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Wooji Juice,<a href="http://www.wooji-juice.com/blog/stupid-swift-tricks-6-animations.html" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-14<br>译者:<a href="https://github.com/alejx" target="_blank" rel="noopener">石榴</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://github.com/Cee" target="_blank" rel="noopener">Cee</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>流畅的动画一开始就被认为是 iOS 应用的特点之一。这不仅归功于 iOS 系统强大的动画引擎(从而使得 App 能够一边展示流畅的动画一边做着其他的事情),还归功于系统提供的非常方便的动画 API:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 无动画</span></span><br><span class="line">doStuff()</span><br><span class="line"><span class="comment">// 有动画</span></span><br><span class="line"><span class="type">UIView</span>.animate(withDuration: <span class="number">1</span>) { doStuff() }</span><br></pre></td></tr></table></figure><p>只需要将你的代码放进 block(闭包)中,就可以让它们拥有流畅的缓入缓出的动画效果。</p><p>然而,如果你使用过这套系统,你可能会遇到一些问题。这个系统可以完美地处理简单的情况,比如让一个东西淡入、淡出,或改变它的颜色,但在更复杂的情况下,这种方法就会开始出现问题。</p><a id="more"></a><p>例如下面这个例子,你想要淡出一个元素,然后删除它。<code>UIView</code> 支持这种操作:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="type">UIView</span>.animate(withDuration: <span class="number">1</span>, animations:</span><br><span class="line">{</span><br><span class="line">someting.alpha = <span class="number">0</span></span><br><span class="line">}, completion:</span><br><span class="line">{</span><br><span class="line">something.removeFormSuperView()</span><br><span class="line">})</span><br></pre></td></tr></table></figure></p><p>但你只能把所有东西都写在 <code>completion</code> block 里时才会工作。在大型项目中,我们需要把复杂的任务拆解成小的方法。但问题就在这些方法中,像在上个例子中的 <code>doStuff()</code>,我们无法在 <code>completion</code> block 中添加代码。</p><p>我们也无法得知动画有多长(甚至都不知道有没有动画),所以如果我们没有办法简单地和动画时间之间同步(如在 <a href="http://www.wooji-juice.com/products/ferrite/" target="_blank" rel="noopener">一个音频编辑软件</a> 中让进度条同步前进)。</p><p>总的来说,我们无法获知关于动画的<em>信息</em>,他们仅仅是执行代码,进行或不进行动画,并不会受我们控制。</p><p>如果我们在视图中添加带有 Auto Layout 的新元素,事情就会变得更复杂:你需要小心地调用 <code>UIView.performWithoutAnimation { }</code>,否则新出现的视图就会从 <code>(x: 0, y: 0, w: 0, h: 0)</code> 瞬移到它们的目标位置。</p><h2 id="视图属性-Animator"><a href="#视图属性-Animator" class="headerlink" title="视图属性 Animator"></a>视图属性 Animator</h2><p>很长时间以来,我一直在改变代码中动画的写法。最开始我写了我自己的 <code>AnimationContext</code> 类来协助,后来苹果提供了他们功能相同的 <code>UIViewPropertyAnimator</code>,现在我会在所有可能的地方使用它。</p><p>一般来说,我发现最有效的方法是写一个「可动画」的方法并显式接受一个 animator 参数:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doStuff</span><span class="params">(with animator: UIViewPropertyAnimator? = <span class="literal">nil</span>)</span></span></span><br><span class="line">{</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>之后我就可以直接调用 <code>doStuff()</code> 不添加动画并完成任务,或调用 <code>doStuff(with: UIViewPropertyAnimator(duration: 1, curve: .easeInOut))</code> 或加其他的参数去完成任务并添加动画。</p><p>(实际情况中,上述方法通常会被称作 <code>reflectCurrentState()</code> 或其他特定领域的名字;该方法执行所有必要的修改,并将视图与最新的数据同步。该方法一般不会被本视图以外的代码调用,而是被视图自己调用,然后会根据需要继续调用其他内部方法,或将 animator 传给其他内部方法。不过这不在本文的讨论范围内。)</p><p><code>doStuff()</code> 可以像之前一样,带有或不带有动画执行一个任务。但现在它带有了更多信息:它知道自己是否执行动画;它可以读取 animator 的 <code>duration</code> 属性(如果有的话)。他可以调用 animator 的 <code>addAnimation</code> 来明确地指定哪些代码需要动画,并直接执行不需要动画的代码;他可以调用 <code>addCompletion</code> 来处理 <code>removeFromSuperView()</code> 或其他方法。</p><p>以上都是相比于之前改进的地方,但也不是没有问题。尤其是它开始变得有点啰嗦:</p><ol><li><code>doStuff(with: ...)</code> 需要写入一个很长的 <code>UIViewPropertyAnimator</code> 构造函数。不是很理想,不过跟下面比起来不算什么:</li><li>在 <code>doStuff()</code> 内部,需要检查 <code>UIViewPropertyAnimator</code> 是否存在并调整代码。</li></ol><p>我们不能简单的依赖 optional chaining(可选链式调用)(如 <code>animator?.addcompletion { something.removeFromSuperview() }</code>),因为如果 animator 是 <code>nil</code> 会导致 block 中的代码被直接跳过,然而无论有没有动画,我们都希望该视图在父视图中被移除。</p><p>为了保证正确的行为,你的代码会类似这个样子:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doStuff</span><span class="params">(with animator: UIViewPropertyAnimator? = <span class="literal">nil</span>)</span></span></span><br><span class="line">{</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">let</span> animator = animator</span><br><span class="line">{</span><br><span class="line"><span class="number">_</span> <span class="keyword">in</span> something.removeFromSuperview()</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line">something.removeFromSuperview()</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>Objective-C 爱好者即使瞧不起 Optional(可选)也笑不出来 – 使用 Objective-C 也不会改善这种情况:<br><figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">- (<span class="keyword">void</span>) doStuffWithAnimator: (<span class="keyword">nullable</span> <span class="built_in">UIViewPropertyAnimator</span> *) animator</span><br><span class="line">{</span><br><span class="line"><span class="keyword">if</span> (animator != <span class="literal">nil</span>)</span><br><span class="line">{</span><br><span class="line">[animator addCompletion: ^(<span class="built_in">UIViewAnimatingPosition</span> position)</span><br><span class="line">{</span><br><span class="line">[something removeFromSuperview];</span><br><span class="line">}];</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">{</span><br><span class="line">[something removeFromSuperView];</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>一旦你在生产环境中想使用这样的代码,你最终会写出更杂乱、更难于阅读和维护的代码。</p><p>幸运的是,我们可以进一步的改进这段代码。</p><h2 id="Optional-不是-Nil-的另一个叫法"><a href="#Optional-不是-Nil-的另一个叫法" class="headerlink" title="Optional 不是 Nil 的另一个叫法"></a>Optional 不是 <code>Nil</code> 的另一个叫法</h2><p>改进这段代码的诀窍就在于,<code>UIViewPropertyAnimator</code> 在这里是 Optional,关键点就在于 Optional 在 Swift 中的意义。</p><p>有的时候人们会抱怨 Swift 的 Optional 非常烦人,因为在 Objective-C 中(Objective-C 中使用 <code>nil</code> 指针来替代 Swift 中的 Optional)你可以直接对指针调用方法。</p><p>Objective-C 不会抱怨指针是不是 <code>nil</code>:如果指针非空,方法会直接被调用;如果是空指针,调用会被无声地忽略掉,不用程序员做其他的事情。</p><p>我不同意这个意见。在 Swift 中,在你知道你在做什么的情况下,你只需要加一个 <code>?</code>,并不是一个很大的负担。但是由于有了 Swift 的 Optional,我们可以做更多事情。</p><p>因为在 Swift 中,Optional 是一个“真实的东西”,而不是“缺少的东西”。无论一个 Optional 的值是什么,就算是 <code>nil</code>,它也是一个枚举值,你可以对它调用方法,调用的方法也会被执行。<a href="http://www.wooji-juice.com/blog/stupid-swift-tricks-5-enums" target="_blank" rel="noopener">讲真的,Swift 的枚举超级好用!</a></p><p>(有趣的是,在 Objective-C 类中对 Swift 的 <code>nil</code> 的底层表示<em>就是</em>空指针,所以它们的效率还是很高的。但是语法层面,它们非常的不同。我们会在接下来利用这个性质。)</p><p>因为在 Swift 中,你可以对几乎所有类型添加拓展,不仅仅是 Objective-C 类。你可以:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">Optional</span> <span class="title">where</span> <span class="title">Wrapped</span> == <span class="title">UIViewPropertyAnimator</span></span></span><br><span class="line"><span class="class"></span>{</span><br><span class="line"> <span class="meta">@discardableResult</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">addCompletion</span><span class="params">(<span class="number">_</span> block: @escaping <span class="params">(UIViewAnimatingPosition)</span></span></span>->()) -> <span class="type">Optional</span><<span class="type">UIViewPropertyAnimator</span>></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> animator = <span class="keyword">self</span></span><br><span class="line"> {</span><br><span class="line"> animator.addCompletion(block)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> block(.end)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>这段代码将难看的代码移动到了 Optional 的库中(但只针对 <code>UIViewPropertyAnimator</code>)。现在,你的视图可以:<br><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doStuff</span><span class="params">(with animator: UIViewPropertyAnimator? = <span class="literal">nil</span>)</span></span></span><br><span class="line">{</span><br><span class="line">animator.addCompletion { <span class="number">_</span> <span class="keyword">in</span> something.removeFromSuperview() }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>现在回调函数总会被执行,无论有没有 animator。</p><p>(注意 <code>animator</code> 和 <code>addCompletion</code> 之间没有 <code>?</code>)</p><p>如果有 animator,block 中的代码会在动画完成时被调用;如果没有 animator,block 中的代码会被立即调用,因为 <code>nil</code> Optional 仍然是 Optional,拥有所有 Optional 的方法,当然也包括我们刚刚添加的方法 – 而不是一个吞下所有的滚落到它表面的方法调用的黑洞。</p><p>我还有类似的拓展方法来执行总是需要被执行的任务,有些是动画的一部分,或其他的立即执行的代码:如果我想让一个元素缓入,我会在把元素放入视图之前将 alpha 值设置成 0,然后调用 <code>animator.perform { something.alpha = 1 }</code> 来保证它无论有没有动画都会变得可见。</p><p>与 Optional 无关,我还在 <code>UIViewPropertyAnimator</code> 中添加了一些静态方法来生成一些常见的动画,如:<code>static func spring(...)</code>、<code>static func linear(...)</code>。Swift 的名称解析方法决定了你可以写出更简洁的代码,如:<code>doStuff(with: .spring(duration: 1))</code>。</p><p>当然,以上只是一些小的代码技巧,而不是重新构想代码或应用结构。但是随着项目的复杂度增加,像这种小的改进也会叠加起来,帮助我们对抗不断增加的复杂度,维持大型项目的可控性。谢谢你,Swift。<a href="https://www.youtube.com/watch?v=9jtU9BbReQk" target="_blank" rel="noopener">Thwift</a>.</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Wooji Juice,<a href="http://www.wooji-juice.com/blog/stupid-swift-tricks-6-animations.html" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-11-14<br>译者:<a href="https://github.com/alejx" target="_blank" rel="noopener">石榴</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://github.com/Cee" target="_blank" rel="noopener">Cee</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p>流畅的动画一开始就被认为是 iOS 应用的特点之一。这不仅归功于 iOS 系统强大的动画引擎(从而使得 App 能够一边展示流畅的动画一边做着其他的事情),还归功于系统提供的非常方便的动画 API:</p>
<figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 无动画</span></span><br><span class="line">doStuff()</span><br><span class="line"><span class="comment">// 有动画</span></span><br><span class="line"><span class="type">UIView</span>.animate(withDuration: <span class="number">1</span>) &#123; doStuff() &#125;</span><br></pre></td></tr></table></figure>
<p>只需要将你的代码放进 block(闭包)中,就可以让它们拥有流畅的缓入缓出的动画效果。</p>
<p>然而,如果你使用过这套系统,你可能会遇到一些问题。这个系统可以完美地处理简单的情况,比如让一个东西淡入、淡出,或改变它的颜色,但在更复杂的情况下,这种方法就会开始出现问题。</p>
</summary>
<category term="Wooji Juice" scheme="https://swift.gg/categories/Wooji-Juice/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="教程" scheme="https://swift.gg/tags/%E6%95%99%E7%A8%8B/"/>
</entry>
<entry>
<title>Hacking Hit Tests</title>
<link href="https://swift.gg/2018/12/27/hacking-hit-tests/"/>
<id>https://swift.gg/2018/12/27/hacking-hit-tests/</id>
<published>2018-12-27T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Soroush Khanlou,<a href="http://khanlou.com/2018/09/hacking-hit-tests/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09-07<br>译者:<a href="https://nemocdz.github.io/" target="_blank" rel="noopener">Nemocdz</a>;校对:<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>回想 <a href="https://developer.apple.com/videos/play/wwdc2015/408/" target="_blank" rel="noopener">Crusty 教我们使用面向协议编程</a>之前的日子,我们大多使用继承来共享代码的实现。通常在 UIKit 编程中,你可能会用 <code>UIView</code> 的子类去添加一些子视图,重写 <code>-layoutSubviews</code>,然后重复这些工作。也许你还会重写 <code>-drawRect</code>。但当你需要做一些特别的事情时,就需要看看 <code>UIView</code> 中其他可以被重写的方法。</p><a id="more"></a><p><code>UIKit</code> 有个十分古怪的地方,那是它的触摸事件处理系统。它主要包括两个方法,<code>-pointInstide:withEvent:</code> 和 <code>-hitTest:withEvent:</code>。</p><p><code>-pointInside:</code> 会告诉调用者给定点是否包含在指定的视图区域中。而 <code>-hitTest:</code> 用 <code>pointInside:</code> 这个方法来告诉调用者哪个子视图(如果有的话)是当前触摸在给定点的接收者。现在我比较感兴趣的是后面这个方法。</p><p>苹果的文档勉强能够让你理解怎么重新实现这个方法。在你学会怎么重新实现方法之前,你都不能改变它的功能。接下来让我们看一遍 <a href="https://developer.apple.com/documentation/uikit/uiview/1622469-hittest?language=objc" target="_blank" rel="noopener">文档</a>,并尝试重写这个函数。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">hitTest</span><span class="params">(<span class="number">_</span> point: CGPoint, with event: UIEvent?)</span></span> -> <span class="type">UIView?</span> {</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>首先,让我们从文档的第二段开始吧:</p><blockquote><p>这个方法会忽略那些隐藏的视图,禁用用户交互视图和 alpha 等级小于 0.01 的视图。</p></blockquote><p>让我们通过一些 <code>gurad</code> 语句来快速预处理这些前提条件。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {</span><br><span class="line"></span><br><span class="line">guard isUserInteractionEnabled else { return nil }</span><br><span class="line"></span><br><span class="line">guard !isHidden else { return nil }</span><br><span class="line"></span><br><span class="line">guard alpha >= 0.01 else { return nil }</span><br><span class="line"></span><br><span class="line">// ...</span><br></pre></td></tr></table></figure><p>相当简单吧。那接下来是?</p><blockquote><p>这个方法调用 <code>pointInside:withEvent:</code> 方法来遍历接收视图层级中每一个子视图,来决定哪个子视图来接收该触摸事件。</p></blockquote><p>逐字阅读文档后,感觉 <code>-pointInside:</code> 会在每一个子视图里被调用(用一个 for 循环),但这并不是完全正确的。</p><p>感谢这个 <a href="https://twitter.com/an0/status/1038254836016394240" target="_blank" rel="noopener">读者</a>。通过他在 <code>-hitTest:</code> 和 <code>-pointInside:</code> 中放置了断点的试验,我们知道 <code>-pointInside:</code> 会在 <code>self</code> 中调用(在有上面那些 guard 的情况下),而不是在每一个子视图中。 所以应该添加另外的 guard 语句,像下面这行代码一样:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">guard</span> <span class="keyword">self</span>.point(inside: point, with: event) <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br></pre></td></tr></table></figure><p><code>-pointInside:</code> 是 <code>UIView</code> 另一个需要重写的方法。它的默认实现会检查传入的某个点是否包含在视图的 <code>bounds</code> 中。如果调用 <code>-pointInside</code> 返回 true,那么意味着触摸事件发生在它的 bounds 中。</p><p>理解完这个小小的差别后,我们可以继续阅读文档了:</p><blockquote><p>如果 <code>-pointInside:withEvnet:</code> 返回 YES,那么子视图的层级也会进行类似的遍历直到找到包含指定点的最前面的视图。</p></blockquote><p>所以,从这里知道我们需要遍历视图树。这意味着循环遍历所有的视图,并调用 <code>-hitTest:</code> 在它们每一个上去找到合适的子视图。在这种情况下,这个方法是递归的。</p><p>为了遍历视图层级,我们需要一个循环。然而,这个方法其中一个更反人类的是需要反向遍历视图。子视图数组中尾部的视图反而会处在 Z 轴中<em>更高</em>的位置,所以它们应该被最先检验。(如果没有这篇 <a href="http://smnh.me/hit-testing-in-ios/" target="_blank" rel="noopener">文章</a>,我可记不起这个点。)</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">for</span> subview <span class="keyword">in</span> subviews.reversed() {</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>传入的坐标点会转换到<em>当前</em>视图的坐标系中,而非我们关心子视图中。幸运的是,UIKit 给了一个处理函数,去转换坐标点的参考系到其他任何的视图的 frame 的参考系中。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">for</span> subview <span class="keyword">in</span> subviews.reversed() {</span><br><span class="line"><span class="keyword">let</span> convertedPoint = subview.convert(point, from: <span class="keyword">self</span>)</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>一旦有了转换后的坐标点,我们就可以很简单地询问每一个子视图该点的目标视图。需要注意的是,如果点处于该视图外部(也就是说,<code>-pointInside:</code> 返回 <em>false</em>),<code>-hitTest</code> 会返回 nil。这时就应该检查层级里的下一个子视图。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">let</span> convertedPoint = subview.convert(point, from: <span class="keyword">self</span>)</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">let</span> candidate = subview.hitTest(convertedPoint, with: event) {</span><br><span class="line"><span class="keyword">return</span> candidate</span><br><span class="line">}</span><br><span class="line"><span class="comment">//...</span></span><br></pre></td></tr></table></figure><p>一旦我们有了合适的循环语句,最后一件需要做的事是 <code>return self</code>。如果视图是可被点击(被我们的 <code>guard</code> 语句断言过的情况),但却没有子视图想要处理这个触摸的话,意味着当前视图,也就是 <code>self</code>,是这个触摸正确的目标。</p><p>这是完整的算法:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">hitTest</span><span class="params">(<span class="number">_</span> point: CGPoint, with event: UIEvent?)</span></span> -> <span class="type">UIView?</span> {</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> isUserInteractionEnabled <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> !isHidden <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> alpha >= <span class="number">0.01</span> <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> <span class="keyword">self</span>.point(inside: point, with: event) <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> subview <span class="keyword">in</span> subviews.reversed() {</span><br><span class="line"><span class="keyword">let</span> convertedPoint = subview.convert(point, from: <span class="keyword">self</span>)</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">let</span> candidate = subview.hitTest(convertedPoint, with: event) {</span><br><span class="line"><span class="keyword">return</span> candidate</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">self</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在我们有了一个参考的实现,可以开始修改它来实现具体的行为。</p><p>在之前的这篇播客<a href="http://khanlou.com/2013/04/changing-the-size-of-a-paging-scroll-view/" target="_blank" rel="noopener">《Changing the size of a paging scroll view》</a>中,我就已经讨论过其中一种行为。我谈到一种“落后并该被废弃”的方法来产生这种效果。本质上,你必须:</p><ol><li>关掉 <code>clipsToBounds</code></li><li>在滑动区域中放一个非隐藏视图</li><li>在非隐藏视图上重写 <code>-hitTest:</code> 来传递所有触摸到 scrollview 中</li></ol><p><code>-hitTest:</code> 方法是这种技术的基石。因为在 UIKit 中,hitTest 方法会代理给每一个视图去实现,决定触摸事件传递给哪个视图接收。这可以让你去重写默认的实现(期望和普通的实现)并替换它为你想做的,甚至返回一个不是原始视图的子视图。多么疯狂。</p><p>让我们看一下另一个例子。如果你已经用过 <a href="http://beacon.party/" target="_blank" rel="noopener">Beacon</a> 今年的版本,你会注意到滑动删除事件行为的物理效果感觉上和其他用原生系统实现的效果有点不一样。这是因为用系统的途径不能完全获得我们想要的表现,所以需要自己重新实现这个功能。</p><p>如你所想,重写滑动和反弹物理效果不需要那么复杂,所以我们用一个 <code>UIScrollView</code> 和将 <code>pagingEnabled</code> 设为 true 来获得尽可能自由的反弹力。用和<a href="http://khanlou.com/2013/04/changing-the-size-of-a-paging-scroll-view/" target="_blank" rel="noopener">这篇旧博客</a>里说的类似的技术,将滑动的视图的 <code>bounds</code> 设置得更小一些并将 <code>panGestureRecognizer</code> 移到事件的 cell 顶层的一个覆盖视图中,来设置一个自定义页面大小。</p><p>然而,当覆盖视图正确的传递触摸事件到 scroll view 时,那里会有覆盖视图不能正确拦截的其他事件。cell 包含着按钮,像 “join event” 按钮和 “delete event” 按钮,都需要接收触摸。有几种自定义实现在 <code>-hitTest:</code> 中可以处理这种情况,其中一种实现就是直接检查这两个按钮的子视图:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">hitTest</span><span class="params">(<span class="number">_</span> point: CGPoint, with event: UIEvent?)</span></span> -> <span class="type">UIView?</span> {</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> isUserInteractionEnabled <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> !isHidden <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> alpha >= <span class="number">0.01</span> <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">guard</span> <span class="keyword">self</span>.point(inside: point, with: event) <span class="keyword">else</span> { <span class="keyword">return</span> <span class="literal">nil</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> joinButton.point(inside: convert(point, to: joinButton), with: event) {</span><br><span class="line"><span class="keyword">return</span> joinButton</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> isDeleteButtonOpen && deleteButton.point(inside: convert(point, to: deleteButton), with: event) {</span><br><span class="line"><span class="keyword">return</span> deleteButton</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">super</span>.hitTest(point, with: event)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种方法会正确地传递正确的点击事件到正确的的按钮中,而且不用打断显示删除按钮的滑动表现。(你可以尝试只忽略 <code>deletionOverlay</code>,不过它不会正确的传递滑动事件。)</p><p><code>-hitTest:</code> 是视图中一个很少重写的地方,但是在需要时,可以提供其他工具很难做到的行为。理解如何自己实现有助于随意替换它。你可以用这个技术去扩大点击的目标区域,去除触摸处理中的某些子视图,而不用把它们从可见的层级中去掉,又或是用一个视图作为另一个将响应触摸的视图的兜底。所有东西都是可能的。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>作者:Soroush Khanlou,<a href="http://khanlou.com/2018/09/hacking-hit-tests/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09-07<br>译者:<a href="https://nemocdz.github.io/" target="_blank" rel="noopener">Nemocdz</a>;校对:<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p>
</blockquote>
<!--此处开始正文-->
<p>回想 <a href="https://developer.apple.com/videos/play/wwdc2015/408/" target="_blank" rel="noopener">Crusty 教我们使用面向协议编程</a>之前的日子,我们大多使用继承来共享代码的实现。通常在 UIKit 编程中,你可能会用 <code>UIView</code> 的子类去添加一些子视图,重写 <code>-layoutSubviews</code>,然后重复这些工作。也许你还会重写 <code>-drawRect</code>。但当你需要做一些特别的事情时,就需要看看 <code>UIView</code> 中其他可以被重写的方法。</p>
</summary>
<category term="KHANLOU" scheme="https://swift.gg/categories/KHANLOU/"/>
<category term="iOS开发" scheme="https://swift.gg/tags/iOS%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>固定大小的数组</title>
<link href="https://swift.gg/2018/12/24/fixed-sized-arrays/"/>
<id>https://swift.gg/2018/12/24/fixed-sized-arrays/</id>
<published>2018-12-24T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Russ Bishop,<a href="http://www.russbishop.net/fixed-sized-arrays" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-10-30<br>译者:<a href="https://github.com/zhongWJ" target="_blank" rel="noopener">zhongWJ</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="https://github.com/Cee" target="_blank" rel="noopener">Cee</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>假设我们想要用 <code>statfs()</code> 方法来确定某个挂载点所对应的 <code>BSD</code> 设备名。例如挂载点 <code>/Volumes/MyDisk</code> 对应的 <code>BSD</code> 设备是 <code>/dev/disk6s2</code>。</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">statfs</span> <span class="title">fsinfo</span>;</span></span><br><span class="line"><span class="keyword">if</span> (statfs(path, &fsinfo) != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">//错误</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><a id="more"></a><p>同等的 Swift 代码如下,只不过多了个 <code>POSIX</code> 错误帮助方法:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">posix_expects_zero</span><R: BinaryInteger><span class="params">(<span class="number">_</span> f: @autoclosure <span class="params">()</span></span></span> <span class="keyword">throws</span> -> <span class="type">R</span>) <span class="keyword">throws</span> {</span><br><span class="line"> <span class="keyword">let</span> returncode = <span class="keyword">try</span> f()</span><br><span class="line"> <span class="keyword">if</span> returncode != <span class="number">0</span> {</span><br><span class="line"> <span class="comment">// 如果需要,请在此处替换为自定义的错误类型。</span></span><br><span class="line"> <span class="comment">// NSError 会自动帮我们通过错误码得到对应的 C 字符串错误消息。</span></span><br><span class="line"> <span class="keyword">throw</span> <span class="type">NSError</span>(</span><br><span class="line"> domain: <span class="type">NSPOSIXErrorDomain</span>,</span><br><span class="line"> code: <span class="built_in">numericCast</span>(returncode),</span><br><span class="line"> userInfo: <span class="literal">nil</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 采用默认的空初始化方法。Swift 能推断出结构体类型,</span></span><br><span class="line"><span class="comment">// 但为了表示得更清楚,这里显式指定类型</span></span><br><span class="line"><span class="keyword">var</span> fsinfo: statfs = statfs()</span><br><span class="line">statfs(path, &fsinfo)</span><br></pre></td></tr></table></figure><h2 id="C-的引入物"><a href="#C-的引入物" class="headerlink" title="C 的引入物"></a>C 的引入物</h2><p><code>statfs()</code> 函数在 C 语言的定义是 <code>int statfs(const char *path, struct statfs *info)</code>。<code>statfs</code> 结构体有多个字段,但我们只关注 <code>mount-from-name</code>:</p><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">statfs</span> {</span></span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="keyword">char</span> f_mntfromname[MAXPATHLEN];</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>在苹果平台上 MAXPATHLEN == PATH_MAX == 1024。</p><p>如果你在代码里硬编码 1024 而不是用更合适的宏,小心我的鬼魂会缠着你和你的家族十二代哦。</p></blockquote><p>噢哦。一个固定大小的数组。当被引入 Swift 中时,它会被当做一个有 1024 个元素的元组:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">var</span> f_mntfromname: (<span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>, <span class="type">Int8</span>)</span><br></pre></td></tr></table></figure><p>这个类型不是很实用。那我们能不能做点什么呢?由于这篇博客的存在,你可能已经猜到答案是「<em>是</em>」。</p><h2 id="C-字符串"><a href="#C-字符串" class="headerlink" title="C 字符串"></a>C 字符串</h2><p>这个固定大小的数组包含了 <code>char</code>。由于文档未曾提及,所以我们并不知道是否通过空字符来表示数组的终止。令人恼火的是,文档暗示在 64 位系统上,<code>f_fstypename</code> 字段是由空字符终止,但对于 mount to/from 两个字段却只字未提。这两个字段是根据被定义为 <code>PATH_MAX</code> 的 <code>MAXPATHLEN</code> 宏来定义的而不是直接根据 <code>PATH_MAX</code> 宏,而 <code>PATH_MAX</code> 宏通常暗示数组由空字符来表示终止。我们是不是应该从中得到一点启发呢?</p><p>对于固定大小的数组,有一些 C API 仍然采用空终止符(所以真实的字符串长度最长可以是 <code>sizeof(array) - 1</code>),而另一些则乐意填充整个缓冲区(所以不以空字符结尾的字符串长度最长可以是 <code>sizeof(array)</code>)。这就是那种有害的绊脚石,它让你的程序看起来运行正常并通过所有测试,然后碰到某些新的 FizzyWizz 硬盘系统会有的奇怪的边界情况时,当突然碰到一个刚好由 1024 个字符组成的名称特别长的 BSD 设备时,结果就是你的程序出现了「<em>可利用内存损坏</em>」错误。</p><p>这些值很有可能是空字符终止的(也可能不是),但我会告诉你如何来处理这个问题,以便在两种情况下都适用。这意味着我们再也不需要考虑这个问题,从而降低大脑负荷。在其他场景复用这个代码的人也不需要再考虑这个问题了。既然有这么大的好处,何不马上开始?</p><p>偏题了,让我们回到正题……</p><h2 id="部分解决方案"><a href="#部分解决方案" class="headerlink" title="部分解决方案"></a>部分解决方案</h2><p>首先我们需要计算字段的偏移量。用新的 <code>MemoryLayout.offset</code> 方法可以得到结果:<code>MemoryLayout<statfs>.offset(of: \Darwin.statfs.f_mntfromname)!</code>。由于结构体和函数名字相同,当我们构造关键路径时,需要提供完整的路径名(fully-qualified name),否则会得到「<em>无法确定有歧义的引用路径</em>」的错误。我们可以强制解包返回值因为我们知道关键路径有效并且字段有偏移量。</p><p>将字段的内存布局偏移量加上 <code>withUnsafePointer</code> 指针就可以得到一个指向结构体字段起始内存的指针。我们可以通过这种方式创建一个字符串对象:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">return</span> <span class="built_in">withUnsafePointer</span>(to: fsinfo, { (ptr) -> <span class="type">String?</span> <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> offset = <span class="type">MemoryLayout</span><statfs>.offset(of: \<span class="type">Darwin</span>.statfs.f_mntfromname)!</span><br><span class="line"> <span class="keyword">let</span> fieldPtr = (<span class="type">UnsafeRawPointer</span>(ptr) + offset).assumingMemoryBound(to: <span class="type">UInt8</span>.<span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">if</span> fieldPtr[<span class="built_in">count</span> - <span class="number">1</span>] != <span class="number">0</span> {</span><br><span class="line"> <span class="keyword">let</span> data = <span class="type">Data</span>(bytes: <span class="type">UnsafeRawPointer</span>(fieldPtr), <span class="built_in">count</span>: <span class="built_in">count</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="type">String</span>(data: data, encoding: .utf8)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">String</span>(cString: fieldPtr)</span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>我们首先快速检查了缓冲区是否是空字符结尾。如果是,就采用 C 字符串这条捷径。反之,为了便于使用 String 的长度限制构造方法,我们创建了一个 Data 实例。看起来用 <code>Data(bytesNoCopy:count:deallocator:)</code> 也行,但 <code>String(data:encoding:)</code> 初始化方法并不保证拷贝 Data 底层的缓冲区,<code>Data</code> 的构造过程也同样如此。虽然这种情况极少见,我们还是谨慎为好。(假如目前的方法会导致性能问题,我可能会花时间调研其他方案。)</p><p>有一种可能情况是,先写入由空字符终止的较短的字符串到缓冲区,剩余的部分则是被垃圾数据填充。由于 Swift 在初始化结构体时会强制清除内存,所以只有当内核拷贝垃圾数据到这块地址时,上述情况才可能发生。我们可以忽略这种情况,因为内核会尽量避免泄漏内核内存到用户空间,否则我们就只能用更耗时的方式了。(将这些字节转换为字符串的方式数不胜数,我这里就不一一列举了。)</p><p>现在我们来实现 <code>statfs</code> 的扩展:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">statfs</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> mntfromname: <span class="type">String?</span> {</span><br><span class="line"> <span class="keyword">mutating</span> <span class="keyword">get</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">withUnsafePointer</span>(to: fsinfo, { (ptr) -> <span class="type">String?</span> <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> offset = <span class="type">MemoryLayout</span><statfs>.offset(of: \<span class="type">Darwin</span>.statfs.f_mntfromname)!</span><br><span class="line"> <span class="keyword">let</span> fieldPtr = (<span class="type">UnsafeRawPointer</span>(ptr) + offset).assumingMemoryBound(to: <span class="type">UInt8</span>.<span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">let</span> <span class="built_in">count</span> = <span class="type">Int</span>(<span class="type">MAXPATHLEN</span>)</span><br><span class="line"> <span class="keyword">if</span> fieldPtr[<span class="built_in">count</span> - <span class="number">1</span>] != <span class="number">0</span> {</span><br><span class="line"> <span class="keyword">let</span> data = <span class="type">Data</span>(bytes: <span class="type">UnsafeRawPointer</span>(fieldPtr), <span class="built_in">count</span>: <span class="built_in">count</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="type">String</span>(data: data, encoding: .utf8)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">String</span>(cString: fieldPtr)</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这段代码很管用,但假如我们也想处理别的字段,例如 <code>f_mntoname</code> 呢?复制代码似乎不怎么好,所以让这段代码支持泛型,使之更加通用才对;我们只需要接受 key path 和 count 作为参数,再稍作修改就可以了:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">fixedArrayToString</span><T><span class="params">(t: T, keyPath: PartialKeyPath<T>, <span class="built_in">count</span>: Int)</span></span> -> <span class="type">String?</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">withUnsafePointer</span>(to: t) { (ptr) -> <span class="type">String?</span> <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> offset = <span class="type">MemoryLayout</span><<span class="type">T</span>>.offset(of: keyPath)!</span><br><span class="line"> <span class="keyword">let</span> fieldPtr = (<span class="type">UnsafeRawPointer</span>(ptr) + offset).assumingMemoryBound(to: <span class="type">UInt8</span>.<span class="keyword">self</span>)</span><br><span class="line"> <span class="keyword">if</span> fieldPtr[<span class="built_in">count</span> - <span class="number">1</span>] != <span class="number">0</span> {</span><br><span class="line"> <span class="keyword">let</span> data = <span class="type">Data</span>(bytes: <span class="type">UnsafeRawPointer</span>(fieldPtr), <span class="built_in">count</span>: <span class="built_in">count</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="type">String</span>(data: data, encoding: .utf8)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">String</span>(cString: fieldPtr)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">extension</span> <span class="title">statfs</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> mntfromname: <span class="type">String?</span> {</span><br><span class="line"> <span class="keyword">get</span> {</span><br><span class="line"> <span class="keyword">return</span> fixedArrayToString(</span><br><span class="line"> t: <span class="keyword">self</span>,</span><br><span class="line"> keyPath: \<span class="type">Darwin</span>.statfs.f_mntfromname,</span><br><span class="line"> <span class="built_in">count</span>: <span class="type">Int</span>(<span class="type">MAXPATHLEN</span>))</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> mntonname: <span class="type">String?</span> {</span><br><span class="line"> <span class="keyword">get</span> {</span><br><span class="line"> <span class="keyword">return</span> fixedArrayToString(</span><br><span class="line"> t: <span class="keyword">self</span>,</span><br><span class="line"> keyPath: \<span class="type">Darwin</span>.statfs.f_mntonname,</span><br><span class="line"> <span class="built_in">count</span>: <span class="type">Int</span>(<span class="type">MAXPATHLEN</span>))</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>现在你知道怎么把有 N 个元素的元组变成更实用的东西了吧。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
如何在 Swift 中处理从 C 语言引入的固定大小数组
</summary>
<category term="Swift" scheme="https://swift.gg/categories/Swift/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
</entry>
<entry>
<title>Swift 属性观察器</title>
<link href="https://swift.gg/2018/12/17/swift-property-observers/"/>
<id>https://swift.gg/2018/12/17/swift-property-observers/</id>
<published>2018-12-17T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Mattt,<a href="https://nshipster.com/swift-property-observers/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-08-20<br>译者:<a href="http://wuqiuhao.github.io" target="_blank" rel="noopener">Hale</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://www.jianshu.com/users/596f2ba91ce9/latest_articles" target="_blank" rel="noopener">pmst</a>,<a href="http://blog.yousanflics.com.cn" target="_blank" rel="noopener">Yousanflics</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>到了 20 世纪 30 年代,Rube Goldberg 已成为家喻户晓的名字,与 <a href="https://upload.wikimedia.org/wikipedia/commons/a/a9/Rube_Goldberg%27s_%22Self-Operating_Napkin%22_%28cropped%29.gif" target="_blank" rel="noopener">“自营餐巾”</a> 等漫画中描绘的奇异复杂和异想天开的发明同义。大约在同一时期,阿尔伯特·爱因斯坦对尼尔斯·玻尔量子力学的普遍解释进行了 <a href="https://en.wikipedia.org/wiki/EPR_paradox" target="_blank" rel="noopener">批判</a>,并从中提出了“鬼魅似的远距作用”这一词汇。</p><p>近一个世纪之后,现代软件开发已经被视为可能成为 Goldbergian 装置的典范——通过量子计算机相信我们会越来越接近这个鬼魅的领域。</p><p>作为软件开发人员,我们提倡尽可能减少代码中的远程操作。这是根据一些众所周知的规范法则得出的,如 <a href="https://en.wikipedia.org/wiki/Single_responsibility_principle" target="_blank" rel="noopener">单一职责原则</a>、<a href="https://en.wikipedia.org/wiki/Principle_of_least_astonishment" target="_blank" rel="noopener">最少意外原则</a> 和 <a href="https://en.wikipedia.org/wiki/Law_of_Demeter" target="_blank" rel="noopener">笛米特法则</a>。尽管它们可能会对代码产生一定的副作用,但更多的时候这些原则能使代码逻辑变得清晰。</p><p>这是本周关于 Swift 属性观察文章的焦点,它提出了一种内置的轻量级替代方案,适用于更正式的解决方案,如模型 - 视图 - 视图模型(MVVM)函数响应式编程(FRP)。</p><a id="more"></a><p>Swift 中有两种属性:<em>存储属性</em>,它们将状态和对象相关联;<em>计算属性</em>,则根据该状态执行计算。例如,</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">S</span> </span>{</span><br><span class="line"> <span class="comment">// 存储属性</span></span><br><span class="line"> <span class="keyword">var</span> stored: <span class="type">String</span> = <span class="string">"stored"</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 计算属性</span></span><br><span class="line"> <span class="keyword">var</span> computed: <span class="type">String</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"computed"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当你声明一个存储属性,你可以使用闭包定义一个 <em>属性观察器</em>,该闭包中的代码会在属性被设值的时候执行。<code>willSet</code> 观察器会在属性被赋新值之前被运行,<code>didSet</code> 观察器则会在属性被赋新值之后运行。无论新值是否等于属性的旧值它们都会被执行。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">S</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> stored: <span class="type">String</span> {</span><br><span class="line"> <span class="keyword">willSet</span> {</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"willSet was called"</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"stored is now equal to \(self.stored)"</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"stored will be set to \(newValue)"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">didSet</span> {</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"didSet was called"</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"stored is now equal to \(self.stored)"</span>)</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"stored was previously set to \(oldValue)"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>例如,运行下面的代码在控制台的输出如下:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> s = <span class="type">S</span>(stored: <span class="string">"first"</span>)</span><br><span class="line">s.stored = <span class="string">"second"</span></span><br></pre></td></tr></table></figure><ul><li>willSet was called</li><li>stored is now equal to first</li><li>stored will be set to second</li><li>didSet was called</li><li>stored is now equal to second</li><li>stored was previously set to first</li></ul><blockquote><p>需要注意的是当属性在初始化方法中进行赋值时,不会触发观察器的代码。从 Swift4.2 开始,你可以将赋值逻辑包装在 <code>defer</code> 代码块来解决这个问题,但这是 <a href="https://twitter.com/jckarter/status/926459181661536256" target="_blank" rel="noopener">一个很快就会被修复的问题</a>,因此你不需要依赖于这种行为。</p></blockquote><p>Swift 的属性观察器从一开始就是语言的一部分。为了更好地理解其原理,让我们快速了解一下它在 Objective-C 中的工作原理。</p><h1 id="Objective-C-中的属性"><a href="#Objective-C-中的属性" class="headerlink" title="Objective-C 中的属性"></a>Objective-C 中的属性</h1><p>从某种意义上说,Objective-C 中的所有属性都是被计算出来的。每次通过点语法访问属性时,都会转换为等效的 getter 或 setter 方法调用。这些调用最终被编译成消息发送,随后再执行读取或写入实例变量的方法。</p><figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 点语法访问</span></span><br><span class="line">person.name = <span class="string">@"Johnny"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...等价于</span></span><br><span class="line">[person setName:<span class="string">@"Johnny"</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...它被编译成</span></span><br><span class="line">objc_msgSend(person, <span class="keyword">@selector</span>(setName:), <span class="string">@"Johnny"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...最终实现</span></span><br><span class="line">person->_name = <span class="string">@"Johnny"</span>;</span><br></pre></td></tr></table></figure><p>编程过程中我们通常想要避免引入副作用,因为它会导致难以推断程序的行为。但很多 Objective-C 开发者已经依赖于这种特性,他们会根据需要在 getter 或 setter 中注入各种额外的行为。</p><p>Swift 的属性设计使这些模式更加标准化,并对装饰状态访问(存储属性)的副作用和重定向状态访问(计算属性)的副作用进行了区分。对于存储属性,<code>willSet</code> 和 <code>didSet</code> 观察器将替换你在 ivar 访问时的代码。对于计算属性,<code>get</code> 和 <code>set</code> 访问器可能会替换在 Objective-C 中实现的一些 <code>@dynamic</code> 属性。</p><p>正因为如此,我们才可以获取更一致的语义,并更好地保证键值观察(KVO)和健值编码(KVC)等属性交互机制。</p><p>那么你可以使用 Swift 属性观察器做些什么呢?以下是一些供你参考的想法:</p><h1 id="标准化或验证值"><a href="#标准化或验证值" class="headerlink" title="标准化或验证值"></a>标准化或验证值</h1><p>有时,你希望对类型确定的值增加额外的约束。</p><p>例如,你正在开发一个和政府机构对接的应用程序,你需要保证用户填写了所有的必填项并且不包含非法的值才能提交表单。</p><p>如果一个表单要求名称字段使用大写字母且不使用重音符号,你可以使用 <code>didSet</code> 属性观察器自动去除重音符号并转化为大写。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> name: <span class="type">String?</span> {</span><br><span class="line"> <span class="keyword">didSet</span> {</span><br><span class="line"> <span class="keyword">self</span>.name = <span class="keyword">self</span>.name?</span><br><span class="line"> .applyingTransform(.stripDiacritics,</span><br><span class="line"> <span class="built_in">reverse</span>: <span class="literal">false</span>)?</span><br><span class="line"> .uppercased()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>幸运的是在观察器内部设置属性不会触发额外的回调,所以上面的代码中不会产生无限循环。我们之所以不使用 <code>willSet</code> 观察器是因为即使我们在其回调中进行任何赋值,都会在属性被赋予 <code>newValue</code> 时覆盖。</p><p>虽然这种方法可以解决一次性问题,但像这样需要重复使用的业务逻辑可以封装到一个类型中。</p><p>更好的设计是创建一个 <code>NormalizedText</code> 类型,它封装了要以这种形式输入的文本的规则:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">NormalizedText</span> </span>{</span><br><span class="line"> <span class="class"><span class="keyword">enum</span> <span class="title">Error</span>: <span class="title">Swift</span>.<span class="title">Error</span> </span>{</span><br><span class="line"> <span class="keyword">case</span> empty</span><br><span class="line"> <span class="keyword">case</span> excessiveLength</span><br><span class="line"> <span class="keyword">case</span> unsupportedCharacters</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">let</span> maximumLength = <span class="number">32</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> value: <span class="type">String</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">init</span>(<span class="number">_</span> string: <span class="type">String</span>) <span class="keyword">throws</span> {</span><br><span class="line"> <span class="keyword">if</span> string.isEmpty {</span><br><span class="line"> <span class="keyword">throw</span> <span class="type">Error</span>.empty</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> value = string.applyingTransform(.stripDiacritics,</span><br><span class="line"> <span class="built_in">reverse</span>: <span class="literal">false</span>)?</span><br><span class="line"> .uppercased(),</span><br><span class="line"> value.canBeConverted(to: .ascii)</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="type">Error</span>.unsupportedCharacters</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">guard</span> value.<span class="built_in">count</span> < <span class="type">NormalizedText</span>.maximumLength <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="type">Error</span>.excessiveLength</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">self</span>.value = value</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>一个可抛出异常的初始化方法可以向调用者发送错误信息,这是 <code>didSet</code> 观察器无法做到的。现在面对 <a href="https://zh.wikipedia.org/wiki/%E5%85%B0%E9%9F%A6%E5%B0%94%E6%99%AE%E5%B0%94%E5%8F%A4%E5%9B%A0%E5%90%89%E5%B0%94%E6%88%88%E6%A0%BC%E9%87%8C%E6%83%A0%E5%B0%94%E6%81%A9%E5%BE%B7%E7%BD%97%E5%B8%83%E5%B0%94%E5%85%B0%E8%92%82%E8%A5%BF%E5%88%A9%E5%A5%A5%E6%88%88%E6%88%88%E6%88%88%E8%B5%AB" target="_blank" rel="noopener">兰韦尔普尔古因吉尔戈格里惠尔恩德罗布尔兰蒂西利奥戈戈戈赫</a> 的 <em>约翰尼</em> 这样的麻烦制造者,我们能为他做些什么!(换言之,以合理的方式传达错误比提供无效的数据更好)</p><h1 id="传播依赖状态"><a href="#传播依赖状态" class="headerlink" title="传播依赖状态"></a>传播依赖状态</h1><p>属性观察器的另一个潜在用例是将状态传播到依赖于视图控制器的组件。</p><p>考虑下面的 <code>Track</code> 模型示例和一个呈现它的 <code>TrackViewController</code>:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Track</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> title: <span class="type">String</span></span><br><span class="line"> <span class="keyword">var</span> audioURL: <span class="type">URL</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TrackViewController</span>: <span class="title">UIViewController</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> player: <span class="type">AVPlayer?</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> track: <span class="type">Track?</span> {</span><br><span class="line"> <span class="keyword">willSet</span> {</span><br><span class="line"> <span class="keyword">self</span>.player?.pause()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">didSet</span> {</span><br><span class="line"> <span class="keyword">guard</span> <span class="keyword">let</span> track = <span class="keyword">self</span>.track <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">self</span>.title = track.title</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> item = <span class="type">AVPlayerItem</span>(url: track.audioURL)</span><br><span class="line"> <span class="keyword">self</span>.player = <span class="type">AVPlayer</span>(playerItem: item)</span><br><span class="line"> <span class="keyword">self</span>.player?.play()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当视图控制器的 <code>track</code> 属性被赋值,以下事情会自动发生:</p><ul><li>之前轨道的音频都会暂停</li><li>视图控制器的 <code>title</code> 会被设置为新轨道对象的标题</li><li>新轨道对象的音频信息会被加载并播放</li></ul><p>很酷, 对吗?</p><p>你甚至可以像 <a href="https://www.youtube.com/watch?v=TVAhhVrpkwM" target="_blank" rel="noopener"><em>捕鼠记</em> 中描绘的场景</a> 一样,将行为与多个观察属性级联起来。</p><p>当然,观察器也存在一定的副作用,它使得有些复杂的行为难以被推断,这是我们在编程中需要避免的。今后在使用这一特性的同时也需要注意这一点。</p><p>然而,在这摇摇欲坠的抽象塔的顶端,一定限度的系统混乱是诱人的,有时是值得的。一直遵循规则的是波尔理论而非爱因斯坦。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
本文介绍了 Swift 中的属性观察器,并介绍了一些比较常见的适用场景。
</summary>
<category term="Swift" scheme="https://swift.gg/categories/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/categories/Swift/NSHipster/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
<category term="NSHipster" scheme="https://swift.gg/tags/NSHipster/"/>
</entry>
<entry>
<title>布隆过滤器与 Swift 4.2</title>
<link href="https://swift.gg/2018/12/13/bloom-filters/"/>
<id>https://swift.gg/2018/12/13/bloom-filters/</id>
<published>2018-12-13T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Soroush Khanlou,<a href="http://khanlou.com/2018/09/bloom-filters/" target="_blank" rel="noopener">原文链接</a>,原文日期:2018-09-19<br>译者:<a href="https://github.com/WAMaker" target="_blank" rel="noopener">WAMaker</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://linusling.com" target="_blank" rel="noopener">小铁匠Linus</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>Swift 4.2 为哈希的实现带来了一些新的变化。在此之前,哈希交由对象本身全权代理。当你向对象索取 <code>哈希值(hashValue)</code>时,它会把处理好的整型值作为哈希值返回。而现在,实现了 <code>Hashable</code> 协议的对象则描述了它的参数是如何组合,并传递给作为入参的 <code>Hasher</code> 对象。这样做有以下几点好处:</p><ul><li>写出好的哈希算法很难。Swift 的使用者不需要知道如何组合参数来获得更好的哈希值。</li><li>出于不提倡用户以任何形式存储哈希值,以及 <a href="https://twitter.com/jckarter/status/1042453831496327168" target="_blank" rel="noopener">一些安全方面因素</a> 的考虑,哈希值在程序每次运行的时候都应该有所不同。描述性的哈希允许哈希函数的种子在每次程序运行的时候发生改变。</li><li>能实现更多有意思的数据结构,这也是我们这篇文章接下来会聚焦的。</li></ul><a id="more"></a><p>我之前写过一篇关于 <a href="http://khanlou.com/2016/07/implementing-dictionary-in-swift/" target="_blank" rel="noopener">如何使用 Swift 的 <code>Hashable</code> 协议从零实现 <code>Dictionary</code></a> 的文章(先阅读它会帮助你阅读本文,但这不是必须的)。今天,我想谈论一种不同类型的,基于概率性而非明确性的数据结构:布隆过滤器(Bloom Filters)。我们会使用 Swift 4.2 的新特性,包括新的哈希模型来构建它。</p><p>布隆过滤器很怪异。想象这样一种数据结构:</p><ul><li>你能够往里插入数据</li><li>你能够查询一个值是否存在</li><li>只需要少量存储资源就能存储大量对象</li></ul><p>但是:</p><ul><li>你不能枚举其中的对象</li><li>它有时会出现误报(但不会出现漏报)</li><li>你不能从中移除数据</li></ul><p>什么时候会想要这种数据结构呢?Medium 使用它们来 <a href="https://blog.medium.com/what-are-bloom-filters-1ec2a50c68ff" target="_blank" rel="noopener">跟踪博文的阅读状态</a>。必应使用它们做 <a href="https://www.youtube.com/watch?v=80LKF2qph6I" target="_blank" rel="noopener">搜索索引</a>。你可以使用它们来构建一个缓存,在无需访问数据库的情况下就能判断用户名是否有效(例如在 @-mention 中)。像服务器这样可能拥有巨大的规模,却不一定有巨大资源的场景中,它们会非常有用。</p><p>(如果你之前做过图形方面的工作,可能好奇它是如何与 <a href="https://en.wikipedia.org/wiki/Bloom_(shader_effect" target="_blank" rel="noopener">高光过滤器</a>) 产生联系的。答案是没有联系。高光过滤器(bloom filters)是小写的 b,而布隆过滤器(Bloom Filters)是由一个叫布隆的人命名的。完全是个巧合。)</p><p>那它们是如何运作的呢?</p><p>将对象放入布隆过滤器如同将它放入集合或字典:计算对象的哈希值,并根据存储数组的大小对哈希值求余。就这点而言,使用布隆过滤器只需要修改该索引处的值:将 false 改为 true,而不用像使用集合或字典那样,把对象存放到索引位置。</p><p>我们先通过一个简单的例子来理解过滤器是如果运作的,之后再对它进行扩展。想象一个拥有 8 个 false 值的布尔数组(或称之为 <a href="https://gist.github.com/natecook1000/552dc3d23d2fc4a54d2e9fcd309e59e9" target="_blank" rel="noopener">比特数组</a>):</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |</span><br><span class="line">---------------------------------</span><br><span class="line">| | | | | | | | |</span><br></pre></td></tr></table></figure><p>它代表了我们的布隆过滤器。我想要插入字符串“soroush”。它的哈希值是 9192644045266971309,当这个值余 8 时得到 5。我们修改那一位的值。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |</span><br><span class="line">---------------------------------</span><br><span class="line">| | | | | | * | | |</span><br></pre></td></tr></table></figure><p>接下来我想要插入字符串“swift”,它的哈希值是 7052914221234348788,余 8 得 4,修改索引 4 的值。</p><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |</span><br><span class="line">---------------------------------</span><br><span class="line">| | | | | * | * | | |</span><br></pre></td></tr></table></figure><p>要测试布隆过滤器是否包含“soroush”,我再次计算它的哈希值并求余,仍旧得到余数 5,对应值是 true。“soroush”确实在布隆过滤器中。</p><p>然而仅仅测试能够通过的用例是不够的,我们需要写一些会导致失败的用例。测试字符串“khanlou”取余得到的索引值是 2,因此我们知道它不在布隆过滤器中。到此为止一切都好。接下去一个测试:对“hashable”字符串取余得到的索引值是 5,这就发生了一次冲突!即使这个值从来没有被加入过,过滤器仍返回了 true。这便是布隆过滤器会发生误报的例子。</p><p>有两个主要的策略可以尽可能减少误报。第一个策略,也是两个策略中相对有趣的:我们可以使用不同的哈希函数计算两次或三次哈希值而非一次。只要它们都是表现良好的哈希函数(均匀分布,一致性,最小的碰撞几率),我们就能为每个值生成多个索引改变布尔值。这次,我们计算两次“soroush”的哈希值,生成 2 个索引并改变布尔值。这时,当我们检查“khanlou”是否在布隆过滤器中,其中一个哈希值可能会和“soroush”的一个哈希值冲突,但两个值同时发生冲突的可能性就会变得很小。你可以把这个数字扩大。在下面的代码我会做 3 次哈希计算,但你可以做更多次。</p><p>当然,如果你计算更多次哈希值,每个元素在布尔数组中会占据更多的空间。事实上,现在的数据几乎不占用空间。8 个布尔值的数组对应 1 字节。所以第二个减小误报的策略是扩大数组的规模。我们能将数组变得足够大而不用担心它的空间消耗。下面的代码中我们会默认使用 512 比特大小的数组。</p><p>现在,即使同时使用这些策略,你依然会得到冲突,即误报,但冲突的几率会减小。这是布隆过滤器的一个缺陷,但在合适的场景用它来节省速度与空间是一笔不错的交易。</p><p>在开始具体的代码之前我有另外三点想要谈谈。首先,你不能改变布隆过滤器的大小。当你对哈希值取余时,这是在破坏信息,在不保留原始哈希值的情况下你不能回溯之前的信息 —— 保留原始值相当于否决了这个数据结构节约空间的优势。</p><p>其次,你能看到想要枚举布隆过滤器所有的值是多么异想天开。你不再拥有这些值,只是它们以哈希形式存在的替代品。</p><p>最后,你同样能看到想要从布隆过滤器中移除元素是不可能的。如果想将布尔值变回 false,你并不知道是哪些值将它变为 true。是准备移除的值还是其它值?这样做会造成漏报和误报。(这对你来说可能是值得权衡的)你可以在每个索引上保留计数而非布尔值来解决这个问题,虽然保留计数还是会带来存储问题,但根据使用场景的不同,这样做或许是值得的。</p><p>废话不多说,让我们开始着手编码。我在这里做的一些决策和你可能会做的有所不同,第一个不同就是要不要让对象支持范型。我认为让对象包含更多关于它需要存储内容的元数据是有意义的,但如果你发现这样做限制太多,你可以改变它。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BloomFilter</span><<span class="title">Element</span>: <span class="title">Hashable</span>> </span>{</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们需要存储两种主要的数据。第一个是 <code>data</code>,用于表示比特数组。它存储了所有和哈希值有关的标记:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">var</span> data: [<span class="type">Bool</span>]</span><br></pre></td></tr></table></figure><p>接下来,我们需要不同的哈希函数。一些布隆过滤器确实会使用不同的方法计算哈希值,但我觉得使用相同的算法,同时混入一个随机生成的值会更简单。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">let</span> seeds: [<span class="type">Int</span>]</span><br></pre></td></tr></table></figure><p>当初始化布隆过滤器时,我们需要初始化这两个实例变量。比特数组会简单的重复 <code>false</code> 值来初始化,而种子值则使用 Swift 4.2 的新 API <code>Int.random</code> 来生成我们需要的种子值。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">init</span>(size: <span class="type">Int</span>, hashCount: <span class="type">Int</span>) {</span><br><span class="line">data = <span class="type">Array</span>(repeating: <span class="literal">false</span>, <span class="built_in">count</span>: size)</span><br><span class="line">seeds = (<span class="number">0</span>..<hashCount).<span class="built_in">map</span>({ <span class="number">_</span> <span class="keyword">in</span> <span class="type">Int</span>.random(<span class="keyword">in</span>: <span class="number">0</span>..<<span class="type">Int</span>.<span class="built_in">max</span>) })</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时,创建一个带有默认值的便利构造器。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">init</span>() {</span><br><span class="line"><span class="keyword">self</span>.<span class="keyword">init</span>(size: <span class="number">512</span>, hashCount: <span class="number">3</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们要实现两个主要的方法:<code>insert</code> 和 <code>contains</code>。它们都需要接收元素作为参数并为每一个种子值计算出对应的哈希值。私有的帮助方法会很有用。由于种子值代表了“不同的”哈希函数,我们就需要为每一个种子生成对应的哈希值。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">hashes</span><span class="params">(<span class="keyword">for</span> element: Element)</span></span> -> [<span class="type">Int</span>] {</span><br><span class="line"><span class="keyword">return</span> seeds.<span class="built_in">map</span>({ seed -> <span class="type">Int</span> <span class="keyword">in</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">})</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>要实现函数主体,我们需要创建一个 <code>Hasher</code> 对象(Swift 4.2 新特性),将想要进行哈希计算的对象传给它。带上种子确保了生成的哈希值不会冲突。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">func</span> <span class="title">hashes</span><span class="params">(<span class="keyword">for</span> element: Element)</span></span> -> [<span class="type">Int</span>] {</span><br><span class="line"><span class="keyword">return</span> seeds.<span class="built_in">map</span>({ seed -> <span class="type">Int</span> <span class="keyword">in</span></span><br><span class="line"><span class="keyword">var</span> hasher = <span class="type">Hasher</span>()</span><br><span class="line">hasher.combine(element)</span><br><span class="line">hasher.combine(seed)</span><br><span class="line"><span class="keyword">let</span> hashValue = <span class="built_in">abs</span>(hasher.finalize())</span><br><span class="line"><span class="keyword">return</span> hashValue</span><br><span class="line">})</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时,注意哈希值的绝对值。哈希计算有可能产生负数,这会导致我们的数组访问崩溃。取绝对值操作减少了 1 比特的信息熵,对我们来说是有益的。</p><p>理想的情况是你能够使用种子来初始化 <code>Hasher</code> 而不是把它混合进去。Swift 的 <code>Hasher</code> 会在每次程序启动的时候被分配一个不同的种子(除非你 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0206-hashable-enhancements.md#effect-on-abi-stability" target="_blank" rel="noopener">设置固定的环境变量</a> 让种子在不同启动间保持一致,而这样做通常是一些测试目的),意味着你不能把这些值写到磁盘上。如果我们控制了 <code>Hasher</code> 的种子,我们就能将这些值写到磁盘上了。而就像这个布隆过滤器展示的那样,它应该只被用于内存缓存。</p><p><code>hashes(for:)</code> 方法完成了很多繁重的工作,让 <code>insert</code> 方法非常简洁:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">insert</span><span class="params">(<span class="number">_</span> element: Element)</span></span> {</span><br><span class="line">hashes(<span class="keyword">for</span>: element)</span><br><span class="line">.forEach({ hash <span class="keyword">in</span></span><br><span class="line">data[hash % data.<span class="built_in">count</span>] = <span class="literal">true</span></span><br><span class="line">})</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>生成所有的哈希值,分别余上 <code>data</code> 数组的长度,并设置对应索引位的值为 <code>true</code>。</p><p><code>contains</code> 方法也同样简单,同时也给了我们使用 Swift 4.2 另一个新特性 <code>allSatisfy</code> 的机会。这个新方法可以判断序列中的所有对象是否都通过了某项用 block 表示的测试:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">contains</span><span class="params">(<span class="number">_</span> element: Element)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"><span class="keyword">return</span> hashes(<span class="keyword">for</span>: element)</span><br><span class="line">.allSatisfy({ hash <span class="keyword">in</span></span><br><span class="line">data[hash % data.<span class="built_in">count</span>]</span><br><span class="line">})</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为 <code>data[hash % data.count]</code> 的结果已经是布尔值了,它与 <code>allSatisfy</code> 十分契合。</p><p>你也可以添加 <code>isEmpty</code> 方法用来检测 <code>data</code> 中的所有值是否都是 false。</p><p>布隆过滤器是一种奇怪的数据结构。我们接触的大多数数据结构都是明确性的。当把一个对象放入字典中时,你知道那个值之后一直在那儿。而布隆过滤器是概率性的,牺牲确定性来换取空间和速度。布隆过滤器不是你会每天用的数据结构,但当你确实需要它时,就会感受到有它真好。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
本文描述了如何使用 Swift 4.2 新特性 Hashable 实现布隆过滤器。
</summary>
<category term="Khanlou" scheme="https://swift.gg/categories/Khanlou/"/>
<category term="Swift" scheme="https://swift.gg/tags/Swift/"/>
</entry>
<entry>
<title>让我们构建一个Swift.Array</title>
<link href="https://swift.gg/2018/12/06/friday-qa-2015-04-17-lets-build-swiftarray/"/>
<id>https://swift.gg/2018/12/06/friday-qa-2015-04-17-lets-build-swiftarray/</id>
<published>2018-12-06T06:00:00.000Z</published>
<updated>2022-08-26T13:33:55.034Z</updated>
<content type="html"><![CDATA[<blockquote><p>作者:Mike Ash,<a href="https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html" target="_blank" rel="noopener">原文链接</a>,原文日期:2015-04-17<br>译者:<a href="https://github.com/dzyding" target="_blank" rel="noopener">灰s</a>;校对:<a href="http://numbbbbb.com/" target="_blank" rel="noopener">numbbbbb</a>,<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a>;定稿:<a href="http://forelax.space" target="_blank" rel="noopener">Forelax</a></p></blockquote><!--此处开始正文--><p>Swift 1.2 现已经作为 Xcode 6.3 的一部分而发布,在新的 API 中有一个允许我们使用值类型建立高效的数据结构,比如 Swift 标准库中的 <code>Array</code> 类型。今天,我们将重新现实 <code>Array</code> 的核心功能。</p><a id="more"></a><h2 id="值类型和引用类型"><a href="#值类型和引用类型" class="headerlink" title="值类型和引用类型"></a>值类型和引用类型</h2><p>在我们开始之前,快速的复习一下值类型和引用类型。在 objc 以及大部分其他面向对象的语言中,我们所使用对象的指针或者引用都属于 <strong>引用类型</strong>。你可以把一个对象的引用赋值给一个变量:</p><figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">MyClass *a = ...;</span><br></pre></td></tr></table></figure><p>现在你可以将这个变量的值复制给另一个变量:</p><figure class="highlight objc"><table><tr><td class="code"><pre><span class="line">MyClass *b = a;</span><br></pre></td></tr></table></figure><p>现在 a 和 b 都指向同一个对象。如果这个对象是可变的,那么对其中一个变量的改变同样会发生在另一个变量上。</p><p><strong>值类型</strong> 就是类似 Objective-C 中 <code>int</code> 这样的存在。使用值类型,变量包含的是真实的值,并不是值的引用。当你用 = 给另一个变量赋值,是将值的拷贝副本赋值给了另一个变量。比如:</p><figure class="highlight objc"><table><tr><td class="code"><pre><span class="line"><span class="keyword">int</span> a = <span class="number">42</span>;</span><br><span class="line"><span class="keyword">int</span> b = a;</span><br><span class="line">b++;</span><br></pre></td></tr></table></figure><p>现在,b 的值是 43,但是 a 依然是 42。 </p><p>在 Swift 中,<code>class</code> 属于引用类型,<code>struct</code> 属于值类型。<br>如果你使用 = 将一个 <code>class</code> 实例的引用赋值给另一个变量,你将得到这个实例的一个新引用。对这个实例的修改对每一个引用可见。<br>如果你使用 = 将一个 <code>struct</code> 实例的引用赋值给另一个变量,你将得到这个实例的一个副本,与原始数据相互独立。 </p><p>与大多数语言不同,Swift 标准库中的数组和字典都是值类型。比如:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]</span><br><span class="line"><span class="keyword">var</span> b = a</span><br><span class="line">b.append(<span class="number">4</span>)</span><br></pre></td></tr></table></figure><p>在大部分语言中,这段代码(或者等效的代码)运行以后, a 和 b 将都是一个指向数组 <code>[1, 2, 3, 4]</code> 的引用。但是在 Swift 中,a 是指向数组 <code>[1, 2, 3]</code> 的引用,b 是指向数组 <code>[1, 2, 3, 4]</code> 的引用。</p><h2 id="值类型的实现"><a href="#值类型的实现" class="headerlink" title="值类型的实现"></a>值类型的实现</h2><p>如果你的对象拥有的属性是固定的,在 Swift 中把它申明成值类型是很简单的:只需要把所有的属性放入一个 <code>struct</code> 中即可。比如,如果你需要一个 2D 的 <code>Point</code> 值类型,你可以简单的申明一个 <code>struct</code> 包含 x 和 y :</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Point</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> x: <span class="type">Int</span></span><br><span class="line"> <span class="keyword">var</span> y: <span class="type">Int</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>很快的,你就申明了一个值类型。但是如何实现类似 <code>Array</code> 这样的值类型呢?你无法把数组里面所有数据放入 <code>struct</code> 的申明中,因为在写代码的过程中你无法预料你将会在数组中放多少数据。你可以创建一个指针指向所有的数据:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Array</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="keyword">var</span> ptr: <span class="type">UnsafeMutablePointer</span><<span class="type">T</span>></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时,你需要在该 <code>struct</code> 每次<strong>分配</strong>和<strong>销毁</strong>的时候进行一些特殊操作。 </p><ul><li>在分配的过程中,你需要把包含的数据拷贝一份放到一个新的内存地址,这时新的 <code>struct</code> 就不会和原数据共享同一份数据了。 </li><li>在销毁的过程中,<code>ptr</code> 指针也需要正常销毁。 </li></ul><p>在 Swift 中不允许对 <code>struct</code> 的分配和销毁过程进行自定义。</p><p>销毁操作可以使用一个 <code>class</code> 实现,它提供了 <code>deinit</code>。同时可以在这里对指针进行销毁。<code>class</code> 并不是值类型,但是我们可以将 <code>class</code> 作为一个内部属性提供给 <code>struct</code> 使用,并把数组作为 <code>struct</code> 暴露给外部接口。看起来就像这样:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ArrayImpl</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="keyword">var</span> ptr: <span class="type">UnsafeMutablePointer</span><<span class="type">T</span>></span><br><span class="line"></span><br><span class="line"> <span class="keyword">deinit</span> {</span><br><span class="line"> ptr.destroy(...)</span><br><span class="line"> ptr.dealloc(...)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Array</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="keyword">var</span> impl: <span class="type">ArrayImpl</span><<span class="type">T</span>></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时在 <code>Array</code> 中申明的方法,它的实际操作都是在 <code>ArrayImpl</code> 上进行的。</p><p>到这里就可以结束了吗?尽管我们使用的是 <code>struct</code>, 但是使用的依旧是引用类型。如果将这个 <code>struct</code> 拷贝一份,我们将获得一个新的 <code>struct</code>,持有的仍然是之前的 <code>ArrayImpl</code>。由于我们无法自定义 <code>struct</code> 的分配过程,所以没有办法同样把 <code>ArrayImpl</code> 也拷贝一份。 </p><p>这个问题的解决方法是放弃在分配的过程中进行拷贝,而是在 <code>ArrayImpl</code> 发生改变的时候进行拷贝。关键在于,就算一个拷贝副本与原始数据共享一个引用,但是只要这个引用的数据不发生改变,值类型的语义就依旧成立。只有当这个共享数据的值发生改变时,值类型和引用类型才有了明显的区别。 </p><p>比如,在实现 <code>append</code> 方法的时候你可以先对 <code>ArrayImpl</code> 进行 <code>copy</code> (假设 <code>ArrayImpl</code> 的实现中有一个 <code>copy</code> 方法,那么将 <code>impl</code> 引用改为原始值的 <code>copy</code>): </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">append</span><span class="params">(value: T)</span></span> {</span><br><span class="line"> impl = impl.copy()</span><br><span class="line"> impl.append(value)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样 Array 就是一个值类型了。尽管 a 和 b 在刚赋值完时仍然共享同一个 <code>impl</code> 引用 ,但是任何会改变 <code>impl</code> 的方法都将对其进行一次 <code>copy</code>,因此保留了不共享数据的错觉。 </p><p>现在可以正常工作了,但是效率却非常低。比如: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> a: [<span class="type">Int</span>] = []</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="number">0</span>..<<span class="number">1000</span> {</span><br><span class="line"> a.append(i)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>尽管使用者无法看到它,但是它将在循环的每次迭代中复制内存中的数据,然后立即销毁之前的数据内存。如何才能优化它呢?</p><h2 id="isUniquelyReferenced"><a href="#isUniquelyReferenced" class="headerlink" title="isUniquelyReferenced"></a>isUniquelyReferenced</h2><p>这是一个 Swift 1.2 中新引入的 API。它漂亮的实现了它字面上的意思。赋予它一个对象的引用然后它将告诉你这个引用是否为独立的。具体来说,当这个对象有且仅有一个强引用时,就会返回 <code>true</code>。 </p><p>我们猜测这个 API 会检查对象的引用计数,并且在引用计数为 1 的时候返回 <code>true</code> 。那 Swift 为什么不直接提供一个接口来查询引用计数呢?可能在实现上这个接口不太好做,并且引用计数属于比较容易被滥用的信息,所以 Swift 提供了这个封装过的,更加安全的接口。</p><p>使用这个 API,之前对 <code>append</code> 方法的实现将可以改成只有在需要的时候才对内存中的数据进行复制: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">append</span><span class="params">(value: T)</span></span> {</span><br><span class="line"> <span class="keyword">if</span> !isUniquelyReferencedNonObjc(&impl) {</span><br><span class="line"> impl = impl.copy()</span><br><span class="line"> }</span><br><span class="line"> impl.append(value)</span><br><span class="line"> }</span><br><span class="line">``` </span><br><span class="line"></span><br><span class="line">这个 <span class="type">API</span> 实际上是一组三个方法中的一个。存在于 <span class="type">Xcode</span> 自带的 <span class="type">Swift</span> 标准库中: </span><br><span class="line"></span><br><span class="line">```swift</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">isUniquelyReferenced</span><T : NonObjectiveCBase><span class="params">(<span class="keyword">inout</span> object: T)</span></span> -> <span class="type">Bool</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">isUniquelyReferencedNonObjC</span><T><span class="params">(<span class="keyword">inout</span> object: T?)</span></span> -> <span class="type">Bool</span></span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">isUniquelyReferencedNonObjC</span><T><span class="params">(<span class="keyword">inout</span> object: T)</span></span> -> <span class="type">Bool</span></span><br></pre></td></tr></table></figure><p>这些方法只能作用在纯 Swift class 中,并不支持 @objc 类型。第一个方法必须确保 T 为 <code>NonObjectiveCBase</code> 的子类。另外两个方法对参数的类型并不做要求,只是当类型为 @objc 时直接返回 false。 </p><p>我无法让我的代码以 <code>NonObjectiveCBase</code> 类型来编译,所以使用了 <code>isUniquelyReferencedNonObjC</code> 来代替。从功能上来说,它们并没有区别。 </p><blockquote><p>译者注:<br>文章中所阐述的 <code>isUniquelyReferencedNonObjC</code> API 已经在 Swift 3.1 的时候被替换为 <code>isKnownUniquelyReferenced</code><br>详情可以参考 swift-evolution 中的这条 <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0125-remove-nonobjectivecbase.md" target="_blank" rel="noopener">建议</a></p></blockquote><h2 id="ArrayImpl"><a href="#ArrayImpl" class="headerlink" title="ArrayImpl"></a>ArrayImpl</h2><p>让我们开始实现 Swift.Array,首先从 <code>ArrayImpl</code> 开始,然后才是 <code>Array</code>。 </p><p>在这里我并不会重新实现 <code>Array</code> 完整的 API,只是实现满足其正常运行的基本功能,并展示它涉及的原理。 </p><p><code>ArrayImpl</code> 有三个属性:指针,数组元素的总数,以及已申请内存空间中的剩余容量。只有指针和元素的总数是必须的,但是相比申请更多的内存空间,实时监控剩余容量并按需申请,我们可以避免一大笔昂贵的内存重新分配。下面是类开始的部分:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ArrayImpl</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="keyword">var</span> space: <span class="type">Int</span></span><br><span class="line"> <span class="keyword">var</span> <span class="built_in">count</span>: <span class="type">Int</span></span><br><span class="line"> <span class="keyword">var</span> ptr: <span class="type">UnsafeMutablePointer</span><<span class="type">T</span>></span><br></pre></td></tr></table></figure><p>在 <code>init</code> 方法中需要一个计数和一个指针,然后将指针所指向的内容复制到新的对象。方法提供了默认值 0 和 nil ,所以 <code>init</code> 可以在不传入任何参数的情况下被用来创建一个拥有空数组的实例: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"> <span class="keyword">init</span>(<span class="built_in">count</span>: <span class="type">Int</span> = <span class="number">0</span>, ptr: <span class="type">UnsafeMutablePointer</span><<span class="type">T</span>> = <span class="literal">nil</span>) {</span><br><span class="line"> <span class="keyword">self</span>.<span class="built_in">count</span> = <span class="built_in">count</span></span><br><span class="line"> <span class="keyword">self</span>.space = <span class="built_in">count</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">self</span>.ptr = <span class="type">UnsafeMutablePointer</span><<span class="type">T</span>>.alloc(<span class="built_in">count</span>)</span><br><span class="line"> <span class="keyword">self</span>.ptr.initializeFrom(ptr, <span class="built_in">count</span>: <span class="built_in">count</span>)</span><br><span class="line"> }</span><br><span class="line">``` </span><br><span class="line"></span><br><span class="line">`initializeFrom` 方法可以将数据复制到新的指针。注意区分 `<span class="type">UnsafeMutablePointer</span>` 处于不同赋值方式之间的不同,这对于确保它们正常工作以及避免崩溃十分重要。不同之处在于数据内存是否被处理为初始化或未初始化。在调用 `alloc` 时,生成的指针处于未初始化的状态,并且可能被垃圾数据填满。一个简单的赋值,例如 `ptr.memory = ...` ,此时是不合法的,因为赋值操作将会在复制新的值之前析构已经存在的值。如果是类似 `int` 这样的基础数据类型将没什么问题,但是如果你操作的是一个复杂数据类型它将崩溃。在这里 `initializeFrom` 将目标指针视为未初始化的内存,而这正是它的本质。 </span><br><span class="line"></span><br><span class="line">接下来是一个改变过的 `append` 方法。它做的第一件事是检查指针是否需要重新分配。如果没有剩余的空间可用,我们需要一块新的内存:</span><br><span class="line"></span><br><span class="line">```swift</span><br><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">append</span><span class="params">(obj: T)</span></span> {</span><br><span class="line"> <span class="keyword">if</span> space == <span class="built_in">count</span> {</span><br><span class="line"> <span class="comment">// 在新的内存分配中,我们将申请两倍的容量,并且最小值为 16 :</span></span><br><span class="line"> <span class="keyword">let</span> newSpace = <span class="built_in">max</span>(space * <span class="number">2</span>, <span class="number">16</span>)</span><br><span class="line"> <span class="keyword">let</span> newPtr = <span class="type">UnsafeMutablePointer</span><<span class="type">T</span>>.alloc(newSpace)</span><br><span class="line"> <span class="comment">// 从旧的内存中将数据拷贝到新的地址</span></span><br><span class="line"> newPtr.moveInitializeFrom(ptr, <span class="built_in">count</span>: <span class="built_in">count</span>)</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 这是另一种赋值,它不仅把目的指针看做是未初始化的,并且会把数据源销毁。 </span></span><br><span class="line"><span class="comment"> 它节省了我们单独写代码来销毁旧内存的操作,同时可能更加高效。 </span></span><br><span class="line"><span class="comment"> 随着数据的移动完成,旧指针将可以被释放,新的数据将被赋值给类的属性:</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> ptr.dealloc(space)</span><br><span class="line"> ptr = newPtr</span><br><span class="line"> space = newSpace</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 现在我们确信有足够的空间,所以可以把新的值放在内存的最后面并且递增 count 属性的值:</span></span><br><span class="line"> (ptr + <span class="built_in">count</span>).initialize(obj)</span><br><span class="line"> <span class="built_in">count</span>++</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>改变过的 <code>remove</code> 方法将更为简洁,因为没有必要重新分配内存。首先,他将在移除一个值之前先将其销毁: </p><blockquote><p>译者注:<br>这里的 # 号不用理会,在早期的 Swift 版本中它表示内外参数同名)**</p></blockquote><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">remove</span><span class="params">(# index: Int)</span></span> {</span><br><span class="line"> (ptr + index).destroy()</span><br><span class="line"> <span class="comment">// moveInitializeFrom 方法负责将所有在被移除元素之后的元素往前挪一个位置</span></span><br><span class="line"> (ptr + index).moveInitializeFrom(ptr + index + <span class="number">1</span>, <span class="built_in">count</span>: <span class="built_in">count</span> - index - <span class="number">1</span>)</span><br><span class="line"> <span class="comment">// 递减 count 属性的值来体现这次删除操作</span></span><br><span class="line"> <span class="built_in">count</span>--</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们同样需要一个 <code>copy</code> 方法来确保当需要的时候可以从数据内存中复制一份。实际关于复制的代码存在于 <code>init</code> 方法中,所以我们只需要创建一个实例也就相当于执行了一次复制:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">copy</span><span class="params">()</span></span> -> <span class="type">ArrayImpl</span><<span class="type">T</span>> {</span><br><span class="line"> <span class="keyword">return</span> <span class="type">ArrayImpl</span><<span class="type">T</span>>(<span class="built_in">count</span>: <span class="built_in">count</span>, ptr: ptr)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样,我们就基本上完成了所有的事情。我们只需要确保在它自己将要被销毁,调用 <code>deinit</code> 方法之后销毁所有数组中的元素并释放指针:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"> <span class="keyword">deinit</span> {</span><br><span class="line"> ptr.destroy(<span class="built_in">count</span>)</span><br><span class="line"> ptr.dealloc(space)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>让我们把它移到 <code>Array struct</code>。它唯一的属性就是一个 <code>ArrayImpl</code>。</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Array</span><<span class="title">T</span>>: <span class="title">SequenceType</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> impl: <span class="type">ArrayImpl</span><<span class="type">T</span>> = <span class="type">ArrayImpl</span><<span class="type">T</span>>()</span><br></pre></td></tr></table></figure><p>所有 <code>mutating</code> 类型的方法都将以检查 <code>impl</code> 是不是独立的引用为开始,并在不是的时候进行复制操作。将把它封装成一个函数提供给其他的方法使用:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">ensureUnique</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">if</span> !<span class="built_in">isUniquelyReferencedNonObjC</span>(&impl) {</span><br><span class="line"> impl = impl.copy()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>append</code> 方法现在只调用了 <code>ensureUnique</code> 方法,然后调用 <code>ArrayImpl</code> 的 <code>append</code> 方法:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">append</span><span class="params">(value: T)</span></span> {</span><br><span class="line"> ensureUnique()</span><br><span class="line"> impl.append(value)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>remove</code> 方法也是一样:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">remove</span><span class="params">(# index: Int)</span></span> {</span><br><span class="line"> ensureUnique()</span><br><span class="line"> impl.remove(index: index)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>count</code> 属性直接通过 <code>ArrayImpl's</code> 来返回:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="built_in">count</span>: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> impl.<span class="built_in">count</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下标操作直接通过底层指针来进行访问。如果我们是在写真实的代码,在这里我们会需要进行一个范围的检查(<code>remove</code> 方法中也是),但是在这个例子中我们将它省略了:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">subscript</span>(index: <span class="type">Int</span>) -> <span class="type">T</span> {</span><br><span class="line"> <span class="keyword">get</span> {</span><br><span class="line"> <span class="keyword">return</span> impl.ptr[index]</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">mutating</span> <span class="keyword">set</span> {</span><br><span class="line"> ensureUnique()</span><br><span class="line"> impl.ptr[index] = newValue</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后,<code>Array</code> 遵循 <code>SequenceType</code> 协议以支持 <code>for in</code> 循环。其必须实现 <code>Generator typealias</code> 和 <code>generate</code> 方法。内置的 <code>GeneratorOf</code> 类型使其很容易实现。<code>GeneratorOf</code> 使用一个代码块确保在其每次被访问的时候返回集合中的下一个元素,或者当到达结尾的时候返回 <code>nil</code>,并创造一个 <code>GeneratorType</code> 来封装该代码块: </p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typealias</span> <span class="type">Generator</span> = <span class="type">GeneratorOf</span><<span class="type">T</span>></span><br></pre></td></tr></table></figure><p><code>generate</code> 方法从 0 开始递增直到运行至结尾,然后开始返回 <code>nil</code>:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">generate</span><span class="params">()</span></span> -> <span class="type">Generator</span> {</span><br><span class="line"> <span class="keyword">var</span> index = <span class="number">0</span></span><br><span class="line"> <span class="keyword">return</span> <span class="type">GeneratorOf</span><<span class="type">T</span>>({</span><br><span class="line"> <span class="keyword">if</span> index < <span class="keyword">self</span>.<span class="built_in">count</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>[index++]</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这就是它的全部!</p><h2 id="Array"><a href="#Array" class="headerlink" title="Array"></a>Array</h2><p>我们的 <code>Array</code> 是一个符合 <code>CollectionType</code> 协议的通用 <code>struct</code>:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Array</span><<span class="title">T</span>>: <span class="title">CollectionType</span> </span>{</span><br></pre></td></tr></table></figure><p>它唯一拥有的属性是一个底层 <code>ArrayImpl</code> 的引用:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">var</span> impl: <span class="type">ArrayImpl</span><<span class="type">T</span>> = <span class="type">ArrayImpl</span><<span class="type">T</span>>()</span><br></pre></td></tr></table></figure><p>任何一个方法如果会改变这个数组必须先检查这个 <code>impl</code> 是否为一个独立的引用,并在它不是的时候进行复制。这个功能被封装成一个私有的方法提供给其他的方法使用:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">ensureUnique</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">if</span> !<span class="built_in">isUniquelyReferencedNonObjC</span>(&impl) {</span><br><span class="line"> impl = impl.copy()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>append</code> 方法会使用 <code>ensureUnique</code> 然后调用 <code>impl</code> 中的 <code>append</code>:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">append</span><span class="params">(value: T)</span></span> {</span><br><span class="line"> ensureUnique()</span><br><span class="line"> impl.append(value)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>remove</code> 的实现基本是相同的:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">mutating</span> <span class="function"><span class="keyword">func</span> <span class="title">remove</span><span class="params">(# index: Int)</span></span> {</span><br><span class="line"> ensureUnique()</span><br><span class="line"> impl.remove(index: index)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>count</code> 属性是一个计算性属性,它将直接通过 <code>impl</code> 来调用:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="built_in">count</span>: <span class="type">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> impl.<span class="built_in">count</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下标操作将直接通过 <code>impl</code> 来修改底层的数据存储。通常这种直接从类的外部进行访问的方式是一个坏主意,但是 <code>Array</code> 和 <code>ArrayImpl</code> 联系的太过紧密,所以看起来并不是很糟糕。<code>subscript</code> 中 <code>set</code> 的部分会改变数组,所以需要使用 <code>ensureUnique</code> 来保持值语义:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">subscript</span>(index: <span class="type">Int</span>) -> <span class="type">T</span> {</span><br><span class="line"> <span class="keyword">get</span> {</span><br><span class="line"> <span class="keyword">return</span> impl.ptr[index]</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">mutating</span> <span class="keyword">set</span> {</span><br><span class="line"> ensureUnique()</span><br><span class="line"> impl.ptr[index] = newValue</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>CollectionType</code> 协议需要一个 <code>Index typealias</code>。对于 <code>Array</code> 来说,这个索引类型就是 <code>Int</code>:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typealias</span> <span class="type">Index</span> = <span class="type">Int</span></span><br></pre></td></tr></table></figure><p>它同时也需要一些属性来提供一个开始和结束的索引。对于 <code>Array</code> 俩说,开始的索引为 0 ,结束的索引就是数组中元素的个数:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> startIndex: <span class="type">Index</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"><span class="keyword">var</span> endIndex: <span class="type">Index</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">count</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>CollectionType</code> 协议包含 <code>SequenceType</code> 协议,它使得对象可以被用于 <code>for/in</code> 循环。它的工作原理是让序列提供一个生成器,该生成器是一个可以返回序列中连续元素的对象。生成器的类型由采用协议的类型来定义。<code>Array</code> 中采用的是 <code>GeneratorOf</code>,它是一个简单的封装用来支持使用一个闭包创建生成器:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typealias</span> <span class="type">Generator</span> = <span class="type">GeneratorOf</span><<span class="type">T</span>></span><br></pre></td></tr></table></figure><p><code>generate</code> 方法将会返回一个生成器。它使用 <code>GeneratorOf</code> 并且提供一个闭包来递增下标,直到下标到达数组的结尾。通过在闭包的外面声明一个 <code>index</code>,使它在调用中被捕获,并且它的值持续存在:</p><figure class="highlight swift"><table><tr><td class="code"><pre><span class="line"> <span class="function"><span class="keyword">func</span> <span class="title">generate</span><span class="params">()</span></span> -> <span class="type">Generator</span> {</span><br><span class="line"> <span class="keyword">var</span> index = <span class="number">0</span></span><br><span class="line"> <span class="keyword">return</span> <span class="type">GeneratorOf</span><<span class="type">T</span>>{</span><br><span class="line"> <span class="keyword">if</span> index < <span class="keyword">self</span>.<span class="built_in">count</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>[index++]</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样就完成了 <code>Array</code> 的实现。</p><h2 id="完整的实现和测试代码"><a href="#完整的实现和测试代码" class="headerlink" title="完整的实现和测试代码"></a>完整的实现和测试代码</h2><p>这里提供了完整的实现,附加一些测试来确保所有的这些正常运行,我放在了 GitHub 上面: </p><p><a href="https://gist.github.com/mikeash/63a791f2aec3318c7c5c" target="_blank" rel="noopener">https://gist.github.com/mikeash/63a791f2aec3318c7c5c</a></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>在 Swift 1.2 中添加的 <code>isUniquelyReferenced</code> 是一个广受好评的改变,它让我们可以实现很多真正有趣的值类型,包括对标准库中值类型集合的复制。 </p><p>今天就到这里。下次再来找乐趣,功能,以及有趣的功能。如果你有感兴趣的主题,请发给我们!邮箱地址:<a href="mailto:[email protected]" target="_blank" rel="noopener">[email protected]</a>。</p><blockquote><p>本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 <a href="http://swift.gg">http://swift.gg</a>。</p></blockquote>]]></content>
<summary type="html">
简述Swift标准库中Array的实现逻辑
</summary>
<category term="Mike Ash" scheme="https://swift.gg/categories/Mike-Ash/"/>
<category term="Swift 解析" scheme="https://swift.gg/tags/Swift-%E8%A7%A3%E6%9E%90/"/>
</entry>
</feed>