<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Development on Joe's Blog</title><link>https://shinrenpan.github.io/categories/development/</link><description>Recent content in Development on Joe's Blog</description><generator>Hugo</generator><language>zh-tw</language><copyright>© Shinren Pan. All rights reserved.</copyright><lastBuildDate>Sun, 14 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://shinrenpan.github.io/categories/development/index.xml" rel="self" type="application/rss+xml"/><item><title>Swift 網路層生存指南 (Final) —— JPNetworking</title><link>https://shinrenpan.github.io/2026-06-14/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-06-14/</guid><description>&lt;p&gt;前三篇（&lt;a href="https://shinrenpan.github.io/2026-01-01"&gt;1&lt;/a&gt;、&lt;a href="https://shinrenpan.github.io/2026-01-10"&gt;2&lt;/a&gt;、&lt;a href="https://shinrenpan.github.io/2026-02-06"&gt;3&lt;/a&gt;）講完了救災概念——SafeBox 處理型別錯置、ShieldedResponse 導航巢狀 JSON、BaseResponse 統一外殼解析。&lt;/p&gt;
&lt;p&gt;這篇把這套思路打包成 Swift Package：&lt;a href="https://github.com/shinrenpan/JPNetworking"&gt;JPNetworking&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="-架構概覽"&gt;🏛️ 架構概覽&lt;/h2&gt;
&lt;pre class="mermaid"&gt;flowchart TD
 A([URLSession.request]) --&amp;gt; B[Build URLRequest]
 B --&amp;gt; C[networkLogger .request]
 C --&amp;gt; D[URLSession.data]
 D --&amp;gt; E{statusCode}
 E --&amp;gt;|401| F{refresher &amp;amp; 首次 401?}
 F --&amp;gt;|no| ERR([throw APIError])
 F --&amp;gt;|yes| G[TokenRefresher.refresh]
 G --&amp;gt; D
 E --&amp;gt;|other| H[networkLogger .response]
 H --&amp;gt; I[EndPoint.validate]
 I --&amp;gt;|throws| ERR
 I --&amp;gt;|ok| J[decode]
 J --&amp;gt;|throws| ERR
 J --&amp;gt;|ok| K([Return T])
 D --&amp;gt;|network error| L{retriesLeft &amp;gt; 0?}
 L --&amp;gt;|yes| D
 L --&amp;gt;|no| ERR
 &lt;/pre&gt;

&lt;hr&gt;
&lt;h2 id="-安裝"&gt;📦 安裝&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#57606a"&gt;// Package.swift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dependencies&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;package&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;url&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;https://github.com/shinrenpan/JPNetworking&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; from&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;0.1.0&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;或在 Xcode：&lt;strong&gt;File → Add Package Dependencies&lt;/strong&gt;。&lt;/p&gt;</description></item><item><title>SwiftUI TextField 限制輸入小數</title><link>https://shinrenpan.github.io/2026-06-13/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-06-13/</guid><description>&lt;p&gt;UIKit 版本靠 &lt;code&gt;shouldChangeCharactersIn&lt;/code&gt; 攔截每一次按鍵，SwiftUI 沒有這個 delegate，思路需要換一下：改成在 &lt;code&gt;onChange&lt;/code&gt; 裡拿到變更後的完整字串，驗證、修正，再寫回去。&lt;/p&gt;
&lt;p&gt;UIKit 版本：&lt;a href="https://shinrenpan.github.io/2020-12-25"&gt;UITextField 限制輸入小數&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="實作"&gt;實作&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;16
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;17
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;18
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;19
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;20
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;21
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;22
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;23
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;24
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;25
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;26
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;27
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;28
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;29
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;30
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;31
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;32
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;33
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;34
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;35
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;36
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;37
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;38
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;struct&lt;/span&gt; &lt;span style="color:#1f2328"&gt;DecimalTextField&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; View &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;@&lt;/span&gt;Binding &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;text&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#6639ba"&gt;String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;body&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; some View &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TextField&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0a3069"&gt;&amp;#34;0.00&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#f6f8fa;background-color:#82071e"&gt;$&lt;/span&gt;text&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;keyboardType&lt;span style="color:#1f2328"&gt;(.&lt;/span&gt;decimalPad&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;onChange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;of&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;_&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; newValue &lt;span style="color:#cf222e"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; text &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; filtered&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;newValue&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;private&lt;/span&gt; &lt;span style="color:#cf222e"&gt;extension&lt;/span&gt; &lt;span style="color:#1f2328"&gt;DecimalTextField&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;filtered&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#cf222e"&gt;_&lt;/span&gt; input&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#6639ba"&gt;String&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#6639ba"&gt;String&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 只保留數字和小數點&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;cleaned&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; input&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;filter&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;0123456789.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;contains&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#953800"&gt;$0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 總長度限制&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt; &lt;span style="color:#0550ae"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;10&lt;/span&gt; &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; text &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 只允許一個小數點&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;parts&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;components&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;separatedBy&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; parts&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt; &lt;span style="color:#0550ae"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt; &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; text &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 小數點後最多 2 位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; parts&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt; &lt;span style="color:#1f2328"&gt;==&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; parts&lt;span style="color:#1f2328"&gt;[&lt;/span&gt;&lt;span style="color:#0550ae"&gt;1&lt;/span&gt;&lt;span style="color:#1f2328"&gt;].&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt; &lt;span style="color:#0550ae"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; text &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 首位是 &amp;#34;.&amp;#34; 自動補成 &amp;#34;0.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;hasPrefix&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;0&amp;#34;&lt;/span&gt; &lt;span style="color:#0550ae"&gt;+&lt;/span&gt; cleaned &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 首位是 0 且下一位不是 &amp;#34;.&amp;#34;，移除多餘的 0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt; &lt;span style="color:#0550ae"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;hasPrefix&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0a3069"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt; &lt;span style="color:#0550ae"&gt;!&lt;/span&gt;cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;hasPrefix&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0a3069"&gt;&amp;#34;0.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#6639ba"&gt;String&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;cleaned&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;dropFirst&lt;/span&gt;&lt;span style="color:#1f2328"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; cleaned
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="與-uikit-版的差異"&gt;與 UIKit 版的差異&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;UIKit&lt;/th&gt;
 &lt;th&gt;SwiftUI&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;攔截時機&lt;/td&gt;
 &lt;td&gt;按鍵當下（字元尚未寫入）&lt;/td&gt;
 &lt;td&gt;變更後（拿到完整新字串）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;復原方式&lt;/td&gt;
 &lt;td&gt;return false 阻止&lt;/td&gt;
 &lt;td&gt;把舊值寫回 &lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;自動補值&lt;/td&gt;
 &lt;td&gt;直接改 &lt;code&gt;textField.text&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;把新值寫回 &lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;程式碼位置&lt;/td&gt;
 &lt;td&gt;delegate 方法&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;onChange&lt;/code&gt; + private func&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;SwiftUI 的 &lt;code&gt;onChange&lt;/code&gt; 是在字串已經改變後才觸發，所以不能「阻止」輸入，只能「修正」回合法值。這是兩個版本最根本的差異。&lt;/p&gt;</description></item><item><title>為什麼我在 SwiftUI 專案裡自創了一套 MVVMC 架構</title><link>https://shinrenpan.github.io/2026-04-24/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-04-24/</guid><description>&lt;p&gt;在公司與外包共同開發，並在外包離開後接手留下來的 SwiftUI 專案。外包用的是 &lt;strong&gt;TCA（The Composable Architecture）&lt;/strong&gt;，target iOS 15+，架構本身有它的邏輯，但接手之後我做了第一個決定，不繼續用 TCA。&lt;/p&gt;
