<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://chiahsien.github.io</id>
    <title>Nelson</title>
    <updated>2026-05-04T15:40:27.648Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://chiahsien.github.io"/>
    <link rel="self" href="https://chiahsien.github.io/atom.xml"/>
    <subtitle>我寫故我在</subtitle>
    <logo>https://chiahsien.github.io/images/avatar.png</logo>
    <icon>https://chiahsien.github.io/favicon.ico</icon>
    <rights>All rights reserved 2026, Nelson</rights>
    <entry>
        <title type="html"><![CDATA[macOS Caps Lock 零延遲設定教學]]></title>
        <id>https://chiahsien.github.io/post/macos-capslock-no-delay/</id>
        <link href="https://chiahsien.github.io/post/macos-capslock-no-delay/">
        </link>
        <updated>2026-05-04T15:34:28.000Z</updated>
        <summary type="html"><![CDATA[<p>在 macOS 底下我們可以按 Caps Lock 鍵切換輸入法，但每次按的時候都會覺得有一點頓頓的，無法立即切換過去，這不是你的錯覺！</p>
<p>本文分享兩個重點：</p>
<ol>
<li>讓 Caps Lock 切換輸入法「立刻生效、沒有延遲」</li>
<li>讓這個設定「開機自動套用」，不用每次重開機都重打指令</li>
</ol>
]]></summary>
        <content type="html"><![CDATA[<p>在 macOS 底下我們可以按 Caps Lock 鍵切換輸入法，但每次按的時候都會覺得有一點頓頓的，無法立即切換過去，這不是你的錯覺！</p>
<p>本文分享兩個重點：</p>
<ol>
<li>讓 Caps Lock 切換輸入法「立刻生效、沒有延遲」</li>
<li>讓這個設定「開機自動套用」，不用每次重開機都重打指令</li>
</ol>
<!-- more -->
<h2 id="一-立即取消-caps-lock-切換延遲">一、立即取消 Caps Lock 切換延遲</h2>
<ol>
<li>
<p>打開「終端機」（Terminal）</p>
</li>
<li>
<p>在終端機貼上以下指令並按 Enter：</p>
<pre><code class="language-bash">hidutil property --set '{&quot;CapsLockDelayOverride&quot;:0}'
</code></pre>
</li>
<li>
<p>測試 Caps Lock 切換中英文，應該會變得非常即時、沒有慢半拍的感覺。</p>
</li>
</ol>
<blockquote>
<p>注意：這個設定在重開機後常常會失效，因此建議再做下面的「開機自動套用」設定。</p>
</blockquote>
<hr>
<h2 id="二-讓-caps-lock-零延遲在開機時自動套用launchagent">二、讓 Caps Lock 零延遲在開機時自動套用（LaunchAgent）</h2>
<p>這一段會建立一個 <code>LaunchAgent</code>，在你登入 macOS 時自動執行上面的 <code>hidutil</code> 指令。</p>
<h3 id="步驟-1建立自動啟動的-plist-設定檔">步驟 1：建立自動啟動的 plist 設定檔</h3>
<p>在終端機貼上以下整段指令（包含 EOF 那行），然後按 Enter：</p>
<pre><code class="language-bash">cat &gt; ~/Library/LaunchAgents/com.user.capslockdelay.plist &lt;&lt; 'EOF'  
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;  
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;  
&lt;plist version=&quot;1.0&quot;&gt;  
&lt;dict&gt;  
  &lt;key&gt;Label&lt;/key&gt;  
  &lt;string&gt;com.user.capslockdelay&lt;/string&gt;  
  &lt;key&gt;ProgramArguments&lt;/key&gt;  
  &lt;array&gt;  
    &lt;string&gt;/usr/bin/hidutil&lt;/string&gt;  
    &lt;string&gt;property&lt;/string&gt;  
    &lt;string&gt;--set&lt;/string&gt;  
    &lt;string&gt;{&quot;CapsLockDelayOverride&quot;:0}&lt;/string&gt;  
  &lt;/array&gt;  
  &lt;key&gt;RunAtLoad&lt;/key&gt;  
  &lt;true/&gt;  
&lt;/dict&gt;  
&lt;/plist&gt;  
EOF
</code></pre>
<p>這會在 <code>~/Library/LaunchAgents/</code> 底下建立一個 <code>com.user.capslockdelay.plist</code> 設定檔。</p>
<h3 id="步驟-2載入設定讓它生效">步驟 2：載入設定讓它生效</h3>
<p>接著在終端機執行：</p>
<pre><code class="language-bash">launchctl load ~/Library/LaunchAgents/com.user.capslockdelay.plist
</code></pre>
<p>之後每次登入 macOS，系統就會自動執行 <code>hidutil property --set {&quot;CapsLockDelayOverride&quot;:0}</code>，讓 Caps Lock 零延遲自動生效。</p>
<h3 id="檢查檔案是否建立成功">檢查檔案是否建立成功</h3>
<pre><code class="language-bash">ls ~/Library/LaunchAgents/com.user.capslockdelay.plistl
</code></pre>
<p>如果看得到這個檔案路徑，代表設定檔已經建立。</p>
<hr>
<h2 id="三-如何移除還原預設設定">三、如何移除／還原預設設定</h2>
<p>如果之後想恢復系統預設（不再自動套用零延遲），可以在終端機執行：</p>
<pre><code class="language-bash">launchctl unload ~/Library/LaunchAgents/com.user.capslockdelay.plist  
rm ~/Library/LaunchAgents/com.user.capslockdelay.plist  
</code></pre>
<p>完成後，重開機後就不會再自動修改 Caps Lock 延遲設定。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[讓 Calibre 找到繁中書籍的元資料]]></title>
        <id>https://chiahsien.github.io/post/calibre-tw-ebook-matadata/</id>
        <link href="https://chiahsien.github.io/post/calibre-tw-ebook-matadata/">
        </link>
        <updated>2026-05-02T15:48:12.000Z</updated>
        <summary type="html"><![CDATA[<h2 id="calibre-什麼都好就是中文書的元資料讓人崩潰">Calibre 什麼都好，就是中文書的元資料讓人崩潰</h2>
<p>用 Calibre 管理電子書庫的人大概都經歷過這個流程：匯入一本書，點「下載元資料與封面」，然後看著搜尋結果轉圈圈，最後跳出來的不是空白就是一堆不相關的英文書。</p>
<p>這不是 Calibre 的問題。Calibre 的元資料架構設計得很好，它支援多個來源同時搜尋，讓你挑最完整的那筆。問題在於，內建的來源幾乎都是英文世界的資料庫，找不到繁體中文書。</p>
<h2 id="現有方案為什麼不夠用">現有方案為什麼不夠用</h2>
<p>目前 Calibre 內建能用的來源大概就這些：</p>
<ul>
<li>
<p><strong>Google Books</strong> — 偶爾能找到中文書，但資料經常殘缺。簡介只有一兩句、標籤沒有、封面模糊，有時候連作者名字都是拼音。</p>
</li>
<li>
<p><strong>Goodreads</strong> — 繁中書目少得可憐，大部分台灣出版的書根本搜不到。</p>
</li>
<li>
<p><strong>Kobo Metadata Plugin</strong> — 社群有人開發了一個從 Kobo 抓資料的外掛，設定台灣區之後確實能用。不過它不是每次都能找到資料，覆蓋率有限。</p>
</li>
</ul>
<p>有更多適合台灣讀者的來源，總是一件好事。所以我決定自己動手。</p>
]]></summary>
        <content type="html"><![CDATA[<h2 id="calibre-什麼都好就是中文書的元資料讓人崩潰">Calibre 什麼都好，就是中文書的元資料讓人崩潰</h2>
<p>用 Calibre 管理電子書庫的人大概都經歷過這個流程：匯入一本書，點「下載元資料與封面」，然後看著搜尋結果轉圈圈，最後跳出來的不是空白就是一堆不相關的英文書。</p>
<p>這不是 Calibre 的問題。Calibre 的元資料架構設計得很好，它支援多個來源同時搜尋，讓你挑最完整的那筆。問題在於，內建的來源幾乎都是英文世界的資料庫，找不到繁體中文書。</p>
<h2 id="現有方案為什麼不夠用">現有方案為什麼不夠用</h2>
<p>目前 Calibre 內建能用的來源大概就這些：</p>
<ul>
<li>
<p><strong>Google Books</strong> — 偶爾能找到中文書，但資料經常殘缺。簡介只有一兩句、標籤沒有、封面模糊，有時候連作者名字都是拼音。</p>
</li>
<li>
<p><strong>Goodreads</strong> — 繁中書目少得可憐，大部分台灣出版的書根本搜不到。</p>
</li>
<li>
<p><strong>Kobo Metadata Plugin</strong> — 社群有人開發了一個從 Kobo 抓資料的外掛，設定台灣區之後確實能用。不過它不是每次都能找到資料，覆蓋率有限。</p>
</li>
</ul>
<p>有更多適合台灣讀者的來源，總是一件好事。所以我決定自己動手。</p>
<!-- more -->
<h2 id="三個外掛三個台灣電子書平台">三個外掛，三個台灣電子書平台</h2>
<p>我寫了三個 Calibre metadata source plugin，分別對應台灣三個主要電子書平台：</p>
<ul>
<li>
<p><strong>Readmoo</strong> — 台灣最大的繁中電子書平台，書目豐富。能抓到書名、作者、出版社、出版日期、ISBN、標籤、完整簡介、封面。</p>
</li>
<li>
<p><strong>HyRead</strong> — 圖書館系統常用的電子書平台，學術和一般書籍都有。除了基本欄位之外，它有專門的 ISBN 搜尋功能，精確度很高。也能抓到集叢名（系列）資訊。</p>
</li>
<li>
<p><strong>Pubu</strong> — 另一個台灣電子書平台，同樣支援系列資訊。如果你的書屬於某個系列，HyRead 和 Pubu 都能幫你自動帶入系列名稱。</p>
</li>
</ul>
<p>三個外掛彼此獨立，可以同時啟用。Calibre 會自動整合所有來源的結果，讓你挑選最完整的那筆。跟既有的 Google Books、Kobo 等來源也不衝突，多裝一個就是多一個機會找到資料。</p>
<h2 id="安裝與使用">安裝與使用</h2>
<p>安裝很簡單。從 GitHub Release 頁面下載三個 .zip 檔，然後在 Calibre 裡：</p>
<p>偏好設定 → 外掛 → 從檔案載入外掛</p>
<p>三個都裝完之後重啟 Calibre 就好。</p>
<p>使用方式就跟平常一樣：選書、右鍵、下載元資料與封面。你會在來源清單裡看到 Readmoo Books、HyRead Books、Pubu Books 三個新來源。不需要額外設定，裝了就能用。</p>
<h2 id="試試看吧">試試看吧</h2>
<p>這三個外掛都開源在 GitHub 上，GPL v3 授權：</p>
<p><a href="https://github.com/chiahsien/calibre-tw-ebook-metadata">https://github.com/chiahsien/calibre-tw-ebook-metadata</a></p>
<p>Release 頁面有打包好的 .zip 可以直接下載安裝。如果遇到問題或有建議，歡迎開 issue 讓我知道。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[KOHi — 把 KOReader 的閱讀標註匯入 Obsidian]]></title>
        <id>https://chiahsien.github.io/post/kohi-koreader-highlights-to-obsidian/</id>
        <link href="https://chiahsien.github.io/post/kohi-koreader-highlights-to-obsidian/">
        </link>
        <updated>2026-05-01T00:59:53.000Z</updated>
        <summary type="html"><![CDATA[<p>用 KOReader 看書的人大概都有同樣的經驗：讀到好段落就順手畫個重點、寫個筆記，但讀完之後呢？這些標註就一直留在閱讀器裡，再也沒打開過。</p>
<p>我自己用 Obsidian 管理所有筆記，一直想把 KOReader 的 highlight 也整合進來，卻找不到現成的工具。手動複製貼上太慢，寫腳本又要處理 KOReader 那套 Lua 格式的 metadata。</p>
<p>所以我寫了 <strong>KOHi</strong>，一個 Obsidian plugin，專門解決這件事。</p>
<h2 id="kohi-做什麼">KOHi 做什麼</h2>
<p>USB 接上閱讀器，在 Obsidian 裡執行一個指令，就能把 KOReader 的 highlight 和筆記匯入 vault。每本書產生一份 Markdown 筆記，包含書籍資訊和所有標註。</p>
<p>核心功能：</p>
<ul>
<li><strong>自動偵測儲存模式</strong> — KOReader 有三種存放 metadata 的方式（book folder / <code>koreader/docsettings</code> / <code>koreader/hashdocsettings</code>），KOHi 全部支援，不用自己去找檔案在哪裡。</li>
<li><strong>模板自訂</strong> — 產生的筆記格式完全可控。用 Nunjucks 模板語法，可以自訂 frontmatter、章節分組、頁碼、highlight 顏色等所有欄位。也提供預設模板，不想自己寫也能直接用。</li>
<li><strong>選擇性匯入</strong> — 可以一次匯入全部，也可以用模糊搜尋挑特定幾本書。</li>
<li><strong>重複匯入覆蓋</strong> — 重新匯入同一本書時，可以選擇是否要自動覆蓋舊筆記，不會產生重複檔案。</li>
</ul>
]]></summary>
        <content type="html"><![CDATA[<p>用 KOReader 看書的人大概都有同樣的經驗：讀到好段落就順手畫個重點、寫個筆記，但讀完之後呢？這些標註就一直留在閱讀器裡，再也沒打開過。</p>
<p>我自己用 Obsidian 管理所有筆記，一直想把 KOReader 的 highlight 也整合進來，卻找不到現成的工具。手動複製貼上太慢，寫腳本又要處理 KOReader 那套 Lua 格式的 metadata。</p>
<p>所以我寫了 <strong>KOHi</strong>，一個 Obsidian plugin，專門解決這件事。</p>
<h2 id="kohi-做什麼">KOHi 做什麼</h2>
<p>USB 接上閱讀器，在 Obsidian 裡執行一個指令，就能把 KOReader 的 highlight 和筆記匯入 vault。每本書產生一份 Markdown 筆記，包含書籍資訊和所有標註。</p>
<p>核心功能：</p>
<ul>
<li><strong>自動偵測儲存模式</strong> — KOReader 有三種存放 metadata 的方式（book folder / <code>koreader/docsettings</code> / <code>koreader/hashdocsettings</code>），KOHi 全部支援，不用自己去找檔案在哪裡。</li>
<li><strong>模板自訂</strong> — 產生的筆記格式完全可控。用 Nunjucks 模板語法，可以自訂 frontmatter、章節分組、頁碼、highlight 顏色等所有欄位。也提供預設模板，不想自己寫也能直接用。</li>
<li><strong>選擇性匯入</strong> — 可以一次匯入全部，也可以用模糊搜尋挑特定幾本書。</li>
<li><strong>重複匯入覆蓋</strong> — 重新匯入同一本書時，可以選擇是否要自動覆蓋舊筆記，不會產生重複檔案。</li>
</ul>
<!-- more -->
<h2 id="匯入後的筆記長什麼樣">匯入後的筆記長什麼樣</h2>
<p>假設你讀了一本書，畫了一些重點，匯入後會產生這樣的 Markdown 筆記：</p>
<pre><code class="language-markdown">---
title: &quot;原子習慣&quot;
author: &quot;James Clear&quot;
language: zh
pages: 320
imported: 2026-05-01
---

## 第一章 原子習慣的驚人力量

&gt; 習慣是自我改善的複利。
&gt;
&gt; — p.16

&gt; 如果你每天都能進步百分之一，一年後你會進步三十七倍。
&gt;
&gt; — p.18

&gt; [!note]
&gt; 這跟投資的複利概念一樣，重點不是單次的幅度，而是持續性。
</code></pre>
<p>標註按章節分組，包含頁碼，你寫的筆記會以 callout 形式呈現。所有格式都可以透過模板調整。</p>
<h2 id="安裝方式">安裝方式</h2>
<p>KOHi 目前還在 Obsidian Community Plugins 的審核中，暫時可以用以下方式安裝。</p>
<h3 id="透過-brat推薦">透過 BRAT（推薦）</h3>
<p><a href="https://github.com/TfTHacker/obsidian42-brat">BRAT</a> 是一個專門用來安裝尚未上架的 Obsidian plugin 的工具。</p>
<ol>
<li>在 Obsidian 的 Community Plugins 裡搜尋並安裝 <strong>BRAT</strong></li>
<li>進入 BRAT 設定 → <strong>Add Beta Plugin</strong> → 貼上 <code>chiahsien/obsidian-kohi</code></li>
<li>回到 Community Plugins 列表，啟用 <strong>KOHi</strong></li>
</ol>
<h3 id="手動安裝">手動安裝</h3>
<ol>
<li>從 <a href="https://github.com/chiahsien/obsidian-kohi/releases">GitHub Releases</a> 下載 <code>main.js</code>、<code>manifest.json</code>、<code>styles.css</code></li>
<li>在 vault 裡建立 <code>.obsidian/plugins/kohi/</code> 資料夾</li>
<li>把下載的檔案放進去</li>
<li>重啟 Obsidian，在 Community Plugins 裡啟用 <strong>KOHi</strong></li>
</ol>
<h2 id="設定與使用">設定與使用</h2>
<ul class="contains-task-list">
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-70585"><label class="task-list-item-label" for="task-item-70585"> 安裝後到 Settings → KOHi，設定幾個必要欄位：</label></li>
</ul>
<table>
<thead>
<tr>
<th>設定</th>
<th>說明</th>
<th>預設值</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mount path</td>
<td>閱讀器的掛載路徑</td>
<td>—</td>
</tr>
<tr>
<td>Output folder</td>
<td>筆記輸出到 vault 的哪個資料夾</td>
<td><code>KOReader Highlights</code></td>
</tr>
<tr>
<td>Filename template</td>
<td>檔名格式，支援 <code>{{title}}</code>、<code>{{author}}</code> 等變數</td>
<td><code>{{title}}</code></td>
</tr>
<tr>
<td>Overwrite existing</td>
<td>重複匯入時是否覆蓋</td>
<td>開啟</td>
</tr>
</tbody>
</table>
<p>設定完成後，打開 Command Palette，執行：</p>
<ul>
<li><strong>KOHi: Import all highlights</strong> — 匯入所有書籍的標註</li>
<li><strong>KOHi: Import selected highlights</strong> — 用模糊搜尋選擇特定書籍</li>
</ul>
<p>筆記會出現在你設定的 output folder 裡。</p>
<h2 id="連結">連結</h2>
<ul>
<li>GitHub：<a href="https://github.com/chiahsien/obsidian-kohi">chiahsien/obsidian-kohi</a></li>
</ul>
<p>歡迎試用。有問題或建議可以直接到 GitHub 開 issue，或是在這裡留言。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Swift Package 多語言支援]]></title>
        <id>https://chiahsien.github.io/post/swift-package-localization-guide/</id>
        <link href="https://chiahsien.github.io/post/swift-package-localization-guide/">
        </link>
        <updated>2026-03-29T06:20:00.000Z</updated>
        <summary type="html"><![CDATA[<p>在開發 iOS 應用程式時，使用 Swift Package Manager (SPM) 來模組化程式碼已經成為主流做法。然而，當你在 Swift Package 中實作多國語言支援時，可能會遇到一個令人困惑的問題：Package 內的本地化在測試時運作正常，但整合到主專案後卻失效了。本文將深入探討這個問題的根源，以及如何透過 <code>CFBundleLocalizations</code> 和 <code>CFBundleAllowMixedLocalizations</code> 這兩個 Info.plist 設定來解決。</p>
]]></summary>
        <content type="html"><![CDATA[<p>在開發 iOS 應用程式時，使用 Swift Package Manager (SPM) 來模組化程式碼已經成為主流做法。然而，當你在 Swift Package 中實作多國語言支援時，可能會遇到一個令人困惑的問題：Package 內的本地化在測試時運作正常，但整合到主專案後卻失效了。本文將深入探討這個問題的根源，以及如何透過 <code>CFBundleLocalizations</code> 和 <code>CFBundleAllowMixedLocalizations</code> 這兩個 Info.plist 設定來解決。</p>
<!-- more -->
<h2 id="swift-package-在多國語言會遇到什麼問題">Swift Package 在多國語言會遇到什麼問題</h2>
<h3 id="問題現象">問題現象</h3>
<p>假設你正在開發一個 Swift Package，並且已經正確配置了多語言支援：</p>
<pre><code class="language-swift">let package = Package(
    name: &quot;MyFeatureKit&quot;,
    defaultLocalization: &quot;en&quot;,
    targets: [
        .target(
            name: &quot;MyFeatureKit&quot;,
            resources: [.process(&quot;Resources&quot;)]
        )
    ]
)
</code></pre>
<p>Package 的資料夾結構如下：</p>
<pre><code>MyFeatureKit/
└── Sources/
    └── MyFeatureKit/
        └── Resources/
            ├── en.lproj/Localizable.strings
            ├── es.lproj/Localizable.strings
            ├── ja.lproj/Localizable.strings
            └── de.lproj/Localizable.strings
</code></pre>
<p>在 Package 內部測試時，你使用 <code>Bundle.module</code> 來載入本地化字串：</p>
<pre><code class="language-swift">Text(&quot;welcome_message&quot;, bundle: .module)
</code></pre>
<p>一切運作正常。但當你將這個 Package 整合到主 App 中時，日文和德文的本地化突然失效了，系統只會回退到預設的英文。</p>
<h3 id="問題根源">問題根源</h3>
<p>這個問題的核心在於 iOS 系統的本地化載入機制：<strong>系統預設只允許 Swift Package 使用主 App Bundle 中明確支援的語言</strong>。</p>
<p>換句話說：</p>
<ul>
<li>主 App 透過 <code>.lproj</code> 資料夾或 <code>CFBundleLocalizations</code> 宣告它支援哪些語言</li>
<li>Swift Package 雖然有自己的 Bundle (<code>Bundle.module</code>) 和本地化資源</li>
<li>但在執行時，系統會檢查主 App 支援的語言清單</li>
<li>如果 Package 嘗試使用主 App 未宣告支援的語言，該語言會被忽略</li>
</ul>
<p>這個設計的原意是確保整個應用程式的本地化體驗一致，避免出現部分介面是語言 A，部分介面是語言 B 的混亂情況。然而，這也造成了 Swift Package 無法獨立提供更多語言支援的限制。</p>
<h2 id="cfbundlelocalizations明確宣告支援的語言">CFBundleLocalizations：明確宣告支援的語言</h2>
<h3 id="什麼是-cfbundlelocalizations">什麼是 CFBundleLocalizations</h3>
<p><code>CFBundleLocalizations</code> 是 Info.plist 中的一個鍵值，用於明確告訴系統你的應用程式支援哪些語言。它的資料型態是字串陣列，每個元素代表一種語言代碼。</p>
<h3 id="解決-swift-package-本地化問題">解決 Swift Package 本地化問題</h3>
<p>假設你的主 App 只有英文和西班牙文的本地化檔案（只有 <code>en.lproj</code> 和 <code>es.lproj</code>），但你的 Swift Package 支援英文、西班牙文、日文和德文。</p>
<p><strong>不加 CFBundleLocalizations 的情況：</strong></p>
<ul>
<li>英文使用者：看到英文（正常）</li>
<li>西班牙文使用者：看到西班牙文（正常）</li>
<li>日文使用者：看到英文（Package 的日文被忽略）</li>
<li>德文使用者：看到英文（Package 的德文被忽略）</li>
</ul>
<p><strong>加入 CFBundleLocalizations 後：</strong></p>
<pre><code class="language-xml">&lt;key&gt;CFBundleLocalizations&lt;/key&gt;
&lt;array&gt;
    &lt;string&gt;en&lt;/string&gt;
    &lt;string&gt;es&lt;/string&gt;
    &lt;string&gt;ja&lt;/string&gt;
    &lt;string&gt;de&lt;/string&gt;
&lt;/array&gt;
</code></pre>
<p>現在的結果：</p>
<ul>
<li>英文使用者：看到英文</li>
<li>西班牙文使用者：看到西班牙文</li>
<li>日文使用者：看到日文（Package 的日文正常載入）</li>
<li>德文使用者：看到德文（Package 的德文正常載入）</li>
</ul>
<h3 id="重要特性與限制">重要特性與限制</h3>
<p><strong>優點：</strong></p>
<ul>
<li>即使主 App 沒有對應的 <code>.lproj</code> 資料夾，只要在 <code>CFBundleLocalizations</code> 中宣告，Swift Package 就能使用該語言</li>
<li>實作簡單，只需修改 Info.plist</li>
</ul>
<p><strong>限制：</strong></p>
<ul>
<li>App Store 會顯示你宣告的所有語言為「支援語言」</li>
<li>使用者可能會期待整個 App 都支援該語言，但實際上只有 Package 部分支援</li>
<li>容易造成使用者體驗不一致：主 App 介面是英文，但 Package 提供的功能是日文</li>
</ul>
<p><strong>適用情境：</strong></p>
<ul>
<li>你計劃在主 App 中也加入這些語言的支援，只是還沒完成</li>
<li>你的 App 結構簡單，大部分內容都在 Package 中</li>
<li>你願意接受 App Store 顯示更多支援語言</li>
</ul>
<h2 id="cfbundleallowmixedlocalizations允許混合本地化">CFBundleAllowMixedLocalizations：允許混合本地化</h2>
<h3 id="什麼是-cfbundleallowmixedlocalizations">什麼是 CFBundleAllowMixedLocalizations</h3>
<p><code>CFBundleAllowMixedLocalizations</code> 是一個布林值設定，用於明確告訴系統：「允許我的依賴項（包括 Swift Package、Framework 等）使用它們自己支援的任何語言，不受主 App 宣告語言的限制。」</p>
<h3 id="解決-swift-package-本地化問題-2">解決 Swift Package 本地化問題</h3>
<p>使用相同的情境：主 App 只有英文和西班牙文，Swift Package 支援英文、西班牙文、日文和德文。</p>
<p><strong>加入 CFBundleAllowMixedLocalizations 後：</strong></p>
<pre><code class="language-xml">&lt;key&gt;CFBundleAllowMixedLocalizations&lt;/key&gt;
&lt;true/&gt;
</code></pre>
<p>結果：</p>
<ul>
<li>英文使用者：主 App 顯示英文，Package 顯示英文</li>
<li>西班牙文使用者：主 App 顯示西班牙文，Package 顯示西班牙文</li>
<li>日文使用者：主 App 顯示英文（或回退到基礎語言），Package 顯示日文</li>
<li>德文使用者：主 App 顯示英文（或回退到基礎語言），Package 顯示德文</li>
</ul>
<h3 id="重要特性與優勢">重要特性與優勢</h3>
<p><strong>優點：</strong></p>
<ul>
<li>App Store 只顯示主 App 實際支援的語言（英文、西班牙文）</li>
<li>Swift Package 可以獨立提供更多語言支援</li>
<li>使用者不會對整體語言支援有錯誤期待</li>
<li>對於只使用特定 Package 功能的使用者，可以獲得更好的本地化體驗</li>
</ul>
<p><strong>限制：</strong></p>
<ul>
<li>可能造成介面語言不一致（主 App 和 Package 顯示不同語言）</li>
<li>需要確保這種混合語言的體驗在 UI/UX 上是可接受的</li>
</ul>
<p><strong>適用情境：</strong></p>
<ul>
<li>主 App 只支援少數語言，但整合的 Swift Package 提供更豐富的多語言支援</li>
<li>Package 是功能型模組（如支付、地圖、社交分享），語言不一致不影響整體體驗</li>
<li>希望 App Store 只顯示主 App 實際完整支援的語言數量</li>
</ul>
<h2 id="實際案例不同語言配置的影響">實際案例：不同語言配置的影響</h2>
<p>讓我們用一個具體的案例來說明這兩個設定的實際差異。</p>
<h3 id="案例設定">案例設定</h3>
<p><strong>主 App (Main Bundle)：</strong></p>
<ul>
<li>實際本地化檔案：英文 (en)、西班牙文 (es)、繁體中文 (zh-Hant)</li>
<li>有完整的 UI 翻譯、App 名稱本地化等</li>
</ul>
<p><strong>Swift Package (第三方支付 SDK)：</strong></p>
<ul>
<li>本地化檔案：英文 (en)、西班牙文 (es)、繁體中文 (zh-Hant)、日文 (ja)、德文 (de)</li>
<li>包含支付流程的 UI 和訊息</li>
</ul>
<h3 id="方案一不做任何設定">方案一：不做任何設定</h3>
<p><strong>Info.plist：</strong></p>
<pre><code class="language-xml">&lt;!-- 空白，不加任何設定 --&gt;
</code></pre>
<p><strong>各語言使用者的體驗：</strong></p>
<table>
<thead>
<tr>
<th>使用者語言</th>
<th>主 App 顯示</th>
<th>Package 顯示</th>
<th>整體體驗</th>
</tr>
</thead>
<tbody>
<tr>
<td>英文</td>
<td>英文</td>
<td>英文</td>
<td>完美一致</td>
</tr>
<tr>
<td>西班牙文</td>
<td>西班牙文</td>
<td>西班牙文</td>
<td>完美一致</td>
</tr>
<tr>
<td>繁體中文</td>
<td>繁體中文</td>
<td>繁體中文</td>
<td>完美一致</td>
</tr>
<tr>
<td>日文</td>
<td>英文（回退）</td>
<td>英文（被限制）</td>
<td>一致但未本地化</td>
</tr>
<tr>
<td>德文</td>
<td>英文（回退）</td>
<td>英文（被限制）</td>
<td>一致但未本地化</td>
</tr>
</tbody>
</table>
<p><strong>App Store 顯示：</strong> 支援 3 種語言</p>
<p><strong>問題：</strong> 日文和德文使用者無法使用 Package 內建的本地化，即使 Package 已經準備好這些語言。</p>
<h3 id="方案二使用-cfbundlelocalizations">方案二：使用 CFBundleLocalizations</h3>
<p><strong>Info.plist：</strong></p>
<pre><code class="language-xml">&lt;key&gt;CFBundleLocalizations&lt;/key&gt;
&lt;array&gt;
    &lt;string&gt;en&lt;/string&gt;
    &lt;string&gt;es&lt;/string&gt;
    &lt;string&gt;zh-Hant&lt;/string&gt;
    &lt;string&gt;ja&lt;/string&gt;
    &lt;string&gt;de&lt;/string&gt;
&lt;/array&gt;
</code></pre>
<p><strong>各語言使用者的體驗：</strong></p>
<table>
<thead>
<tr>
<th>使用者語言</th>
<th>主 App 顯示</th>
<th>Package 顯示</th>
<th>整體體驗</th>
</tr>
</thead>
<tbody>
<tr>
<td>英文</td>
<td>英文</td>
<td>英文</td>
<td>完美一致</td>
</tr>
<tr>
<td>西班牙文</td>
<td>西班牙文</td>
<td>西班牙文</td>
<td>完美一致</td>
</tr>
<tr>
<td>繁體中文</td>
<td>繁體中文</td>
<td>繁體中文</td>
<td>完美一致</td>
</tr>
<tr>
<td>日文</td>
<td>英文（回退）</td>
<td>日文</td>
<td>不一致但部分本地化</td>
</tr>
<tr>
<td>德文</td>
<td>英文（回退）</td>
<td>德文</td>
<td>不一致但部分本地化</td>
</tr>
</tbody>
</table>
<p><strong>App Store 顯示：</strong> 支援 5 種語言</p>
<p><strong>問題：</strong></p>
<ul>
<li>日文和德文使用者在 App Store 看到「支援日文/德文」</li>
<li>下載後發現只有支付流程是日文/德文，主 App 介面仍是英文</li>
<li>可能造成使用者困惑或負評</li>
</ul>
<h3 id="方案三使用-cfbundleallowmixedlocalizations">方案三：使用 CFBundleAllowMixedLocalizations</h3>
<p><strong>Info.plist：</strong></p>
<pre><code class="language-xml">&lt;key&gt;CFBundleAllowMixedLocalizations&lt;/key&gt;
&lt;true/&gt;
</code></pre>
<p><strong>各語言使用者的體驗：</strong></p>
<table>
<thead>
<tr>
<th>使用者語言</th>
<th>主 App 顯示</th>
<th>Package 顯示</th>
<th>整體體驗</th>
</tr>
</thead>
<tbody>
<tr>
<td>英文</td>
<td>英文</td>
<td>英文</td>
<td>完美一致</td>
</tr>
<tr>
<td>西班牙文</td>
<td>西班牙文</td>
<td>西班牙文</td>
<td>完美一致</td>
</tr>
<tr>
<td>繁體中文</td>
<td>繁體中文</td>
<td>繁體中文</td>
<td>完美一致</td>
</tr>
<tr>
<td>日文</td>
<td>英文（回退）</td>
<td>日文</td>
<td>不一致但部分本地化</td>
</tr>
<tr>
<td>德文</td>
<td>英文（回退）</td>
<td>德文</td>
<td>不一致但部分本地化</td>
</tr>
</tbody>
</table>
<p><strong>App Store 顯示：</strong> 支援 3 種語言</p>
<p><strong>優勢：</strong></p>
<ul>
<li>日文和德文使用者在 App Store 知道 App 主要支援 3 種語言</li>
<li>下載後發現支付流程有日文/德文支援，這是額外的驚喜</li>
<li>使用者期待管理更合理</li>
</ul>
<h2 id="決策指南何時使用哪一種設定">決策指南：何時使用哪一種設定</h2>
<p>根據不同的開發情境，選擇合適的設定策略：</p>
<table>
<thead>
<tr>
<th>情境</th>
<th>推薦方案</th>
<th>理由</th>
</tr>
</thead>
<tbody>
<tr>
<td>Swift Package 語言 ⊆ 主 App 語言</td>
<td>不需要任何設定</td>
<td>系統預設行為已足夠</td>
</tr>
<tr>
<td>短期內會為主 App 加入 Package 支援的所有語言</td>
<td>CFBundleLocalizations</td>
<td>提前宣告，為完整本地化做準備</td>
</tr>
<tr>
<td>Package 支援的額外語言是長期規劃，近期不會在主 App 實作</td>
<td>CFBundleAllowMixedLocalizations</td>
<td>避免誤導使用者，同時提供更好的 Package 體驗</td>
</tr>
<tr>
<td>Package 是核心功能，語言支援是主要賣點</td>
<td>CFBundleLocalizations + 盡快完成主 App 本地化</td>
<td>確保整體體驗一致性</td>
</tr>
<tr>
<td>Package 是輔助功能（如第三方 SDK、工具庫）</td>
<td>CFBundleAllowMixedLocalizations</td>
<td>Package 語言不一致影響較小</td>
</tr>
<tr>
<td>有多個 Package，各自支援不同語言集合</td>
<td>CFBundleAllowMixedLocalizations</td>
<td>統一管理，避免 Info.plist 過於複雜</td>
</tr>
<tr>
<td>企業內部 App，使用者明確知道語言支援範圍</td>
<td>CFBundleLocalizations</td>
<td>可以接受混合語言體驗</td>
</tr>
<tr>
<td>面向大眾市場的 App</td>
<td>CFBundleAllowMixedLocalizations</td>
<td>使用者體驗和期待管理更重要</td>
</tr>
</tbody>
</table>
<h3 id="特殊情境處理">特殊情境處理</h3>
<p><strong>情境 A：同時使用兩個設定</strong></p>
<pre><code class="language-xml">&lt;key&gt;CFBundleLocalizations&lt;/key&gt;
&lt;array&gt;
    &lt;string&gt;en&lt;/string&gt;
    &lt;string&gt;es&lt;/string&gt;
&lt;/array&gt;
&lt;key&gt;CFBundleAllowMixedLocalizations&lt;/key&gt;
&lt;true/&gt;
</code></pre>
<p><strong>結果：</strong> <code>CFBundleAllowMixedLocalizations</code> 會覆蓋 <code>CFBundleLocalizations</code> 的限制，Package 可以使用任何它支援的語言。此設定組合通常沒有必要。</p>
<p><strong>情境 B：Package 的 defaultLocalization 與主 App 不同</strong></p>
<pre><code class="language-swift">// Package.swift
let package = Package(
    defaultLocalization: &quot;ja&quot;  // 主 App 的基礎語言是 &quot;en&quot;
)
</code></pre>
<p><strong>問題：</strong> 當系統找不到合適的語言時，Package 會回退到日文，而主 App 會回退到英文，造成體驗不一致。</p>
<p><strong>解決方案：</strong> 確保 Package 的 <code>defaultLocalization</code> 與主 App 的基礎語言（<code>CFBundleDevelopmentRegion</code>）一致。</p>
<h2 id="實作檢查清單">實作檢查清單</h2>
<p>在實作 Swift Package 多語言支援時，使用此檢查清單確保一切配置正確：</p>
<h3 id="swift-package-端">Swift Package 端</h3>
<ul class="contains-task-list">
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-1848864"> Package.swift 中設定了 <label class="task-list-item-label" for="task-item-1848864"> Package.swift 中設定了 `defaultLocalization`</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-6549468"> 所有語言的資料夾使用正確的命名格式（如 <code>en.lproj</code>、<code>zh-Hans.lproj</code><label class="task-list-item-label" for="task-item-6549468"> 所有語言的資料夾使用正確的命名格式（如 `en.lproj`、`zh-Hans.lproj`）</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-7993816"> <code>Localizable.strings</code><label class="task-list-item-label" for="task-item-7993816"> `Localizable.strings` 檔案使用 UTF-16 編碼</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-3893798"> 在程式碼中使用 <code>Bundle.module</code> 而非 <label class="task-list-item-label" for="task-item-3893798"> 在程式碼中使用 `Bundle.module` 而非 `Bundle.main`</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-5724162"><label class="task-list-item-label" for="task-item-5724162"> 在 Package 內部測試過所有語言的載入</label></li>
</ul>
<h3 id="主-app-端">主 App 端</h3>
<ul class="contains-task-list">
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-395152"> 決定使用 <code>CFBundleLocalizations</code> 或 <label class="task-list-item-label" for="task-item-395152"> 決定使用 `CFBundleLocalizations` 或 `CFBundleAllowMixedLocalizations`</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-5676057"><label class="task-list-item-label" for="task-item-5676057"> 在 Info.plist 中正確加入選擇的設定</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-4657666"> 如果使用 <code>CFBundleLocalizations</code><label class="task-list-item-label" for="task-item-4657666"> 如果使用 `CFBundleLocalizations`，確認所有語言代碼正確</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-4283730"> Package 的 <code>defaultLocalization</code><label class="task-list-item-label" for="task-item-4283730"> Package 的 `defaultLocalization` 與主 App 的基礎語言一致</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-2132086"><label class="task-list-item-label" for="task-item-2132086"> 在實際裝置上測試各種語言環境</label></li>
</ul>
<h3 id="測試驗證">測試驗證</h3>
<ul class="contains-task-list">
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-6222301"><label class="task-list-item-label" for="task-item-6222301"> 切換系統語言，確認 Package 內容正確本地化</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-4388108"><label class="task-list-item-label" for="task-item-4388108"> 測試 Package 支援但主 App 不支援的語言</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-8210257"><label class="task-list-item-label" for="task-item-8210257"> 檢查 App Store Connect 顯示的支援語言清單是否符合預期</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox" id="task-item-9399291"><label class="task-list-item-label" for="task-item-9399291"> 確認沒有使用到不存在的語言代碼（會導致回退）</label></li>
</ul>
<h2 id="總結">總結</h2>
<p>Swift Package 的多語言支援問題源自於 iOS 系統對本地化資源載入的限制機制。理解 <code>CFBundleLocalizations</code> 和 <code>CFBundleAllowMixedLocalizations</code> 的差異，能幫助你根據專案需求做出正確的技術決策：</p>
<ul>
<li><strong>CFBundleLocalizations</strong>：明確宣告支援的語言，適合計劃完整本地化的情境</li>
<li><strong>CFBundleAllowMixedLocalizations</strong>：允許 Package 獨立提供額外語言，適合大多數整合第三方 Package 的情境</li>
</ul>
<p>對於多數開發者而言，<code>CFBundleAllowMixedLocalizations</code> 提供了更靈活且使用者友善的解決方案。它讓你能夠誠實地向使用者呈現 App 的實際語言支援情況，同時允許整合的 Swift Package 提供更豐富的多語言體驗。</p>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlelocalizations">Apple Developer Documentation - CFBundleLocalizations</a></li>
<li><a href="https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleallowmixedlocalizations">Apple Developer Documentation - CFBundleAllowMixedLocalizations</a></li>
<li><a href="https://developer.apple.com/documentation/xcode/localizing-package-resources">Apple Developer Documentation - Localizing Package Resources</a></li>
</ul>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[我偏好的 Coordinator Pattern]]></title>
        <id>https://chiahsien.github.io/post/coordinator-pattern/</id>
        <link href="https://chiahsien.github.io/post/coordinator-pattern/">
        </link>
        <updated>2026-03-28T14:03:00.000Z</updated>
        <summary type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>當我實作畫面流程時，我會為一組「流程」建立一個 Coordinator。如果這組流程很複雜，它可以拆分成多個「子流程」，那每一個子流程也會有對應的 Sub-Coordinator。主流程的 Coordinator 可以管理子流程的 Sub-Coordinator。</p>
<p>Coordinator 用來管理流程的畫面，它負責建立畫面、傳遞資料給畫面、回應畫面的請求、移除畫面等等。如果用 <strong>tree</strong> 來理解畫面流程的話，Coordinator 就是 <strong>root</strong>，各個畫面就是 <strong>leaf</strong>。換個角度看，Coordinator 像一個<strong>容器</strong> — 它自己不產出畫面內容，而是決定裡面放哪些畫面、什麼時候切換。</p>
]]></summary>
        <content type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>當我實作畫面流程時，我會為一組「流程」建立一個 Coordinator。如果這組流程很複雜，它可以拆分成多個「子流程」，那每一個子流程也會有對應的 Sub-Coordinator。主流程的 Coordinator 可以管理子流程的 Sub-Coordinator。</p>
<p>Coordinator 用來管理流程的畫面，它負責建立畫面、傳遞資料給畫面、回應畫面的請求、移除畫面等等。如果用 <strong>tree</strong> 來理解畫面流程的話，Coordinator 就是 <strong>root</strong>，各個畫面就是 <strong>leaf</strong>。換個角度看，Coordinator 像一個<strong>容器</strong> — 它自己不產出畫面內容，而是決定裡面放哪些畫面、什麼時候切換。</p>
<!-- more -->
<h2 id="uikit">UIKit</h2>
<p>在 UIKit 實作 Coordinator Pattern 的時候，我喜歡使用 <code>UIViewController</code> 作為 Coordinator，並且在裡頭內嵌一個 <code>UINavigationController</code>。</p>
<h3 id="coordinator-結構">Coordinator 結構</h3>
<p>Coordinator 持有 <code>UINavigationController</code>，負責建立畫面、處理事件、決定導航行為：</p>
<pre><code class="language-swift">public final class FeatureCoordinator: UIViewController {
    private let rootNav = UINavigationController()

    public override func viewDidLoad() {
        super.viewDidLoad()

        addChild(rootNav)
        rootNav.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(rootNav.view)
        NSLayoutConstraint.activate([
            rootNav.view.topAnchor.constraint(equalTo: view.topAnchor),
            rootNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            rootNav.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            rootNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])
        rootNav.didMove(toParent: self)

        showHome()
    }

    private func showHome() {
        let homeVC = HomeViewController()
        homeVC.delegate = self
        rootNav.pushViewController(homeVC, animated: false)
    }

    private func showDetail(item: String) {
        let detailVC = DetailViewController(item: item)
        detailVC.delegate = self
        rootNav.pushViewController(detailVC, animated: true)
    }

    private func showSettings() {
        let settingsVC = SettingsViewController()
        settingsVC.delegate = self
        rootNav.pushViewController(settingsVC, animated: true)
    }
}

// MARK: - Event Handlers

extension FeatureCoordinator: HomeViewControllerDelegate {
    func homeViewController(
        _ vc: HomeViewController,
        didSelectItem item: String
    ) {
        showDetail(item: item)
    }

    func homeViewControllerDidTapSettings(
        _ vc: HomeViewController
    ) {
        showSettings()
    }
}

extension FeatureCoordinator: DetailViewControllerDelegate {
    func detailViewController(
        _ vc: DetailViewController,
        didSelectRelatedItem item: String
    ) {
        showDetail(item: item)
    }

    func detailViewControllerDidFinish(
        _ vc: DetailViewController
    ) {
        rootNav.popViewController(animated: true)
    }
}

extension FeatureCoordinator: SettingsViewControllerDelegate {
    func settingsViewControllerDidLogOut(
        _ vc: SettingsViewController
    ) {
        rootNav.popToRootViewController(animated: true)
    }
}
</code></pre>
<h3 id="畫面與-coordinator-的溝通偏好-delegate">畫面與 Coordinator 的溝通：偏好 Delegate</h3>
<p>畫面透過 delegate 與 Coordinator 溝通。我偏好 delegate 而非 closure，因為：</p>
<ul>
<li><strong>Contract 明確</strong>：protocol 就是溝通介面的完整定義，一眼就能看出畫面會發出哪些事件</li>
<li><strong>不容易漏</strong>：Xcode 會強制你實作每個 protocol method，不會忘記處理某個事件</li>
<li><strong>可讀性</strong>：當畫面有多種事件需要回傳時，closure 會變成一堆散落的 property，delegate 則集中在一個 protocol 裡</li>
</ul>
<p>Closure 適合一次性、單一事件的回傳（例如 completion block），但在 Coordinator pattern 中，畫面通常有多種事件需要通知 Coordinator，delegate 更合適。</p>
<pre><code class="language-swift">// MARK: - View Delegate Protocols

protocol HomeViewControllerDelegate: AnyObject {
    func homeViewController(
        _ vc: HomeViewController,
        didSelectItem item: String
    )
    func homeViewControllerDidTapSettings(
        _ vc: HomeViewController
    )
}

protocol DetailViewControllerDelegate: AnyObject {
    func detailViewController(
        _ vc: DetailViewController,
        didSelectRelatedItem item: String
    )
    func detailViewControllerDidFinish(
        _ vc: DetailViewController
    )
}

protocol SettingsViewControllerDelegate: AnyObject {
    func settingsViewControllerDidLogOut(
        _ vc: SettingsViewController
    )
}
</code></pre>
<h3 id="子流程管理與反向溝通">子流程管理與反向溝通</h3>
<p>既然 Coordinator 是 <code>UIViewController</code>，子流程的管理就是標準的 UIKit parent-child 關係。呈現子流程用 <code>present</code>，子流程結束後透過 delegate 把結果傳回 parent Coordinator — 溝通方式與畫面 → Coordinator 一致，整個架構的溝通模型是統一的。</p>
<pre><code class="language-swift">// MARK: - Sub-Coordinator Management

protocol SubFeatureCoordinatorDelegate: AnyObject {
    func subFeatureCoordinator(
        _ coordinator: SubFeatureCoordinator,
        didFinishWith result: SubFeatureResult
    )
}

// In parent FeatureCoordinator:
extension FeatureCoordinator {
    func showSubFeature() {
        let subCoordinator = SubFeatureCoordinator()
        subCoordinator.delegate = self
        present(subCoordinator, animated: true)
    }
}

extension FeatureCoordinator: SubFeatureCoordinatorDelegate {
    func subFeatureCoordinator(
        _ coordinator: SubFeatureCoordinator,
        didFinishWith result: SubFeatureResult
    ) {
        coordinator.dismiss(animated: true)
        // Handle result from sub-flow
    }
}
</code></pre>
<p>這正好是 UIViewController-based Coordinator 相較 protocol-based 做法的優勢：你不需要手動從 <code>childCoordinators</code> 陣列移除子 Coordinator，<code>dismiss</code> 就是一切。Sub-Coordinator 如果有需要清理的資源，應該在自己的 <code>deinit</code> 處理 — Coordinator 是 self-contained 的，不該讓 parent 操心內部清理。</p>
<h2 id="swiftui">SwiftUI</h2>
<p>在 SwiftUI 實作 Coordinator Pattern 的時候，對應 UIKit 版的「Coordinator = UIViewController」，我使用 <strong>SwiftUI View 作為 Coordinator</strong>，讓它擁有 <code>NavigationStack</code> 和 <code>@State</code> 導航狀態。</p>
<h3 id="coordinator-結構-2">Coordinator 結構</h3>
<p>Coordinator View 持有 <code>NavigationStack</code> 的 path 和 sheet 狀態，負責建立畫面、處理事件、決定導航行為：</p>
<pre><code class="language-swift">struct FeatureCoordinatorView: View {
    enum Route: Hashable {
        case home
        case detail(String)
        case settings
    }

    @State private var path: [Route] = []
    @State private var sheetRoute: Route?

    var body: some View {
        NavigationStack(path: $path) {
            HomeView(onEvent: handleHomeEvent)
                .navigationDestination(for: Route.self) { route in
                    view(for: route)
                }
        }
        .sheet(item: $sheetRoute) { route in
            NavigationStack {
                view(for: route)
            }
        }
    }

    @ViewBuilder
    private func view(for route: Route) -&gt; some View {
        switch route {
        case .home:
            HomeView(onEvent: handleHomeEvent)
        case .detail(let item):
            DetailView(item: item, onEvent: handleDetailEvent)
        case .settings:
            SettingsView(onEvent: handleSettingsEvent)
        }
    }

    // MARK: - Event Handlers

    private func handleHomeEvent(_ event: HomeView.Event) {
        switch event {
        case .didSelectItem(let item):
            path.append(.detail(item))
        case .didTapSettings:
            path.append(.settings)
        }
    }

    private func handleDetailEvent(_ event: DetailView.Event) {
        switch event {
        case .didSelectRelatedItem(let item):
            path.append(.detail(item))
        case .didFinish:
            if !path.isEmpty { path.removeLast() }
        }
    }

    private func handleSettingsEvent(_ event: SettingsView.Event) {
        switch event {
        case .didLogOut:
            path.removeAll()
        }
    }
}
</code></pre>
<h3 id="畫面與-coordinator-的溝通event-enum-closure">畫面與 Coordinator 的溝通：Event Enum + Closure</h3>
<p>UIKit 版偏好 delegate，SwiftUI 版則改用每個 View 自己定義的 <code>Event</code> enum 搭配 <code>onEvent</code> closure。精神是一樣的 — <strong>contract 明確、不容易漏</strong>。</p>
<p>具體做法：</p>
<ul>
<li>每個 View 內部宣告自己的 <code>Event</code> enum，列出這個 View 會發出的所有事件</li>
<li>Coordinator 建立 View 時必須提供 <code>(Event) -&gt; Void</code>，漏了就 compile error</li>
<li>Handler 裡 switch 這個 concrete enum，Swift 會強制處理每個 case — 不會有 <code>default</code> fallback 的漏洞</li>
</ul>
<p>命名用 <code>Event</code> 而非 <code>NavigationEvent</code>，因為不是每個事件都跟導航有關。更重要的是，Event 的 case 應該描述<strong>發生了什麼事</strong>，而非<strong>指揮 Coordinator 該做什麼導航</strong> — 讓 View 保持 context-independent。</p>
<pre><code class="language-swift">struct DetailView: View {
    enum Event {
        case didSelectRelatedItem(String)
        case didFinish(DetailResult)
    }

    let item: String
    let onEvent: (Event) -&gt; Void

    var body: some View {
        VStack {
            Text(&quot;詳情: \(item)&quot;)

            Button(&quot;完成&quot;) {
                onEvent(.didFinish(.success))
            }
        }
    }
}
</code></pre>
<h3 id="子流程管理與反向溝通-2">子流程管理與反向溝通</h3>
<p>與 UIKit 版一樣，子流程用獨立的 Coordinator View 實作，透過 <code>.sheet</code> 或 <code>.fullScreenCover</code> 呈現。子流程完成後透過 <code>onEvent</code> closure 把結果傳回 parent Coordinator — 溝通方式與畫面 → Coordinator 一致。</p>
<pre><code class="language-swift">struct FeatureCoordinatorView: View {
    // ...
    @State private var isShowingSubFeature = false

    var body: some View {
        NavigationStack(path: $path) {
            // ...
        }
        .sheet(isPresented: $isShowingSubFeature) {
            SubFeatureCoordinatorView(
                onEvent: handleSubFeatureEvent
            )
        }
    }

    private func handleSubFeatureEvent(
        _ event: SubFeatureCoordinatorView.Event
    ) {
        switch event {
        case .didFinish(let result):
            isShowingSubFeature = false
            // Handle result from sub-flow
        }
    }
}

// Sub-Coordinator 也是一個 View，擁有自己的 NavigationStack
struct SubFeatureCoordinatorView: View {
    enum Event {
        case didFinish(SubFeatureResult)
    }

    let onEvent: (Event) -&gt; Void

    @State private var path: [SubRoute] = []

    var body: some View {
        NavigationStack(path: $path) {
            // Sub-flow's root view
            // ...
        }
    }
}
</code></pre>
<h2 id="qa">Q&amp;A</h2>
<h3 id="一定要整個-app-都用-coordinator-嗎">一定要整個 app 都用 Coordinator 嗎？</h3>
<p>不用。Coordinator 可以作為 app 的 root 直接使用，也可以在開發新功能時局部導入 — 為某個功能建立 Coordinator 不需要改動既有架構，侵入性很低。</p>
<h3 id="為什麼選-uiviewcontroller-而不是社群常見的-protocol-based-coordinator">為什麼選 UIViewController 而不是社群常見的 Protocol-based Coordinator？</h3>
<p>社群主流的 Coordinator pattern（如 Soroush Khanlou 原版）是用 <code>Coordinator</code> protocol 搭配 <code>childCoordinators</code> 陣列來管理子流程。我選擇直接用 <code>UIViewController</code> 作為 Coordinator，理由如下：</p>
<ul>
<li><strong>少一層抽象</strong>：Coordinator 的 lifecycle 直接跟 UIKit 綁定，不需要自己維護 <code>start()</code> / <code>stop()</code> 等生命週期方法</li>
<li><strong>不需要手動管理 <code>childCoordinators</code> 陣列</strong>：UIKit 的 <code>children</code>（child view controller）已經替你做了這件事，少一個容易遺漏的 bookkeeping</li>
<li><strong><code>present / dismiss</code> 不需要額外 wiring</strong>：因為 Coordinator 本身就是 <code>UIViewController</code>，所以呈現子流程就是標準的 <code>present(_:animated:)</code>，結束就是 <code>dismiss</code>，不需要額外的 routing 邏輯</li>
</ul>
<h3 id="為什麼不定義一個所有-coordinator-都-conform-的-protocol">為什麼不定義一個所有 Coordinator 都 conform 的 protocol？</h3>
<p>我刻意不定義一個所有 Coordinator 都要 conform 的 <code>CoordinatorProtocol</code>。Coordinator 是一個概念，不是一個介面。</p>
<p>每個流程的需求不同 — 有的要處理 deep link，有的不用；有的有子流程，有的只有兩個畫面。硬定義一個共用 protocol（<code>start()</code>、<code>childCoordinators</code>、<code>router</code> 等）反而綁手綁腳，逼你為了滿足 protocol 而寫不需要的東西。</p>
<p>依需求開發每一個 Coordinator，比套一個 protocol 更實際。</p>
<h3 id="這樣做對畫面復用有什麼幫助">這樣做對畫面復用有什麼幫助？</h3>
<p>Coordinator 全權管理導航，帶來一個實務上很重要的好處：<strong>畫面不需要知道自己被放在什麼容器裡</strong>。</p>
<p>畫面不用管自己是被 push 進 NavigationStack、用 sheet 呈現、還是當 tab 的 root — 它只負責顯示內容、發出事件。導航相關的 UI（back button、close button、toolbar action、tab item）全部由 Coordinator 或容器決定，畫面本身不碰。</p>
<p>主要的例外是 title：畫面可以自己聲明標題。在 UIKit 中是 <code>self.title</code>，在 SwiftUI 中是 <code>.navigationTitle</code> — 兩者的性質一樣，都是「我叫什麼名字」，不是「我要怎麼被導航」。至於標題最終怎麼呈現，是容器的事。</p>
<p>這讓同一個畫面可以在不同情境直接復用，不用為了換容器而改畫面的程式碼。</p>
<h3 id="uikit-用-delegate-swiftui-用-closure為什麼不統一">UIKit 用 delegate、SwiftUI 用 closure，為什麼不統一？</h3>
<p>兩個框架選擇不同溝通方式，是因為架構特性不同：</p>
<ul>
<li><strong>UIKit 用 delegate</strong>：ViewController 是 reference type（class），closure capture <code>self</code> 容易造成 retain cycle，每個地方都要寫 <code>[weak self]</code>。Delegate 用 <code>weak</code> 一次解決，而且整個 UIKit 框架本身就大量使用 delegate（<code>UITableViewDelegate</code>、<code>UITextFieldDelegate</code> 等），風格一致</li>
<li><strong>SwiftUI 用 closure</strong>：View 是 value type（struct），沒有 retain cycle 的問題。SwiftUI 框架本身就是 closure 慣例 — <code>Button(action:)</code>、<code>.onTapGesture {}</code>、<code>.task {}</code> 全部都是 closure，用 delegate 反而格格不入。而且 View struct 會被頻繁重建，delegate 這種需要 assign reference 的模式不適合這個生命週期</li>
</ul>
<p>不是 closure 或 delegate 誰絕對更好，而是<strong>跟著框架的慣例和型別系統走</strong>。</p>
<h3 id="coordinator-要負責-dependency-injection-嗎">Coordinator 要負責 dependency injection 嗎？</h3>
<p>Coordinator 負責管理流程，不負責規定 dependency 怎麼到達畫面。Coordinator 建立畫面時注入 dependency、畫面自己從 DI container 取得、或透過 SwiftUI 的 environment 傳遞，都可以 — 視專案的 DI 策略決定。</p>
<h3 id="deep-link-怎麼跟-coordinator-整合">Deep link 怎麼跟 Coordinator 整合？</h3>
<p>收到 deep link 時，Coordinator 如果知道怎麼處理就直接顯示對應畫面；如果不知道，就詢問它能建立的 Sub-Coordinator — 每個 Sub-Coordinator 提供一個 static method 來回答自己能不能處理這個 deep link。Parent Coordinator 找到能處理的 Sub-Coordinator 後，建立並呈現它。這個 resolve 過程沿著 tree 從 root 往 leaf 遞迴，跟 Coordinator 管理畫面的結構一致。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[自動排序 Xcode 專案檔以減少合併衝突]]></title>
        <id>https://chiahsien.github.io/post/sort-xcode-project-file-reduce-merge-conflicts/</id>
        <link href="https://chiahsien.github.io/post/sort-xcode-project-file-reduce-merge-conflicts/">
        </link>
        <updated>2026-02-15T14:48:37.000Z</updated>
        <summary type="html"><![CDATA[<p>Xcode 的 <code>project.pbxproj</code> 檔案採用文字格式儲存專案結構，但 Xcode 在新增檔案或修改設定時，不保證項目的插入順序一致。多人協作時，即使修改不同的檔案或 target，也可能因為項目順序差異而產生 merge conflict。這些衝突往往與實際變更無關，純粹是格式問題。</p>
<h2 id="我的解決方案">我的解決方案</h2>
<p>將 <code>project.pbxproj</code> 中的各個區塊按固定規則排序，確保相同內容產生相同的檔案結構。配合 git pre-commit hook，每次提交前自動排序，團隊成員的專案檔就能維持一致的順序，大幅降低無意義的衝突。</p>
<p>我開發了一個腳本工具來執行這個任務，也已經在多個專案上跑了好幾年，GitHub repo 放在這裡：<a href="https://github.com/chiahsien/sort-Xcode-project-file">https://github.com/chiahsien/sort-Xcode-project-file</a></p>
<p>雖然目前推崇使用 Swift Package Manager 進行模組化，Xcode 16 也引入了 buildable folders 功能來減少專案檔變更，甚至也有 Tuist 或 Xcode Gen 這類的工具來生成專案檔，但這些新技術主要針對新專案或願意大幅重構的專案。對於已經開發多年、結構複雜的舊專案，貿然改用 SPM 模組化或轉換成 folder references 風險過高。此時，這個排序工具仍是最務實的選擇，能以最小成本解決 merge conflict 問題。</p>
]]></summary>
        <content type="html"><![CDATA[<p>Xcode 的 <code>project.pbxproj</code> 檔案採用文字格式儲存專案結構，但 Xcode 在新增檔案或修改設定時，不保證項目的插入順序一致。多人協作時，即使修改不同的檔案或 target，也可能因為項目順序差異而產生 merge conflict。這些衝突往往與實際變更無關，純粹是格式問題。</p>
<h2 id="我的解決方案">我的解決方案</h2>
<p>將 <code>project.pbxproj</code> 中的各個區塊按固定規則排序，確保相同內容產生相同的檔案結構。配合 git pre-commit hook，每次提交前自動排序，團隊成員的專案檔就能維持一致的順序，大幅降低無意義的衝突。</p>
<p>我開發了一個腳本工具來執行這個任務，也已經在多個專案上跑了好幾年，GitHub repo 放在這裡：<a href="https://github.com/chiahsien/sort-Xcode-project-file">https://github.com/chiahsien/sort-Xcode-project-file</a></p>
<p>雖然目前推崇使用 Swift Package Manager 進行模組化，Xcode 16 也引入了 buildable folders 功能來減少專案檔變更，甚至也有 Tuist 或 Xcode Gen 這類的工具來生成專案檔，但這些新技術主要針對新專案或願意大幅重構的專案。對於已經開發多年、結構複雜的舊專案，貿然改用 SPM 模組化或轉換成 folder references 風險過高。此時，這個排序工具仍是最務實的選擇，能以最小成本解決 merge conflict 問題。</p>
<!-- more -->
<h2 id="排序範圍">排序範圍</h2>
<p>此工具排序以下 array 結構：</p>
<ul>
<li><code>children</code> — group 內的檔案與 subgroup（目錄排在檔案前面）</li>
<li><code>files</code> — build phase 的檔案列表</li>
<li><code>buildConfigurations</code> — build configuration 列表</li>
<li><code>targets</code> — 專案 target 列表</li>
<li><code>packageProductDependencies</code> — Swift Package product dependency</li>
<li><code>packageReferences</code> — Swift Package reference</li>
</ul>
<h2 id="安全性">安全性</h2>
<p>這些 array 是宣告性內容，Xcode 透過 24 字元的十六進位 ID 參照物件，而非依賴位置。排序不影響建置行為或專案結構，僅改變檔案內的呈現順序。</p>
<p>不排序的區塊：<code>PBXFrameworksBuildPhase</code> section 的 framework 連結順序會影響符號解析，工具會偵測到這個區塊並完整保留原始順序。其餘不在上述排序範圍內的 array（例如 <code>buildPhases</code>）則不會被處理，同樣維持原始順序。</p>
<p>寫入方式採用原子操作（透過暫存檔 + <code>os.replace()</code>），即使過程中發生錯誤，也不會留下損壞的 <code>.pbxproj</code> 檔案。</p>
<p><strong>警告：</strong><br>
雖然這個工具已經在多個不同專案執行很長一段時間了，我還是強烈建議在修改之前先做好備份，才不會出現難以挽回的錯誤！</p>
<h2 id="與原版的差異">與原版的差異</h2>
<p>本版本是 WebKit 專案的 fork，以 Python 3 重寫並新增以下功能：</p>
<ul>
<li><strong>Natural sorting</strong>：數字部分按數值比較，<code>file2.m</code> 排在 <code>file10.m</code> 前面，符合人類直覺</li>
<li><strong>Case-insensitive 選項</strong>：提供 <code>--case-insensitive</code> 參數支援不分大小寫排序，預設仍為 case-sensitive 以保持原始行為</li>
<li><strong>目錄優先排序</strong>：<code>children</code> array 中目錄排在檔案前面，符合檔案系統慣例</li>
<li><strong>自動去除重複</strong>：移除重複的項目 reference</li>
<li><strong>擴充排序範圍</strong>：包含所有 <code>children</code> array、<code>files</code> array、<code>targets</code> 列表、<code>packageProductDependencies</code> 與 <code>packageReferences</code></li>
<li><strong>CI 檢查模式</strong>：<code>--check</code> 參數可檢查檔案是否已排序，不修改檔案，適合整合到 CI pipeline</li>
<li><strong>遞迴搜尋</strong>：<code>-r</code> 參數可遞迴搜尋目錄下所有 <code>project.pbxproj</code> 並排序，適合 monorepo</li>
<li><strong>Stdin/stdout 支援</strong>：使用 <code>-</code> 參數可從 stdin 讀取、寫到 stdout，方便管線操作</li>
<li><strong>原子寫入</strong>：透過暫存檔 + <code>os.replace()</code> 確保寫入過程不會損壞原始檔案</li>
</ul>
<h2 id="使用方法">使用方法</h2>
<h3 id="基本呼叫">基本呼叫</h3>
<pre><code class="language-bash">python3 sort-Xcode-project-file.py path/to/Project.xcodeproj
</code></pre>
<p>腳本會自動找到 <code>project.pbxproj</code> 並就地排序。也可以直接指定 <code>project.pbxproj</code> 檔案路徑。</p>
<h3 id="選項">選項</h3>
<pre><code class="language-bash"># 使用 case-insensitive sorting
python3 sort-Xcode-project-file.py --case-insensitive Project.xcodeproj

# CI 檢查模式：exit 0 = 已排序，exit 1 = 未排序（不修改檔案）
python3 sort-Xcode-project-file.py --check Project.xcodeproj

# 遞迴搜尋目錄下所有 project.pbxproj 並排序
python3 sort-Xcode-project-file.py -r .

# 從 stdin 讀取，寫到 stdout
cat project.pbxproj | python3 sort-Xcode-project-file.py - &gt; sorted.pbxproj

# 抑制 warning 訊息
python3 sort-Xcode-project-file.py -w Project.xcodeproj

# 顯示版本號
python3 sort-Xcode-project-file.py --version

# 顯示說明
python3 sort-Xcode-project-file.py --help
</code></pre>
<h3 id="git-pre-commit-hook-整合">Git Pre-commit Hook 整合</h3>
<p>在專案根目錄建立 <code>Scripts</code> 目錄，將 <code>sort-Xcode-project-file.py</code> 放進去，然後建立 <code>.git/hooks/pre-commit</code>：</p>
<pre><code class="language-bash">#!/bin/sh

echo 'Sorting Xcode project files'

GIT_ROOT=$(git rev-parse --show-toplevel)
sorter=&quot;$GIT_ROOT/Scripts/sort-Xcode-project-file.py&quot;

git diff --name-only --cached | grep &quot;project.pbxproj&quot; | while IFS= read -r filePath; do
  fullFilePath=&quot;$GIT_ROOT/$filePath&quot;
  python3 &quot;$sorter&quot; &quot;$fullFilePath&quot;
  git add &quot;$fullFilePath&quot;
done

echo 'Done sorting Xcode project files'
</code></pre>
<p>記得設定執行權限：</p>
<pre><code class="language-bash">chmod +x .git/hooks/pre-commit
</code></pre>
<p>另外可以在 <code>.gitattributes</code> 加上以下設定，進一步減少合併衝突：</p>
<pre><code>*.pbxproj merge=union
</code></pre>
<blockquote>
<p><strong>注意：</strong> <code>merge=union</code> 會讓 Git 自動保留衝突的雙方內容。搭配排序工具使用效果很好，但如果專案檔沒有經過排序，可能會產生無效的結果。請確保團隊成員都有使用這個排序工具。</p>
</blockquote>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[讓 KOReader 的資料夾顯示書籍封面]]></title>
        <id>https://chiahsien.github.io/post/koreader-patch-browser-cover/</id>
        <link href="https://chiahsien.github.io/post/koreader-patch-browser-cover/">
        </link>
        <updated>2026-02-01T15:18:52.000Z</updated>
        <summary type="html"><![CDATA[<p>這次要介紹的是我為 KOReader 檔案瀏覽（Mosaic）所做的另一個 userpatch：<code>2-browser-folder-cover.lua</code>。這個 patch 可以讓資料夾在 Mosaic 檢視時顯示封面圖片，支援放置自訂 <code>.cover</code> 檔案，若無自訂封面則會自動從該資料夾或其子資料夾的書籍取得封面。此外還提供兩種顯示風格：單一封面或 2×2 格狀封面。</p>
<p>下載路徑：<a href="https://github.com/chiahsien/KOReader.Patches">https://github.com/chiahsien/KOReader.Patches</a></p>
]]></summary>
        <content type="html"><![CDATA[<p>這次要介紹的是我為 KOReader 檔案瀏覽（Mosaic）所做的另一個 userpatch：<code>2-browser-folder-cover.lua</code>。這個 patch 可以讓資料夾在 Mosaic 檢視時顯示封面圖片，支援放置自訂 <code>.cover</code> 檔案，若無自訂封面則會自動從該資料夾或其子資料夾的書籍取得封面。此外還提供兩種顯示風格：單一封面或 2×2 格狀封面。</p>
<p>下載路徑：<a href="https://github.com/chiahsien/KOReader.Patches">https://github.com/chiahsien/KOReader.Patches</a></p>
<!-- more -->
<p><strong>重點摘要</strong></p>
<ul>
<li>功能：資料夾顯示自訂或來源於書籍的封面；支援單一封面與 2×2 格狀兩種風格；遞迴搜尋子資料夾。</li>
<li>來源：修改自 <code>sebdelsol/KOReader.patches</code> 的 <code>2-browser-folder-cover.lua</code>，新增格狀封面、遞迴搜尋、<code>.cover</code> 支援、非同步載入、e-ink 閃爍消除、LRU 快取與 UI 選項。</li>
</ul>
<h2 id="為什麼會需要這個-patch">為什麼會需要這個 patch</h2>
<p>KOReader 的 Mosaic 檢視本身會以書籍封面作為格子顯示；但資料夾通常只會顯示資料夾名稱或預設圖示。這個 patch 補強了資料夾的視覺表現，讓資料夾也能像書籍一樣顯示代表性的封面，改進瀏覽體驗，特別適合把資料夾當作書櫃或系列集合來管理的使用者。</p>
<h2 id="主要功能">主要功能</h2>
<ul>
<li>支援自訂封面檔案：將自訂圖片放在資料夾內，檔名前綴為 <code>.cover</code> 並附上副檔名（範例：<code>.cover.jpg</code>、<code>.cover.png</code>、<code>.cover.webp</code>）。</li>
<li>兩種封面風格：
<ul>
<li><strong>Single cover</strong>（單一封面）：每個資料夾顯示一張書籍封面，底部對齊。</li>
<li><strong>Grid (2×2)</strong>（格狀封面）：最多顯示四張書籍封面以 2×2 格狀排列，每格使用 aspect fill（裁切溢出以填滿格子）。支援不完整的格狀排列：2 張填滿上排、3 張多填左下角。只找到 1 張時自動退回單一封面顯示。</li>
</ul>
</li>
<li>自動從書籍封面取代：若沒有 <code>.cover</code>，會在資料夾內尋找有效的書籍封面並使用。</li>
<li>遞迴搜尋子資料夾：若資料夾本身沒有可用封面，會往下搜尋子資料夾（預設深度 3）以找到合適的書籍封面。</li>
<li>非同步封面載入：當書籍封面尚未被 KOReader 擷取時，資料夾格子會在封面就緒後自動重新整理，不需手動操作。</li>
<li>e-ink 閃爍消除：資料夾跳過預設的 <code>original_update()</code> 流程，避免先畫預設圖示再替換封面所產生的 e-ink 閃爍。</li>
<li>性能優化：Per-directory LRU widget 快取（最多保留 10 個目錄）與封面來源快取，避免重複掃描目錄；settings version 追蹤機制，只在設定變更時才清除快取。</li>
</ul>
<h2 id="封面搜尋順序">封面搜尋順序</h2>
<p>Patch 依以下優先順序決定資料夾封面：</p>
<ol>
<li><strong>自訂 <code>.cover</code> 檔案</strong>：檢查資料夾內是否有 <code>.cover.{jpg,jpeg,png,webp,gif}</code>。找到即直接使用，不論目前選擇的封面風格為何，自訂封面一律以單一封面方式顯示。</li>
<li><strong>封面來源快取</strong>：若該資料夾先前已解析過書籍封面路徑，直接重用，跳過目錄掃描。</li>
<li><strong>掃描資料夾內的書籍</strong>：呼叫 <code>BookInfoManager:getBookInfo()</code> 逐一檢查檔案，收集有效封面（grid 模式最多 4 張、single 模式 1 張）。</li>
<li><strong>遞迴搜尋子資料夾</strong>：若仍需更多封面，往下遞迴搜尋子資料夾（最多深度 3）以尋找額外的書籍封面。</li>
</ol>
<p>若封面仍在背景擷取中，資料夾格子會註冊到 CoverBrowser 的 polling 機制，待封面就緒後自動重試。</p>
<h2 id="與上游原作者差異">與上游（原作者）差異</h2>
<p>此版本基於 <code>sebdelsol/KOReader.patches</code> 的實作，但做了下列主要改動：</p>
<ul>
<li>新增 2×2 格狀封面模式（Grid mode），支援不完整排列。</li>
<li>新增對自訂 <code>.cover</code> 檔案的偵測與使用。</li>
<li>新增遞迴搜尋子資料夾以尋找書籍封面（避免空資料夾顯示預設圖示）。</li>
<li>新增非同步封面載入，封面未就緒時自動重試。</li>
<li>消除 e-ink 閃爍：資料夾直接設定封面，不再先畫預設圖示。</li>
<li>改用 Per-directory LRU widget 快取（最多 10 個目錄）與封面來源快取，大幅降低 UI 建構成本。</li>
<li>尊重 KOReader 既有的封面快取有效性檢查，避免使用過期封面。</li>
</ul>
<h2 id="安裝與使用">安裝與使用</h2>
<ol>
<li>
<p>將 <code>2-browser-folder-cover.lua</code> 複製到 KOReader 的 <code>patches</code> 資料夾（通常位於 <code>&lt;koreader_data_dir&gt;/patches/</code>）。常見路徑：</p>
<ul>
<li>Kobo: <code>/mnt/onboard/.adds/koreader/patches/</code></li>
<li>Kindle: <code>/mnt/us/documents/koreader/patches/</code></li>
<li>Android: <code>/sdcard/koreader/patches/</code></li>
<li>Desktop: <code>~/.koreader/patches/</code></li>
</ul>
</li>
<li>
<p>重新啟動 KOReader，patch 會在啟動時自動載入。</p>
</li>
<li>
<p>使用方法：</p>
<ul>
<li>若要使用自訂封面，於資料夾放入檔名為 <code>.cover</code> 並含有副檔名的圖片檔（例如 <code>.cover.jpg</code>）。</li>
<li>若未放 <code>.cover</code>，patch 會先檢查該資料夾內的書籍封面，找不到時再往子資料夾遞迴搜尋（最多 3 層）。</li>
<li>可於 KOReader 設定中找到新增的選項（File browser settings → Mosaic and detailed list settings）：
<ul>
<li><strong>Folder cover style</strong>：子選單，可選擇 &quot;Single cover&quot;（預設）或 &quot;Grid (2×2)&quot;。</li>
<li>Crop folder custom image（裁切自訂封面，預設啟用）</li>
<li>Show folder name（顯示資料夾名稱，預設啟用）</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="注意事項與建議">注意事項與建議</h2>
<ul>
<li><code>.cover</code> 的優先度高於書籍封面：一旦發現 <code>.cover</code>，patch 會直接使用該圖片並略過書籍封面搜尋。自訂封面一律以單一封面顯示，不受封面風格設定影響。</li>
<li>若資料夾或子資料夾包含大量檔案或深度很深，遞迴搜尋可能帶來額外的檔案系統存取成本，建議將預設深度（目前程式內使用 3）視情況調整或僅在目標目錄使用 <code>.cover</code>。</li>
<li>若發現封面顯示不正常，請先檢查 <code>BookInfoManager</code> 是否已正確擷取並快取了該書的封面，或在 KOReader 中重建封面快取。</li>
</ul>
<h2 id="授權與來源">授權與來源</h2>
<p>此 patch 為我自行維護的衍生版本，原始實作與靈感來自：</p>
<ul>
<li><a href="https://github.com/sebdelsol/KOReader.patches">sebdelsol/KOReader.patches</a></li>
</ul>
<p>若想回到上游版本或查看原始程式碼，可參考上面連結。</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[如何為單一 Feature 建立 Swift Package]]></title>
        <id>https://chiahsien.github.io/post/swift-package-with-resources/</id>
        <link href="https://chiahsien.github.io/post/swift-package-with-resources/">
        </link>
        <updated>2025-11-03T15:12:51.000Z</updated>
        <summary type="html"><![CDATA[<p>在專案開發到一定規模後，你可能會發現某些 feature 其實相對獨立：它們有自己的流程、畫面、資源檔，甚至可以被其他專案重用。這時候，最乾淨、最有彈性的做法，就是把它抽成 <strong>Swift Package</strong>。</p>
<p>以我最近在做的功能為例，它是一個完整的獨立模組 - 有多個頁面、支援多國語言、使用圖片與動畫資源。為了避免日後整合時出現命名衝突、相依過重或編譯過慢的問題，我選擇把它獨立成一個 Swift Package。在將 feature 抽出到 Package 的過程我也踩到了一些坑，趁著這個機會記錄下來，以免日後忘記。</p>
<h2 id="為什麼要把-feature-打包成-swift-package">為什麼要把 Feature 打包成 Swift Package？</h2>
<p>建立專屬的 Swift Package 有幾個明顯的好處：</p>
<ul>
<li>✅ <strong>可獨立開發與測試</strong>：模組化後不必依賴主專案，可單獨編譯與驗證。</li>
<li>⚙️ <strong>降低相依與衝突</strong>：減少命名重複、依賴鏈過長等問題。</li>
<li>🚀 <strong>加快編譯速度</strong>：主專案不需每次都重新編譯整個功能。</li>
<li>🔄 <strong>方便整合與重用</strong>：未來可以直接被其他 app 或團隊使用。</li>
</ul>
]]></summary>
        <content type="html"><![CDATA[<p>在專案開發到一定規模後，你可能會發現某些 feature 其實相對獨立：它們有自己的流程、畫面、資源檔，甚至可以被其他專案重用。這時候，最乾淨、最有彈性的做法，就是把它抽成 <strong>Swift Package</strong>。</p>
<p>以我最近在做的功能為例，它是一個完整的獨立模組 - 有多個頁面、支援多國語言、使用圖片與動畫資源。為了避免日後整合時出現命名衝突、相依過重或編譯過慢的問題，我選擇把它獨立成一個 Swift Package。在將 feature 抽出到 Package 的過程我也踩到了一些坑，趁著這個機會記錄下來，以免日後忘記。</p>
<h2 id="為什麼要把-feature-打包成-swift-package">為什麼要把 Feature 打包成 Swift Package？</h2>
<p>建立專屬的 Swift Package 有幾個明顯的好處：</p>
<ul>
<li>✅ <strong>可獨立開發與測試</strong>：模組化後不必依賴主專案，可單獨編譯與驗證。</li>
<li>⚙️ <strong>降低相依與衝突</strong>：減少命名重複、依賴鏈過長等問題。</li>
<li>🚀 <strong>加快編譯速度</strong>：主專案不需每次都重新編譯整個功能。</li>
<li>🔄 <strong>方便整合與重用</strong>：未來可以直接被其他 app 或團隊使用。</li>
</ul>
<!-- more -->
<hr>
<h2 id="問題一如何支援多國語言">問題一：如何支援多國語言？</h2>
<p>若要在 Package 內使用多國語言，有兩件事情一定要搞清楚：</p>
<ol>
<li><strong>檔案結構要正確放置。</strong></li>
<li><strong>讀取時要明確指定 <code>.module</code>。</strong></li>
</ol>
<h3 id="正確的資料夾結構">正確的資料夾結構</h3>
<p>根據 Apple 官方文件，多國語言檔（<code>.lproj</code>）必須直接放在 <code>Resources</code> 資料夾底下，<strong>不能再有子目錄</strong>。正確的結構如下：</p>
<pre><code>MyPackage/
├─ Sources/
│  └─ MyLibrary/
│     └─ Resources/
│        ├─ en.lproj/
│        │  └─ Localizable.strings
│        ├─ zh-Hant.lproj/
│        │  └─ Localizable.strings
│        └─ Strings.dict
</code></pre>
<p>這樣做可以確保 <code>.lproj</code> 檔會被正確地讀取與載入。若放錯層級，Xcode 雖然不會報錯，但翻譯字串就是不會出現。</p>
<h3 id="正確的呼叫方式">正確的呼叫方式</h3>
<p>在 Package 內呼叫多國語言字串時，別忘了指定 <code>bundle: .module</code>。否則 Swift 會自動去主專案尋找對應字串，導致載不到 Package 內的翻譯。</p>
<pre><code class="language-swift">extension String {
    var localized: String {
        NSLocalizedString(
            self,
            tableName: &quot;Localizable&quot;,
            bundle: .module,    // 關鍵：指定為 package 的 bundle
            value: self,
            comment: &quot;&quot;
        )
    }
}
</code></pre>
<p>使用時只要 <code>&quot;Some.Localized.String.Key&quot;.localized</code> 就能正確取回包內的翻譯字串。這樣做讓 package 在任何專案中都能獨立運作，無需依賴主專案的語言設定。</p>
<hr>
<h2 id="問題二如何使用圖片與動畫資源">問題二：如何使用圖片與動畫資源？</h2>
<p>圖片與多國語言的處理方式類似，同樣要注意<strong>資料夾結構</strong>與<strong>載入方式</strong>。</p>
<h3 id="圖片資源結構">圖片資源結構</h3>
<p>官方建議將圖片檔（<code>.xcassets</code>）放在 <code>Resources</code> 目錄底下。若你使用像 Lottie 這類第三方函式庫，也可以把相關 JSON 資源放在同個目錄。</p>
<pre><code>MyPackage/
├─ Sources/
│  └─ MyLibrary/
│     └─ Resources/
│        ├─ Images.xcassets/
│        │  ├─ Avatar.imageset/
│        │  ├─ Error.imageset/
│        │  └─ ...
│        └─ Lottie/
│           ├─ greeting.json
│           └─ ...
</code></pre>
<h3 id="呼叫圖片的方式">呼叫圖片的方式</h3>
<p>有兩種常見方式可以載入圖片：</p>
<ol>
<li><code>UIImage(resource: .xxx)</code></li>
<li><code>UIImage(named: &quot;xxx&quot;, in: .module, with: nil)</code></li>
</ol>
<p>而如果你使用 Lottie 動畫，則要記得加上 <code>bundle</code>：</p>
<p><code>LottieAnimationView(name: &quot;greeting&quot;, bundle: .module)</code></p>
<p>這樣才能確保動畫資源來自 package，而不是主專案。</p>
<hr>
<h2 id="問題三packageswift-要怎麼設定">問題三：Package.swift 要怎麼設定？</h2>
<p>最後一步，就是在 <code>Package.swift</code> 中設定好本地化與資源處理。這一步如果漏掉，前面做的一切可能都不會生效。</p>
<pre><code class="language-swift">let package = Package(
    name: &quot;MyLibrary&quot;,
    defaultLocalization: &quot;en&quot;,  // 一定要設定預設語言
    platforms: [
        .iOS(.v16)
    ],
    products: [
        .library(
            name: &quot;MyLibrary&quot;,
            targets: [&quot;MyLibrary&quot;]
        ),
    ],
    dependencies: [
        .package(url: &quot;https://github.com/airbnb/lottie-spm.git&quot;, from: &quot;4.5.2&quot;),
    ],
    targets: [
        .target(
            name: &quot;MyLibrary&quot;,
            dependencies: [
                .product(name: &quot;Lottie&quot;, package: &quot;lottie-spm&quot;),
            ],
            resources: [
                .process(&quot;Resources&quot;)  // 使用 .process 讓 Xcode 處理資源檔
            ]
        ),
    ]
)
</code></pre>
<p>這裡的兩個重點是：</p>
<ul>
<li><code>defaultLocalization</code>：沒有這行，多國語言會無法自動套用。</li>
<li><code>.process(&quot;Resources&quot;)</code>：讓 Xcode 知道要把資源包含進 target。</li>
</ul>
<hr>
<h2 id="結語讓-feature-真正成為可重用的模組">結語：讓 Feature 真正成為可重用的模組</h2>
<p>當一個功能變得越來越複雜時，把它獨立成 Swift Package 不只是整潔問題，更是 <strong>架構與維護性的升級</strong>。</p>
<p>模組化能讓你：</p>
<ul>
<li>更容易進行單元測試；</li>
<li>快速重用或移植到新專案；</li>
<li>明確定義每個功能的邊界與責任。</li>
</ul>
<p>只要依照上面的步驟設定語言與資源，就能建立一個乾淨、可移植、可重用的 Feature Package。當你下一次打開 Xcode 時，或許就會開始想：「這個功能，其實也該是一個獨立的 package 吧？」</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[我的 Mac 設定]]></title>
        <id>https://chiahsien.github.io/post/my-mac-setup/</id>
        <link href="https://chiahsien.github.io/post/my-mac-setup/">
        </link>
        <updated>2025-07-26T06:36:04.000Z</updated>
        <summary type="html"><![CDATA[<p>工程師都會有自己習慣的電腦設定，我自然也不例外。本文記錄了我自己的環境建置，方便以後換電腦或換工作時可以快速 setup。</p>
]]></summary>
        <content type="html"><![CDATA[<p>工程師都會有自己習慣的電腦設定，我自然也不例外。本文記錄了我自己的環境建置，方便以後換電腦或換工作時可以快速 setup。</p>
<!-- more -->
<hr>
<h2 id="01-系統地基-system-foundation">01. 系統地基 (System Foundation)</h2>
<p>拿到新電腦的第一步，先把終端機與套件管理搞定，這是所有開發工作的基礎。</p>
<h3 id="套件管理homebrew">套件管理：Homebrew</h3>
<p><a href="https://brew.sh/">Homebrew</a> 是 macOS 必備的套件管理工具。</p>
<pre><code class="language-shell">/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;
</code></pre>
<p><strong>Apple Silicon (M1/M2/M3...) 設定注意：</strong></p>
<p>安裝完成後，為了讓系統能正確找到 <code>brew</code> 指令，建議將環境變數設定寫入 <code>~/.zprofile</code>（而非 <code>.zshrc</code>），這樣能避免每次開新分頁都重複執行，提升效能：</p>
<pre><code class="language-shell">echo 'eval &quot;$(/opt/homebrew/bin/brew shellenv)&quot;' &gt;&gt; ~/.zprofile
eval &quot;$(/opt/homebrew/bin/brew shellenv)&quot;
</code></pre>
<h3 id="終端機與-shell">終端機與 Shell</h3>
<ul>
<li><strong>Terminal App</strong>
<ul>
<li>我改用 <a href="https://ghostty.org/">Ghostty</a>（推薦，已推出正式版）或 <a href="https://iterm2.com/">iTerm2</a> 取代內建終端機。</li>
<li>如果需要更強大的功能，可以試試 <a href="https://tabby.sh/">Tabby</a>，它跨平台且支援 SSH / Serial / Telnet 連線。</li>
<li><em>Tip: Ghostty 雖然強調開箱即用，還是可以透過修改設定檔自訂。有人建立了 <a href="https://spectre-ghostty-config.vercel.app/">這個網站</a> 方便使用者調整設定。</em></li>
</ul>
</li>
<li><strong>Shell</strong>
<ul>
<li><a href="https://ohmyz.sh/">Oh My Zsh</a>：讓 Zsh 更好用、更漂亮的必裝框架。</li>
</ul>
</li>
</ul>
<pre><code class="language-shell">sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
</code></pre>
<p>如果安裝了 Oh My Zsh 又想要自動補完 brew 的指令，記得在 <code>~/.zprofile</code> 加入：</p>
<pre><code class="language-shell">FPATH=&quot;$(brew --prefix)/share/zsh/site-functions:${FPATH}&quot;
</code></pre>
<h3 id="版本控制核心git">版本控制核心：Git</h3>
<p><a href="https://git-scm.com/">Git</a> 是版本控制的靈魂。</p>
<pre><code class="language-shell">brew install git
brew install git-lfs
</code></pre>
<hr>
<h2 id="02-開發與語言環境-coding-environment">02. 開發與語言環境 (Coding Environment)</h2>
<h3 id="編輯器與-ide">編輯器與 IDE</h3>
<ul>
<li><strong><a href="https://code.visualstudio.com/">Visual Studio Code</a></strong>：我的首選編輯器，理由是「速度快」、「界面友好」、「套件生態系豐富」。必備套件可參考 <a href="https://chiahsien.github.io/post/visual-studio-code-extensions/">這篇文章</a>。</li>
<li><strong><a href="https://developer.apple.com/xcode/">Xcode</a></strong>：iOS/macOS 開發必備。
<ul>
<li>推薦使用 <a href="https://github.com/RobotsAndPencils/XcodesApp">Xcodes</a> 來管理多版本 Xcode，詳情參考 <a href="https://chiahsien.github.io/post/xcode-the-right-way/">這篇文章</a>。</li>
<li>定期使用 <a href="https://github.com/vashpan/xcode-dev-cleaner">DevCleaner</a> 清理肥大的暫存檔。</li>
<li>搭配 <a href="https://github.com/github/CopilotForXcode">Github Copilot for Xcode</a> 輔助開發。</li>
</ul>
</li>
<li><strong><a href="https://zed.dev/">Zed</a></strong>：使用 Rust 開發的高效能編輯器，速度極快且內建 AI 支援，適合追求極致效能或需要開啟大檔案的人。</li>
<li><strong><a href="https://www.sublimetext.com/">Sublime Text</a></strong>：以前用過，真的很快，但因介面不夠友善懶得折騰而放棄。</li>
<li><strong><a href="https://typora.io/">Typora</a></strong>：Markdown 編輯器首選，<strong>所見即所得</strong>的體驗與佈景主題讓我離不開它。
<ul>
<li><em>輕量替代品：<a href="https://miaoyan.app/">妙言</a>，簡單輕巧是它的特色。</em></li>
</ul>
</li>
</ul>
<h3 id="語言執行環境-runtimes">語言執行環境 (Runtimes)</h3>
<p>以 Ruby 為例，我使用 <code>rbenv</code> 來管理版本：</p>
<pre><code class="language-shell">brew install rbenv ruby-build
</code></pre>
<p>接著安裝常用的 Ruby 版本並設為全域預設：</p>
<pre><code class="language-shell">rbenv install 3.2.2 # 請依當下最新穩定版調整
rbenv rehash
rbenv global 3.2.2
</code></pre>
<p>最後記得在 <code>~/.zshrc</code> 加入初始化設定：</p>
<pre><code class="language-shell">eval &quot;$(rbenv init - zsh)&quot;
</code></pre>
<p><em>(如果安裝過程遇到 permission denied，可嘗試 <code>sudo chown -R &quot;$(whoami)&quot;:admin /usr/local/var</code> 修復)</em></p>
<hr>
<h2 id="03-開發輔助工具-development-tools">03. 開發輔助工具 (Development Tools)</h2>
<h3 id="git-gui-客戶端">Git GUI 客戶端</h3>
<p>Git 指令雖然強大，但在檢視複雜的線圖或做部分 commit 時，GUI 工具還是比較直覺。</p>
<ul>
<li><strong><a href="https://git-fork.com/">Fork</a></strong>：目前的主力，介面非常友善且操作流暢。</li>
<li><strong>其他選擇</strong>：
<ul>
<li><a href="https://www.sourcetreeapp.com/">SourceTree</a></li>
<li><a href="https://www.git-tower.com/mac/">Tower</a></li>
<li><a href="http://www.syntevo.com/smartgit/">SmartGit</a></li>
<li><a href="https://www.gitkraken.com/git-client">GitKraken</a></li>
</ul>
</li>
</ul>
<h3 id="api-與網路除錯">API 與網路除錯</h3>
<ul>
<li><strong><a href="https://www.usebruno.com/">Bruno</a></strong>：管理與呼叫 API 的工具。</li>
<li><strong><a href="https://kapeli.com/dash">Dash</a></strong>：強大的離線 API 文件瀏覽器與程式碼片段管理工具。</li>
<li><strong><a href="https://proxyman.com/">Proxyman</a></strong>：介面漂亮的抓包工具，用來檢查與修改 HTTP/HTTPS 請求。</li>
</ul>
<h3 id="ui-檢測">UI 檢測</h3>
<ul>
<li><strong><a href="https://lookin.work/">Lookin</a></strong> (免費)：若 Xcode 內建工具不夠用時的進階選擇。</li>
<li><strong>其他付費選擇</strong>：<a href="https://revealapp.com/">Reveal</a> 或 <a href="https://sherlock.inspiredcode.io/">Sherlock</a>。</li>
</ul>
<hr>
<h2 id="04-生產力與系統增強-productivity-utilities">04. 生產力與系統增強 (Productivity &amp; Utilities)</h2>
<p>這區塊收錄了讓 Mac 更順手的小工具，依照功能分類：</p>
<h3 id="滑鼠增強">滑鼠增強</h3>
<ul>
<li><strong><a href="https://better-mouse.com/">Better Mouse</a></strong>：我不喜歡安裝肥大的驅動程式，這是更流暢、功能強大的輕量化替代品。</li>
<li><strong><a href="https://github.com/tjsky/logi-options-plus-mini">logi-options-plus-mini</a></strong>：如果你依然必須使用羅技官方驅動 (Logi Options+)，建議使用這個工具進行最小化安裝，去除不必要的臃腫功能。</li>
</ul>
<h3 id="視窗與檔案管理">視窗與檔案管理</h3>
<ul>
<li><strong>視窗管理</strong>：
<ul>
<li>用 <a href="https://itunes.apple.com/tw/app/magnet/id441258766?mt=12">Magnet</a> 或 <a href="https://github.com/rxhanson/Rectangle">Rectangle</a> (免費) 進行視窗分割。</li>
<li>用 <a href="https://alt-tab-macos.netlify.app/">AltTab</a> 或 <a href="https://bahoom.com/hyperswitch">HyperSwitch</a> 將 Windows 的視窗切換邏輯帶回 Mac。</li>
<li><em>Tip: 如果你有安裝 <a href="https://www.raycast.com/">Raycast</a>，它的 Window Management extension 也能完美處理視窗分割的需求。</em></li>
</ul>
</li>
<li><strong>Finder 增強</strong>：
<ul>
<li><a href="https://github.com/Ji4n1ng/OpenInTerminal">Open In Terminal</a>：在 Finder 快速開啟終端機。</li>
<li><a href="https://github.com/sbarex/QLMarkdown">QLMarkdown</a>：按空白鍵預覽 Markdown 文件。</li>
<li><a href="https://github.com/sbarex/SourceCodeSyntaxHighlight">Syntax Highlight</a>：按空白鍵預覽帶有高亮色彩的原始碼。</li>
<li><a href="https://www.binarynights.com/forklift/">ForkLift 3</a>：雙視窗檔案管理 + FTP 傳輸工具。</li>
<li><a href="https://apps.apple.com/tw/app/qspace/id1469774098?mt=12">QSpace</a>：功能單純的多面板 Finder。</li>
</ul>
</li>
<li><strong>解壓縮</strong>：
<ul>
<li><a href="https://theunarchiver.com/">The Unarchiver</a> 或 <a href="https://www.keka.io/">Keka</a>。</li>
<li>如果不介意 command line，也可考慮 <a href="https://rar.tw/download.html">WinRAR for Mac</a>。</li>
</ul>
</li>
</ul>
<h3 id="知識管理-pkm">知識管理 (PKM)</h3>
<ul>
<li><strong>筆記軟體</strong>：<a href="https://logseq.com/">Logseq</a> 與 <a href="https://obsidian.md/">Obsidian</a>，無論在家或公司都用它們整理思緒。</li>
<li><strong>心智圖</strong>：<a href="https://www.xmind.net/">XMind</a>，用於紀錄發散性或階層性的想法（雖然稍嫌笨重）。</li>
</ul>
<h3 id="系統優化與小工具">系統優化與小工具</h3>
<ul>
<li><strong>啟動器</strong>：
<ul>
<li><a href="https://www.raycast.com/">Raycast</a>：我的首選，自訂性高。</li>
<li><a href="https://www.alfredapp.com/">Alfred</a>：也是很棒的替代品。</li>
</ul>
</li>
<li><strong>防休眠</strong>：
<ul>
<li><a href="https://apps.apple.com/tw/app/amphetamine/id937984704?mt=12">Amphetamine</a>：長時間跑程式或下載時防止電腦休眠。</li>
<li><em>Tip: Raycast 也有 Coffee / Anti-sleep 相關的 extension 可以達到一樣的效果，不一定要裝獨立 App。</em></li>
</ul>
</li>
<li><strong>軟體更新檢查</strong>：<a href="https://github.com/mangerlahn/Latest">Latest</a> 或 <a href="https://aerolite.dev/applite">Applite</a>，檢查新版本非常方便。</li>
<li><strong>截圖工具</strong>：<a href="https://shottr.cc/">Shottr</a>。</li>
</ul>
<hr>
<h2 id="05-日常應用-daily-essentials">05. 日常應用 (Daily Essentials)</h2>
<h3 id="瀏覽器">瀏覽器</h3>
<p>我目前的需求是「多組帳號切換」、「設定可同步」與「套件多」。</p>
<ul>
<li><strong>主力使用</strong>：<a href="https://vivaldi.com/?lang=zh_TW">Vivaldi</a>。</li>
<li><strong>其他 Chromium 選擇</strong>：<a href="https://www.google.com.tw/chrome/browser/desktop/index.html">Google Chrome</a> (以前常用)、<a href="https://www.microsoft.com/zh-tw/edge">Microsoft Edge</a>、<a href="https://brave.com/zh/">Brave</a>、<a href="https://arc.net/">Arc</a>。
<ul>
<li><em>必備 Extension 記錄在 <a href="https://chiahsien.github.io/post/essential-google-chrome-extension/">這篇文章</a>。</em></li>
</ul>
</li>
<li><strong>WebKit 選擇</strong>：<a href="https://browser.kagi.com/">Orion</a> (重視隱私且支援 Chrome 套件)。</li>
<li><strong>AI 瀏覽器 (新趨勢)</strong>：
<ul>
<li><a href="https://www.diabrowser.com">Dia</a></li>
<li><a href="https://chatgpt.com/zh-Hant/atlas/">ChatGPT Atlas</a></li>
<li><a href="https://www.perplexity.ai/comet">Perplexity Comet</a></li>
</ul>
</li>
<li><strong>Firefox</strong>：<a href="https://www.mozilla.org/zh-TW/firefox/new/">Firefox</a> (因帳號切換功能不符需求而未採用)。</li>
</ul>
<h3 id="通訊軟體">通訊軟體</h3>
<p>為了避免開一大堆視窗，我通常使用整合型工具：</p>
<ul>
<li><strong>整合工具</strong>：<a href="https://ferdium.org/">Ferdium</a> (主力)，其他類似選擇還有 <a href="https://meetfranz.com/">Franz</a> 與 <a href="https://getstation.com/">Station</a>。</li>
<li><strong>常用服務</strong>：<a href="https://apps.apple.com/tw/app/line/id539883307?mt=12">LINE</a>、<a href="https://slack.com/downloads/osx">Slack</a>、<a href="https://telegram.org/">Telegram</a>、<a href="https://www.messenger.com/">Facebook Messenger</a>。</li>
</ul>
<hr>
<h2 id="06-線上工具-online-tools">06. 線上工具 (Online Tools)</h2>
<p>偶爾才用一次的需求，可以使用一些免費的線上工具解決。</p>
<ul>
<li><strong><a href="https://jsoneditoronline.org/">JSON Editor Online</a></strong>：檢視與編輯 JSON 的工具。</li>
<li><strong>圖表繪製</strong>：
<ul>
<li><a href="https://www.draw.io/">draw.io</a>、<a href="https://excalidraw.com/">Excalidraw</a>、<a href="https://www.tldraw.com/">tldraw</a>、<a href="https://www.zenflowchart.com/">Zen Flowchart</a>、<a href="https://www.yworks.com/yed-live/">yEd live</a>。</li>
<li><a href="http://asciiflow.com/">ASCIIFlow Infinity</a>：輕鬆畫出 ASCII 圖，適合放在程式碼註解裡說明架構。</li>
</ul>
</li>
<li><strong>圖片工具</strong>：
<ul>
<li><a href="https://collagemaker.tools/photo/">Collage Maker</a>：圖片拼貼工具，通常送 PR 給同事 review UI 修改時，用來製作前後對照圖。</li>
</ul>
</li>
</ul>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[必備的 Google Chrome Extension]]></title>
        <id>https://chiahsien.github.io/post/essential-google-chrome-extension/</id>
        <link href="https://chiahsien.github.io/post/essential-google-chrome-extension/">
        </link>
        <updated>2025-07-26T06:04:16.000Z</updated>
        <summary type="html"><![CDATA[<p>我用的瀏覽器主要是 Chromium based，從早期愛用的 <a href="https://www.google.com.tw/chrome/">Google Chrome</a>，到現在改用 <a href="https://vivaldi.com/zh-hant/">Vivaldi</a>、<a href="https://brave.com/zh/">Brave Browser</a>、<a href="https://arc.net/">Arc</a>、<a href="https://diabrowser.com/">Dia</a>。有時還會用 WebKit based 而且支援 Chrome/Firefox 套件的 <a href="https://browser.kagi.com">Orion Browser</a>。</p>
<p>每次換工作都是一次整理工作環境的機會，我的瀏覽器也藉此重裝，雖然每次的工作都不完全一樣，但我發現有些 extension 是不管在之前還是現在的工作、不管是公司還是家裏都會安裝的。以下就是我必備的幾個 extension：</p>
]]></summary>
        <content type="html"><![CDATA[<p>我用的瀏覽器主要是 Chromium based，從早期愛用的 <a href="https://www.google.com.tw/chrome/">Google Chrome</a>，到現在改用 <a href="https://vivaldi.com/zh-hant/">Vivaldi</a>、<a href="https://brave.com/zh/">Brave Browser</a>、<a href="https://arc.net/">Arc</a>、<a href="https://diabrowser.com/">Dia</a>。有時還會用 WebKit based 而且支援 Chrome/Firefox 套件的 <a href="https://browser.kagi.com">Orion Browser</a>。</p>
<p>每次換工作都是一次整理工作環境的機會，我的瀏覽器也藉此重裝，雖然每次的工作都不完全一樣，但我發現有些 extension 是不管在之前還是現在的工作、不管是公司還是家裏都會安裝的。以下就是我必備的幾個 extension：</p>
<!-- more -->
<ul>
<li>
<p><a href="https://chromewebstore.google.com/detail/adguard-adblocker/bgnkhhnnamicmpeenaelnjfhikgbkllg">AdGuard 廣告封鎖器</a><br>
這一定是我第一個安裝的套件，實在是因為現在的網頁充斥著大量的廣告，不裝擋廣告套件根本無法好好瀏覽網頁了。</p>
</li>
<li>
<p><a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb">Bitwarden</a><br>
我使用 Bitwarden 來儲存帳號密碼，透過這個套件就可以在瀏覽器快速存取密碼登入網站了。</p>
</li>
<li>
<p><a href="https://chrome.google.com/webstore/detail/tab-group/gjgjkhbmehogehkdnoooeihkipifimme">Tab Group</a><br>
很容易為了查資料不知不覺就開啟幾十個分頁，由於只是臨時查詢的頁面，存為書籤感覺不太必要，一直開著又很浪費資源。這時我就會用這個套件把分頁存成不同的 group 方便日後參考，不需要的時候就把整個 group 砍掉。<a href="https://arc.net/">Arc Browser</a> 跟 <a href="https://vivaldi.com/zh-hant/">Vivaldi</a> 瀏覽器在分頁管理方面做得非常好，完全不需要這個套件。</p>
</li>
<li>
<p><a href="https://chromewebstore.google.com/detail/tab-copy/micdllihgoppmejpecmkilggmaagfdmb">Tab Copy</a><br>
這個套件可以讓你自訂格式，複製目前開啟的分頁資訊，對於時常需要寫信寫文件傳訊息的工程師來說很方便。</p>
</li>
<li>
<p><a href="https://chrome.google.com/webstore/detail/autopagerize/igiofjhpmpihnifddepnpngfjhkfenbp">AutoPagerize</a><br>
顧名思義就是自動載入下一頁的套件，幫忙我們節省寶貴的時間。</p>
</li>
<li>
<p><a href="https://immersivetranslate.com/zh-TW/">沉浸式翻譯</a><br>
方便翻譯單字、片段或是全文的好工具。</p>
</li>
<li>
<p><a href="https://chrome.google.com/webstore/detail/clearly-reader-your-missi/odfonlkabodgbolnmmkdijkaeggofoop">Cleary Reader</a>、<a href="https://chrome.google.com/webstore/detail/simpread-reader-view/ijllcpnolfcooahcekpamkbidhejabll">簡悅 - SimpRead</a> 或是 <a href="https://ranhe.xyz/circle/">Circle 閱讀模式</a><br>
讓你瞬間進入沉浸式閱讀的 Chrome 擴展，提供與 Safari 類似的 read mode，以及其他更有彈性的設定。</p>
</li>
</ul>
]]></content>
    </entry>
</feed>