&lt;p&gt;接著把 minimum deployment target 直升 iOS 17+。&lt;code&gt;@Observable&lt;/code&gt; 可以用了，&lt;code&gt;WithPerceptionTracking&lt;/code&gt; 全部可以掃掉，光這件事就讓整個 codebase 清爽不少，bug 也少了不少。&lt;/p&gt;
&lt;p&gt;但還有一個問題一直卡著我：&lt;strong&gt;導航&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="-swiftui-導航用過就知道"&gt;🚩 SwiftUI 導航，用過就知道&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;NavigationStack&lt;/code&gt;、&lt;code&gt;NavigationLink&lt;/code&gt;、&lt;code&gt;.navigationDestination&lt;/code&gt;，光看 API 名稱感覺很整齊，用下去才知道有多零碎。&lt;/p&gt;
&lt;p&gt;頁面跳轉的邏輯開始出現在各個 View 裡，A 頁知道 B 頁的存在，B 頁知道 C 頁的存在。專案小的時候還好，Feature 一多，導航邏輯就像藤蔓一樣纏進每個 View，要改一個跳轉邏輯得找半天。&lt;/p&gt;
&lt;p&gt;更麻煩的是 state 管理，&lt;code&gt;navigationPath&lt;/code&gt;、&lt;code&gt;@State var isPresented&lt;/code&gt;、&lt;code&gt;.sheet&lt;/code&gt;、&lt;code&gt;.fullScreenCover&lt;/code&gt; 各自為政，散落在不同層級的 View 裡，沒有一個統一的地方可以看清楚整個 app 的導航狀態。&lt;/p&gt;
&lt;p&gt;然後你就會遇到這種需求：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「使用者在購物車結帳完成後，dismiss 掉結帳流程，然後自動切換到『訂單』Tab，同時把購物車的 Navigation stack 清空。」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;UIKit 的做法很直覺：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#57606a"&gt;// 清空 stack、切 Tab，三行搞定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;navigationController&lt;span style="color:#1f2328"&gt;?.&lt;/span&gt;popToRootViewController&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;animated&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tabBarController&lt;span style="color:#1f2328"&gt;?.&lt;/span&gt;selectedIndex &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dismiss&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;animated&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#cf222e"&gt;true&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;SwiftUI 呢？你需要在對的時機、對的層級，同時操控 &lt;code&gt;navigationPath&lt;/code&gt;、&lt;code&gt;selectedTab&lt;/code&gt; 這些分散在不同 View 的 state，還要確保時序正確，不然畫面會閃、動畫會跳、甚至 state 不同步。更麻煩的是，深層的 View 根本拿不到外層的 &lt;code&gt;selectedTab&lt;/code&gt;，除非你用 &lt;code&gt;@Environment&lt;/code&gt; 或 &lt;code&gt;@Binding&lt;/code&gt; 一層一層往下傳，然後每個中間層都變成了導航邏輯的搬運工。&lt;/p&gt;</description></item><item><title>Swift 網路層生存指南 (3) —— API 亂象下的終極防線</title><link>https://shinrenpan.github.io/2026-02-06/</link><pubDate>Fri, 06 Feb 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-02-06/</guid><description>&lt;p&gt;這篇文章，我想聊聊一個「笑著笑著就哭了」的開發日常。&lt;/p&gt;
&lt;p&gt;在過去開發的經驗中，常遇到 Backends 他們自己也沒 Spec, 給出的 Response 像是一場隨機發生的驚喜：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;(成功 200)&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;直球對決型&lt;/strong&gt;：&lt;code&gt;{ &amp;quot;id&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;Gemini&amp;quot; }&lt;/code&gt; (直接就是 DTO)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;標準殼型&lt;/strong&gt;：&lt;code&gt;{ &amp;quot;data&amp;quot;: { &amp;quot;id&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;Gemini&amp;quot; } }&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;腦袋抽風多一層型&lt;/strong&gt;：&lt;code&gt;{ &amp;quot;data&amp;quot;: { &amp;quot;somekey&amp;quot;: { &amp;quot;id&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;Gemini&amp;quot; } } }&lt;/code&gt; (不知道為何要多個 key)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大禮包型&lt;/strong&gt;：&lt;code&gt;{ &amp;quot;data&amp;quot;: { &amp;quot;list&amp;quot;: [...] } }&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;(失敗 4xx/5xx)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;資料欄位瞬間蒸發，只剩 &lt;code&gt;{ &amp;quot;message&amp;quot;: &amp;quot;something wrong&amp;quot; }&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面對這種「薛丁格的 JSON」，如果你只寫標準的 &lt;code&gt;Codable&lt;/code&gt;，你的 Console 大概會被 &lt;code&gt;keyNotFound&lt;/code&gt; 洗版到你懷疑人生。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="-架構圖當-http-狀態碼與動態路徑聯動"&gt;🏛️ 架構圖：當 HTTP 狀態碼與動態路徑聯動&lt;/h2&gt;
&lt;p&gt;這套設計的核心在於：&lt;strong&gt;不再盲目相信 JSON 內容，而是透過注入「解析路徑 (decodePath)」來對付那些抽風的 Key。&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>Swift 網路層生存指南 (2)</title><link>https://shinrenpan.github.io/2026-01-10/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-01-10/</guid><description>&lt;p&gt;如果說 &lt;code&gt;SafeBox&lt;/code&gt; 是處理「單兵作戰」的防禦，那麼 &lt;code&gt;BaseResponseProtocol&lt;/code&gt; 就是針對「陣地戰」的全局控管。&lt;/p&gt;
&lt;p&gt;在一個大型專案中，你可能會遇到來自多個微服務的 API，它們的結構通常長這樣：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服務 A&lt;/strong&gt;: &lt;code&gt;{ &amp;quot;status&amp;quot;: true, &amp;quot;data&amp;quot;: { ... } }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服務 B&lt;/strong&gt;: &lt;code&gt;{ &amp;quot;code&amp;quot;: 200, &amp;quot;result&amp;quot;: { ... }, &amp;quot;msg&amp;quot;: &amp;quot;success&amp;quot; }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服務 C&lt;/strong&gt;: 直接吐一個 &lt;code&gt;[ { ... } ]&lt;/code&gt; 連殼都沒有。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;為了不幫每個服務都寫一套 Decoder，我們需要一個強大的通用協議。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="-響應層架構圖"&gt;🏛️ 響應層架構圖&lt;/h2&gt;
&lt;p&gt;這張圖展示了 &lt;code&gt;BaseResponseProtocol&lt;/code&gt; 如何協同 &lt;code&gt;ShieldedResponse&lt;/code&gt; 進行深度導航，直接挖出內部的 Payload。&lt;/p&gt;
&lt;pre class="mermaid"&gt;graph LR
 A[API Response JSON] --&amp;gt; B{BaseResponseProtocol}
 B --&amp;gt; C[Status Check: isSuccess]
 B --&amp;gt; D[Message: message]
 B --&amp;gt; E[Data Container: result]

 subgraph Shielded_Navigation [路徑導航引擎]
 E --&amp;gt; F[userInfo: decodePath]
 F --&amp;gt; G[Key Path: &amp;#39;data&amp;#39; -&amp;gt; &amp;#39;items&amp;#39;]
 G --&amp;gt; H[Final Payload: T]
 end

 H --&amp;gt; I[Domain Convertible]
 &lt;/pre&gt;

&lt;hr&gt;
&lt;h2 id="-核心協議定義"&gt;🛠️ 核心協議定義&lt;/h2&gt;
&lt;p&gt;這套協議的核心在於 &lt;code&gt;associatedtype Payload&lt;/code&gt;，它讓你在實作時才決定具體的資料型別，同時強制要求必須符合 &lt;code&gt;Sendable&lt;/code&gt; 以適應 Swift 6。&lt;/p&gt;</description></item><item><title>Swift 網路層生存指南 (1)</title><link>https://shinrenpan.github.io/2026-01-01/</link><pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2026-01-01/</guid><description>&lt;p&gt;這套架構的核心，是我在無數個被 API 炸掉的午夜，與一個「非人類助手」共同參悟出來的生存法則。&lt;/p&gt;
&lt;p&gt;身為 iOS 工程師，我們最大的壓力來源通常不是複雜的 UI，而是**「後端不按牌理出牌的 API」**。當髒資料導致 App 出錯，面對老闆的連環追問：「&lt;strong&gt;為什麼別人的 App 沒事，我們的會閃退？&lt;/strong&gt;」、「&lt;strong&gt;這不是昨天才修過嗎？&lt;/strong&gt;」，你需要的不是更多的 &lt;code&gt;try?&lt;/code&gt;，而是這套強大的「救災架構」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="-badbackend-奇葩行為大賞-血汗處刑清單"&gt;🚩 BadBackend 奇葩行為大賞 (血汗處刑清單)&lt;/h2&gt;
&lt;p&gt;在給出解決方案前，我們先看看這些讓開發者血壓飆升的真實案例。這不是虛構，這是我們的日常：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;【薛丁格的 ID】&lt;/strong&gt;：有資料時 &lt;code&gt;id: &amp;quot;123&amp;quot;&lt;/code&gt;，沒資料時欄位直接失蹤，或是給 &lt;code&gt;id: null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;【型別人格分裂】&lt;/strong&gt;：&lt;code&gt;price&lt;/code&gt; 這一秒是 &lt;code&gt;Double (99.0)&lt;/code&gt;，下一秒變 &lt;code&gt;String (&amp;quot;99.0&amp;quot;)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;【Bool 的創意大賽】&lt;/strong&gt;：&lt;code&gt;true&lt;/code&gt; 有時是 &lt;code&gt;1&lt;/code&gt;，有時是 &lt;code&gt;&amp;quot;Y&amp;quot;&lt;/code&gt;，有時是 &lt;code&gt;&amp;quot;on&amp;quot;&lt;/code&gt;，甚至還給過 &lt;code&gt;&amp;quot;checked&amp;quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;【外殼變色龍】&lt;/strong&gt;：今天資料包在 &lt;code&gt;data&lt;/code&gt;，明天改叫 &lt;code&gt;items&lt;/code&gt;，後天直接吐 Array 不包殼。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="-救災架構視覺化"&gt;🏛️ 救災架構視覺化&lt;/h2&gt;
&lt;p&gt;為了讓你理解這套系統是如何在混亂中維持秩序，我們來看這張資料流向圖：&lt;/p&gt;
&lt;pre class="mermaid"&gt;graph TD
 subgraph JSON_Source [原始髒資料]
 A[Missing Keys / Nulls]
 B[Type Mismatch: &amp;#39;123&amp;#39; vs 123]
 C[Corrupted Array Elements]
 end

 subgraph Defense_Layer [DTO 防禦層 - SafeBox / SafeArray]
 D{SafeBox Decoder}
 E{SafeArray Recovery}
 D --&amp;gt;|Type Rescue| F[Normalizing Types]
 D --&amp;gt;|Key Missing| G[Inject Default Value]
 E --&amp;gt;|Element Fail| H[Insert Default Instance]
 end

 subgraph Domain_Layer [Domain 轉換層]
 I[toDomain Mapping]
 J{關鍵欄位驗證策略}
 I --&amp;gt; J
 J --&amp;gt;|情境一| K[給予隨機 ID / 保證渲染]
 J --&amp;gt;|情境二| L[回傳 nil / 直接過濾]
 end

 JSON_Source --&amp;gt; Defense_Layer
 Defense_Layer --&amp;gt; Domain_Layer
 Domain_Layer --&amp;gt; M[ViewModel / UI]

 style Defense_Layer fill:#f96,stroke:#333,stroke-width:2px
 style Domain_Layer fill:#bbf,stroke:#333,stroke-width:2px
 &lt;/pre&gt;

&lt;hr&gt;
&lt;h2 id="-核心救災工具包"&gt;🛠️ 核心救災工具包&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SafeBox&lt;/code&gt; 是整套架構的基石，處理三種情況：null 補預設值、型別錯置嘗試轉換、欄位缺失補預設值。&lt;/p&gt;</description></item><item><title>FHIR OAuth Demo</title><link>https://shinrenpan.github.io/2025-03-18/</link><pubDate>Tue, 18 Mar 2025 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2025-03-18/</guid><description>&lt;p&gt;醫療系統的 App 要存取病患資料，繞不開 FHIR。這篇記錄如何用 SMART on FHIR 完成 OAuth 授權，並用 Apple 官方的 FHIRModels 套件解析回傳的資料。&lt;/p&gt;
&lt;p&gt;
 &lt;img src="https://shinrenpan.github.io/images/2025-03-18/01.gif" alt=""&gt;

&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="什麼是-smart-on-fhir"&gt;什麼是 SMART on FHIR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FHIR&lt;/strong&gt;（Fast Healthcare Interoperability Resources）：HL7 制定的醫療資料交換標準，R4 是目前主流版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SMART on FHIR&lt;/strong&gt;：在 FHIR 上套用 OAuth 2.0 的授權框架，讓第三方 App 可以安全存取醫療系統的資料&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;流程跟一般的 OAuth Authorization Code Flow 一樣：取得 code → 換 token → 用 token 呼叫 API。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="整體流程"&gt;整體流程&lt;/h2&gt;
&lt;pre class="mermaid"&gt;sequenceDiagram
 participant App
 participant Browser as ASWebAuthenticationSession
 participant Server as SMART Server

 App-&amp;gt;&amp;gt;Browser: 開啟授權 URL
 Browser-&amp;gt;&amp;gt;Server: 使用者登入授權
 Server--&amp;gt;&amp;gt;Browser: redirect_uri?code=xxx
 Browser--&amp;gt;&amp;gt;App: callback URL
 App-&amp;gt;&amp;gt;Server: POST /token（帶 code）
 Server--&amp;gt;&amp;gt;App: access_token
 App-&amp;gt;&amp;gt;Server: GET /fhir/Patient（帶 Bearer token）
 Server--&amp;gt;&amp;gt;App: FHIR Bundle（病患資料）
 &lt;/pre&gt;

&lt;hr&gt;
&lt;h2 id="step-1建立授權-url"&gt;Step 1：建立授權 URL&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;makeOAuthURL&lt;/span&gt;&lt;span style="color:#1f2328"&gt;()&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; URL&lt;span style="color:#1f2328"&gt;?&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;authURI&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;https://launch.smarthealthit.org/.../auth/authorize&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;components&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; URLComponents&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;string&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; authURI&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;nil&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; components&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;queryItems &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#cf222e"&gt;init&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;name&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;response_type&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; value&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#cf222e"&gt;init&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;name&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;redirect_uri&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; value&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;app://&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#cf222e"&gt;init&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;name&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;aud&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; value&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;https://launch.smarthealthit.org/.../fhir&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#cf222e"&gt;init&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;name&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;scope&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; value&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;patient/*.cruds&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; components&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;url
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;scope: patient/*.cruds&lt;/code&gt; 代表要求對所有 Patient 資源的讀寫權限。&lt;code&gt;redirect_uri&lt;/code&gt; 用自訂 scheme &lt;code&gt;app://&lt;/code&gt;，授權完成後系統會把 callback URL 傳回 App。&lt;/p&gt;</description></item><item><title>用 MQTT 設計 IM 架構</title><link>https://shinrenpan.github.io/2025-03-06/</link><pubDate>Thu, 06 Mar 2025 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2025-03-06/</guid><description>&lt;p&gt;上一篇寫了 &lt;a href="https://shinrenpan.github.io/2025-03-04"&gt;MQTT 聊天 Demo&lt;/a&gt;，這篇來聊設計思路。&lt;/p&gt;
&lt;p&gt;MQTT 的 Pub/Sub 模型乍看只適合廣播，但只要 Topic 設計得好，點對點、群組、系統通知全部都能處理，payload 再加上 REST API 補完，一套完整的 IM 架構就出來了。&lt;/p&gt;
&lt;pre class="mermaid"&gt;sequenceDiagram
 participant A as User A
 participant B as User B
 participant Broker as MQTT Broker
 participant API as REST API

 Note over A, Broker: 登入 / 訂閱
 A-&amp;gt;&amp;gt;Broker: subscribe(userId_A)
 A-&amp;gt;&amp;gt;Broker: subscribe(system)
 B-&amp;gt;&amp;gt;Broker: subscribe(userId_B)
 B-&amp;gt;&amp;gt;Broker: subscribe(system)

 Note over A, API: A 傳訊息給 B
 A-&amp;gt;&amp;gt;Broker: publish(userId_B, payload: { type, content, senderId })
 Broker--&amp;gt;&amp;gt;B: 推送訊息

 Note over B, API: B 取補充資料
 B-&amp;gt;&amp;gt;API: GET /api/users/A/profile
 API--&amp;gt;&amp;gt;B: 頭像、顯示名稱

 Note over Broker: 系統廣播
 Broker--&amp;gt;&amp;gt;A: system topic
 Broker--&amp;gt;&amp;gt;B: system topic
 &lt;/pre&gt;

&lt;hr&gt;
&lt;h2 id="topic-設計每個-user-訂閱兩個-topic"&gt;Topic 設計：每個 User 訂閱兩個 Topic&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{userId} ← 只有你收得到
system ← 所有人都收得到
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;登入後，每個 User 訂閱自己的 &lt;code&gt;userId&lt;/code&gt; Topic 和 &lt;code&gt;system&lt;/code&gt; Topic，就這樣。&lt;/p&gt;</description></item><item><title>MQTT 實作聊天 Demo</title><link>https://shinrenpan.github.io/2025-03-04/</link><pubDate>Tue, 04 Mar 2025 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2025-03-04/</guid><description>&lt;p&gt;MQTT 是一個輕量的 Pub/Sub 協定，拿來做聊天有幾個優點：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QoS（Quality of Service）內建&lt;/strong&gt;：提供三個等級的送達保證，最高等級 QoS 2 確保訊息恰好送達一次，不需要自己實作重送機制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload 完全客製化&lt;/strong&gt;：IM 需要的語意——已讀、上線狀態、群組通知——全部定義在 payload 裡，彈性高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Facebook Messenger 早期也是用 MQTT 做即時訊息的傳輸層。&lt;/p&gt;
&lt;p&gt;
 &lt;img src="https://shinrenpan.github.io/images/2025-03-05/01.gif" alt=""&gt;

&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="架構概覽"&gt;架構概覽&lt;/h2&gt;
&lt;p&gt;採用 MVVMC，兩個頁面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Home&lt;/strong&gt;：輸入使用者名稱進入聊天室&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Room&lt;/strong&gt;：聊天室主頁面，連線 MQTT broker、收發訊息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MQTT 的操作集中在 &lt;code&gt;MQTTManager&lt;/code&gt;，ViewModel 不直接碰 MQTT。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mqttmanager"&gt;MQTTManager&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;actor MQTTManager &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;static&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;shared&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; MQTTManager&lt;span style="color:#1f2328"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;private&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;topic&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;JoeChatDemo/chat&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;private&lt;/span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;client&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; MQTTClient&lt;span style="color:#1f2328"&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;private&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;eventLoopGroup&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NIOTSEventLoopGroup&lt;span style="color:#1f2328"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;用 &lt;code&gt;actor&lt;/code&gt; 封裝，確保 &lt;code&gt;client&lt;/code&gt; 的狀態在 concurrent 環境下安全存取。&lt;/p&gt;</description></item><item><title>播放本地端 m3u8</title><link>https://shinrenpan.github.io/2022-07-13/</link><pubDate>Wed, 13 Jul 2022 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2022-07-13/</guid><description>&lt;p&gt;AVPlayer 可以直接以 &lt;code&gt;file://&lt;/code&gt; URL 播放本地影片，但對於 &lt;code&gt;.m3u8&lt;/code&gt; playlist，它會在解析完 manifest 後，&lt;strong&gt;直接嘗試以原始路徑載入每一個 segment&lt;/strong&gt;。這個過程完全繞過了 &lt;code&gt;AVAssetResourceLoaderDelegate&lt;/code&gt;，導致無法攔截並自訂載入行為。&lt;/p&gt;
&lt;p&gt;簡單說：&lt;code&gt;file://&lt;/code&gt; URL 太「正常」了，AVPlayer 自己就處理掉，不給你插手的機會。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="解法自訂-scheme"&gt;解法：自訂 Scheme&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;AVAssetResourceLoaderDelegate&lt;/code&gt; 只對 &lt;strong&gt;AVPlayer 不認識的 scheme&lt;/strong&gt; 生效。&lt;/p&gt;
&lt;p&gt;做法是在建立 &lt;code&gt;AVURLAsset&lt;/code&gt; 時，把 &lt;code&gt;file://&lt;/code&gt; 替換成自訂的 &lt;code&gt;local://&lt;/code&gt;：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;file:///var/mobile/.../video.m3u8
 ↓ 替換 scheme
local:///var/mobile/.../video.m3u8
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;AVPlayer 看到 &lt;code&gt;local://&lt;/code&gt;，不知道怎麼處理，就把請求轉交給 &lt;code&gt;AVAssetResourceLoaderDelegate&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;關鍵 delegate 方法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;resourceLoader&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#cf222e"&gt;_&lt;/span&gt; resourceLoader&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; AVAssetResourceLoader&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; shouldWaitForRenewalOfRequestedResource renewalRequest&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; AVAssetResourceRenewalRequest&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#6639ba"&gt;Bool&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;resourceLoader&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#cf222e"&gt;_&lt;/span&gt; resourceLoader&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; AVAssetResourceLoader&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; shouldWaitForLoadingOfRequestedResource loadingRequest&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; AVAssetResourceLoadingRequest&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#6639ba"&gt;Bool&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;hr&gt;
&lt;p&gt;&lt;a href="https://github.com/shinrenpan/play-local-m3u8"&gt;Demo&lt;/a&gt; ｜ &lt;a href="https://stackoverflow.com/questions/45670774/playing-offline-hls-with-aes-128-encryption-ios/45957045#45957045"&gt;參考&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;本文使用 Claude 共同完成&lt;/em&gt;&lt;/p&gt;</description></item><item><title>UITextField 限制輸入小數</title><link>https://shinrenpan.github.io/2020-12-25/</link><pubDate>Fri, 25 Dec 2020 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2020-12-25/</guid><description>&lt;p&gt;金融、電商類的 app 常常需要限制輸入金額，只允許輸入合法的小數——不能有多個小數點、小數點後只能兩位、開頭不能多餘的零。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UITextField&lt;/code&gt; 本身不提供這些限制，需要自己透過 delegate 的 &lt;code&gt;shouldChangeCharactersIn&lt;/code&gt; 來攔截每一次輸入。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="設定"&gt;設定&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;textField&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;keyboardType &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;decimalPad
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;textField&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;delegate &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#cf222e"&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;decimalPad&lt;/code&gt; 讓鍵盤只顯示數字和小數點，但這只是 UI 層面的限制，使用者仍然可以透過貼上功能輸入任意字元，所以 delegate 的驗證不能省。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="shouldchangecharactersin-實作"&gt;shouldChangeCharactersIn 實作&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;16
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;17
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;18
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;19
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;20
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;21
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;22
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;23
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;24
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;25
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;26
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;27
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;28
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;29
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;30
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;31
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;32
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;33
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;34
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;35
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;textField&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#cf222e"&gt;_&lt;/span&gt; textField&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; UITextField&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; shouldChangeCharactersIn range&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; NSRange&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; replacementString string&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#6639ba"&gt;String&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#6639ba"&gt;Bool&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;text&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; textField&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;text &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;true&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 1. 刪除事件直接放行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#0550ae"&gt;!&lt;/span&gt;string&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;isEmpty&lt;/span&gt; &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;true&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 2. 只允許數字和小數點&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;0123456789.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;contains&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;string&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 3. 總長度限制（最多 10 位）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt; &lt;span style="color:#0550ae"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;10&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 4. 小數點後最多 2 位&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;dotRange&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;range&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;of&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;dotIndex&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSRange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;dotRange&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; &lt;span style="color:#cf222e"&gt;in&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;).&lt;/span&gt;location
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; range&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;location &lt;span style="color:#0550ae"&gt;-&lt;/span&gt; dotIndex &lt;span style="color:#0550ae"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#0550ae"&gt;2&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 5. 首位是 0 時，下一個只能是小數點&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; text &lt;span style="color:#1f2328"&gt;==&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; string &lt;span style="color:#0550ae"&gt;!=&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; textField&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;text &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; string
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 6a. 首位直接輸入小數點，自動補成 &amp;#34;0.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;isEmpty&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; string &lt;span style="color:#1f2328"&gt;==&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; textField&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;text &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;0.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// 6b. 禁止重複輸入小數點&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;if&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;contains&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt; string &lt;span style="color:#1f2328"&gt;==&lt;/span&gt; &lt;span style="color:#0a3069"&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="六種邊界情況"&gt;六種邊界情況&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;情況&lt;/th&gt;
 &lt;th&gt;處理方式&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;刪除（string 為空）&lt;/td&gt;
 &lt;td&gt;直接放行&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;貼上非法字元&lt;/td&gt;
 &lt;td&gt;攔截，return false&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;超過 10 位&lt;/td&gt;
 &lt;td&gt;攔截，return false&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;小數點後超過 2 位&lt;/td&gt;
 &lt;td&gt;攔截，return false&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;首位輸入 0 再輸入數字&lt;/td&gt;
 &lt;td&gt;替換成輸入的數字&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;首位輸入小數點&lt;/td&gt;
 &lt;td&gt;自動補成 &lt;code&gt;0.&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;重複輸入小數點&lt;/td&gt;
 &lt;td&gt;攔截，return false&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;SwiftUI 版本另見：&lt;a href="https://shinrenpan.github.io/2026-06-13"&gt;SwiftUI TextField 限制輸入小數&lt;/a&gt;&lt;/p&gt;</description></item><item><title>UILabel 計算行數、每行字串、每行大小</title><link>https://shinrenpan.github.io/2020-07-01/</link><pubDate>Wed, 01 Jul 2020 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2020-07-01/</guid><description>&lt;p&gt;&lt;code&gt;UILabel&lt;/code&gt; 很方便，但有幾件事它不告訴你：這段文字實際跑了幾行？每行的內容是什麼？每行佔的大小是多少？&lt;/p&gt;
&lt;p&gt;這些需求在做客製化 UI 的時候常常會遇到，例如「超過三行就顯示漸層遮罩」、「第一行要特別標色」、「根據每行高度計算捲動位置」。&lt;code&gt;UILabel&lt;/code&gt; 的公開 API 沒有提供，只好自己往下挖。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="為什麼-uilabel-沒有這些-api"&gt;為什麼 UILabel 沒有這些 API&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;UILabel&lt;/code&gt; 的設計目標是顯示文字，不是提供排版資訊。實際的排版工作由底層的 &lt;strong&gt;CoreText&lt;/strong&gt; 或 &lt;strong&gt;TextKit（NSLayoutManager）&lt;/strong&gt; 完成，&lt;code&gt;UILabel&lt;/code&gt; 把這些細節都封裝起來了。&lt;/p&gt;
&lt;p&gt;要拿到行數和行內容，就必須繞過 &lt;code&gt;UILabel&lt;/code&gt;，自己用同樣的文字和寬度重新跑一遍排版計算。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="三個計算屬性"&gt;三個計算屬性&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;16
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;17
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;18
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;19
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;20
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;21
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;22
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;23
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;24
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;25
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;26
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;27
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;28
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;29
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;30
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;31
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;32
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;33
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;34
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;35
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;36
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;37
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;38
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;39
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;40
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;41
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;42
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;43
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;44
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;45
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;46
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;47
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;48
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;49
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;50
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;51
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;52
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;53
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;54
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;55
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;56
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;57
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;58
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;59
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;60
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;61
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;62
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;63
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;64
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;65
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;extension&lt;/span&gt; &lt;span style="color:#1f2328"&gt;UILabel&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// &lt;/span&gt;&lt;span style="color:#57606a"&gt;MARK:&lt;/span&gt;&lt;span style="color:#57606a"&gt; - 行數&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;lineCount&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#6639ba"&gt;Int&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;text&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; attributedText &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;zero &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;framesetter&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTFramesetterCreateWithAttributedString&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;text&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;fittingRect&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CGRect&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;x&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; y&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; width&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; frame&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;width&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; height&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;greatestFiniteMagnitude&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;path&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; UIBezierPath&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;rect&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; fittingRect&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;ctFrame&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTFramesetterCreateFrame&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;framesetter&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; CFRangeMake&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;),&lt;/span&gt; path&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;cgPath&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; &lt;span style="color:#cf222e"&gt;nil&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;lines&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTFrameGetLines&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;ctFrame&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#cf222e"&gt;as&lt;/span&gt; &lt;span style="color:#6639ba"&gt;Array&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; lines&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;count&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// &lt;/span&gt;&lt;span style="color:#57606a"&gt;MARK:&lt;/span&gt;&lt;span style="color:#57606a"&gt; - 每行字串&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;lineStrings&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[&lt;/span&gt;&lt;span style="color:#6639ba"&gt;String&lt;/span&gt;&lt;span style="color:#1f2328"&gt;]?&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;text&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;font&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; font &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;nil&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;attStr&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSMutableAttributedString&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;string&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; attStr&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;addAttribute&lt;span style="color:#1f2328"&gt;(.&lt;/span&gt;font&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; value&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; font&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; range&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; NSRange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;location&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; length&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; attStr&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;length&lt;span style="color:#1f2328"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;framesetter&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTFramesetterCreateWithAttributedString&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;attStr&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;fittingRect&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CGRect&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;x&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; y&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; width&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; frame&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;width&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; height&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;greatestFiniteMagnitude&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;path&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; UIBezierPath&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;rect&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; fittingRect&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;ctFrame&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTFramesetterCreateFrame&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;framesetter&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; CFRangeMake&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;&lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; attStr&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;length&lt;span style="color:#1f2328"&gt;),&lt;/span&gt; path&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;cgPath&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; &lt;span style="color:#cf222e"&gt;nil&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;lines&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTFrameGetLines&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;ctFrame&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#cf222e"&gt;as&lt;/span&gt;&lt;span style="color:#1f2328"&gt;?&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[&lt;/span&gt;CTLine&lt;span style="color:#1f2328"&gt;]&lt;/span&gt; &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#cf222e"&gt;nil&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; lines&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#6a737d"&gt;map&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; line &lt;span style="color:#cf222e"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;range&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; CTLineGetStringRange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;line&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#1f2328"&gt;(&lt;/span&gt;text &lt;span style="color:#cf222e"&gt;as&lt;/span&gt; NSString&lt;span style="color:#1f2328"&gt;).&lt;/span&gt;substring&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;with&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; NSRange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;location&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; range&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;location&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; length&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; range&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;length&lt;span style="color:#1f2328"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#57606a"&gt;// &lt;/span&gt;&lt;span style="color:#57606a"&gt;MARK:&lt;/span&gt;&lt;span style="color:#57606a"&gt; - 每行 Frame&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;lineFrames&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[&lt;/span&gt;CGRect&lt;span style="color:#1f2328"&gt;]&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;guard&lt;/span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;text&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; attributedText &lt;span style="color:#cf222e"&gt;else&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[]&lt;/span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;layoutManager&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSLayoutManager&lt;span style="color:#1f2328"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;textStorage&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSTextStorage&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;attributedString&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; text&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; textStorage&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;addLayoutManager&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;layoutManager&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;textContainer&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSTextContainer&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;size&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; CGSize&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;width&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; frame&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;width&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; height&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;greatestFiniteMagnitude&lt;span style="color:#1f2328"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; textContainer&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;lineFragmentPadding &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; textContainer&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;lineBreakMode &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;byWordWrapping
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; textContainer&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;maximumNumberOfLines &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; layoutManager&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;addTextContainer&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;textContainer&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;result&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[&lt;/span&gt;CGRect&lt;span style="color:#1f2328"&gt;]&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#1f2328"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;index&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;lineRange&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSRange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;location&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; NSNotFound&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; length&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#0550ae"&gt;0&lt;/span&gt;&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;while&lt;/span&gt; index &lt;span style="color:#0550ae"&gt;&amp;lt;&lt;/span&gt; layoutManager&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;numberOfGlyphs &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;rect&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; layoutManager&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;lineFragmentUsedRect&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;forGlyphAt&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; index&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; effectiveRange&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;&amp;amp;&lt;/span&gt;lineRange&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;append&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;rect&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; NSMaxRange&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;lineRange&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="為什麼-linecount-和-linestrings-用-coretextlineframes-用-textkit"&gt;為什麼 lineCount 和 lineStrings 用 CoreText，lineFrames 用 TextKit&lt;/h2&gt;
&lt;p&gt;CoreText 和 TextKit 是兩套不同層級的文字排版系統：&lt;/p&gt;</description></item><item><title>變更 Git commit 註解符號</title><link>https://shinrenpan.github.io/2020-03-06/</link><pubDate>Fri, 06 Mar 2020 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2020-03-06/</guid><description>&lt;p&gt;Git commit message 的編輯器裡，&lt;code&gt;#&lt;/code&gt; 預設是註解符號，存檔後這一行不會被納入 commit message。&lt;/p&gt;
&lt;p&gt;大多數時候沒問題，但如果你的 commit message 需要用 &lt;code&gt;#&lt;/code&gt; 開頭——例如引用 issue 編號 &lt;code&gt;#123 Fix login bug&lt;/code&gt;——Git 就會把整行吃掉，留下一個空的 commit message。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="解法"&gt;解法&lt;/h2&gt;
&lt;p&gt;把 Git 的 commit 註解符號改成其他字元，例如 &lt;code&gt;;&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config --global core.commentchar &lt;span style="color:#0a3069"&gt;&amp;#39;;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;改完之後，commit 編輯器裡改以 &lt;code&gt;;&lt;/code&gt; 開頭的行才會被視為註解，&lt;code&gt;#&lt;/code&gt; 就可以自由使用了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="只改單一-repo"&gt;只改單一 repo&lt;/h2&gt;
&lt;p&gt;如果不想改全域設定，拿掉 &lt;code&gt;--global&lt;/code&gt; 就只會影響當前 repo：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git config core.commentchar &lt;span style="color:#0a3069"&gt;&amp;#39;;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>硬體推播至 iPhone</title><link>https://shinrenpan.github.io/2020-03-04/</link><pubDate>Wed, 04 Mar 2020 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2020-03-04/</guid><description>&lt;p&gt;如果你在網路上搜尋「讓硬體裝置接收 iPhone 通知」，大概會找不到什麼有用的結果。這個需求聽起來直覺，實作起來卻很冷門——大多數人根本不知道這個功能叫什麼。&lt;/p&gt;
&lt;p&gt;關鍵字是 &lt;strong&gt;ANCS&lt;/strong&gt;：Apple Notification Center Service。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="ancs-是什麼"&gt;ANCS 是什麼&lt;/h2&gt;
&lt;p&gt;ANCS 是 Apple 定義的一套 BLE（Bluetooth Low Energy）GATT 協定，讓藍牙配件可以接收 iPhone 上的通知。&lt;/p&gt;
&lt;p&gt;角色分兩種：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Notification Provider（NP）&lt;/strong&gt;：iPhone，扮演 GATT Server，負責發送通知&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notification Consumer（NC）&lt;/strong&gt;：硬體配件，扮演 GATT Client，負責接收通知&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ANCS 公開三個 GATT Characteristic：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Characteristic&lt;/th&gt;
 &lt;th&gt;方向&lt;/th&gt;
 &lt;th&gt;用途&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Notification Source&lt;/td&gt;
 &lt;td&gt;NP → NC&lt;/td&gt;
 &lt;td&gt;推送通知事件（新增、修改、刪除）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Control Point&lt;/td&gt;
 &lt;td&gt;NC → NP&lt;/td&gt;
 &lt;td&gt;請求通知的詳細資料&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Data Source&lt;/td&gt;
 &lt;td&gt;NP → NC&lt;/td&gt;
 &lt;td&gt;回傳通知詳細內容&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;流程大致是：iPhone 有新通知 → 透過 Notification Source 推給配件 → 配件透過 Control Point 要求詳細內容 → iPhone 透過 Data Source 回傳 app 名稱、標題、內文等資訊。&lt;/p&gt;</description></item><item><title>NSObject 鏈式 Setter</title><link>https://shinrenpan.github.io/2020-03-01/</link><pubDate>Sun, 01 Mar 2020 00:00:00 +0000</pubDate><guid>https://shinrenpan.github.io/2020-03-01/</guid><description>&lt;p&gt;UIKit 開發久了，最煩的事情之一就是初始化一個 View：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;let&lt;/span&gt; &lt;span style="color:#953800"&gt;subView&lt;/span&gt;&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; UIView &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;&lt;span style="color:#cf222e"&gt;init&lt;/span&gt;&lt;span style="color:#1f2328"&gt;(&lt;/span&gt;frame&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;zero&lt;span style="color:#1f2328"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subView&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;translatesAutoresizingMaskIntoConstraints &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#cf222e"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subView&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;clipsToBounds &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#cf222e"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subView&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;layer&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;cornerRadius &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#0550ae"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;subView&lt;span style="color:#1f2328"&gt;.&lt;/span&gt;backgroundColor &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#1f2328"&gt;.&lt;/span&gt;black
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;五個屬性，五行，&lt;code&gt;subView&lt;/code&gt; 重複出現五次。這還只是一個 View，整個畫面十幾個元件疊下來，&lt;code&gt;viewDidLoad&lt;/code&gt; 就已經半條命了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="核心想法"&gt;核心想法&lt;/h2&gt;
&lt;p&gt;Swift 的 &lt;code&gt;WritableKeyPath&lt;/code&gt; 可以讓你用 &lt;code&gt;\.propertyName&lt;/code&gt; 的語法指向某個屬性，配合 &lt;code&gt;@discardableResult&lt;/code&gt; 讓方法回傳 &lt;code&gt;self&lt;/code&gt;，就能串起來一路點下去。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;extension&lt;/span&gt; &lt;span style="color:#1f2328"&gt;NSObjectProtocol&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;@&lt;/span&gt;discardableResult
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;setup&lt;/span&gt;&lt;span style="color:#1f2328"&gt;&amp;lt;&lt;/span&gt;Value&lt;span style="color:#1f2328"&gt;&amp;gt;(&lt;/span&gt;&lt;span style="color:#cf222e"&gt;_&lt;/span&gt; keypath&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; WritableKeyPath&lt;span style="color:#1f2328"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#cf222e"&gt;Self&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; Value&lt;span style="color:#1f2328"&gt;&amp;gt;,&lt;/span&gt; value&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; Value&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#cf222e"&gt;Self&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;result&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#cf222e"&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result&lt;span style="color:#1f2328"&gt;[&lt;/span&gt;keyPath&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; keypath&lt;span style="color:#1f2328"&gt;]&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; value
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;@&lt;/span&gt;discardableResult
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;func&lt;/span&gt; &lt;span style="color:#6639ba"&gt;setup&lt;/span&gt;&lt;span style="color:#1f2328"&gt;&amp;lt;&lt;/span&gt;Value&lt;span style="color:#1f2328"&gt;&amp;gt;(&lt;/span&gt;&lt;span style="color:#cf222e"&gt;_&lt;/span&gt; keypath&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; WritableKeyPath&lt;span style="color:#1f2328"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#cf222e"&gt;Self&lt;/span&gt;&lt;span style="color:#1f2328"&gt;,&lt;/span&gt; Value&lt;span style="color:#1f2328"&gt;&amp;gt;,&lt;/span&gt; condition&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; &lt;span style="color:#1f2328"&gt;()&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; Value&lt;span style="color:#1f2328"&gt;)&lt;/span&gt; &lt;span style="color:#1f2328"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#cf222e"&gt;Self&lt;/span&gt; &lt;span style="color:#1f2328"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;var&lt;/span&gt; &lt;span style="color:#953800"&gt;result&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; &lt;span style="color:#cf222e"&gt;self&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result&lt;span style="color:#1f2328"&gt;[&lt;/span&gt;keyPath&lt;span style="color:#1f2328"&gt;:&lt;/span&gt; keypath&lt;span style="color:#1f2328"&gt;]&lt;/span&gt; &lt;span style="color:#1f2328"&gt;=&lt;/span&gt; condition&lt;span style="color:#1f2328"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#cf222e"&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#1f2328"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;兩個多載：&lt;/p&gt;</description></item></channel></rss>