在專案開發到一定規模後,你可能會發現某些 feature 其實相對獨立:它們有自己的流程、畫面、資源檔,甚至可以被其他專案重用。這時候,最乾淨、最有彈性的做法,就是把它抽成 Swift Package

以我最近在做的功能為例,它是一個完整的獨立模組 - 有多個頁面、支援多國語言、使用圖片與動畫資源。為了避免日後整合時出現命名衝突、相依過重或編譯過慢的問題,我選擇把它獨立成一個 Swift Package。在將 feature 抽出到 Package 的過程我也踩到了一些坑,趁著這個機會記錄下來,以免日後忘記。

為什麼要把 Feature 打包成 Swift Package?

建立專屬的 Swift Package 有幾個明顯的好處:

  • 可獨立開發與測試:模組化後不必依賴主專案,可單獨編譯與驗證。
  • ⚙️ 降低相依與衝突:減少命名重複、依賴鏈過長等問題。
  • 🚀 加快編譯速度:主專案不需每次都重新編譯整個功能。
  • 🔄 方便整合與重用:未來可以直接被其他 app 或團隊使用。

問題一:如何支援多國語言?

若要在 Package 內使用多國語言,有兩件事情一定要搞清楚:

  1. 檔案結構要正確放置。
  2. 讀取時要明確指定 .module

正確的資料夾結構

根據 Apple 官方文件,多國語言檔(.lproj)必須直接放在 Resources 資料夾底下,不能再有子目錄。正確的結構如下:

MyPackage/
├─ Sources/
│  └─ MyLibrary/
│     └─ Resources/
│        ├─ en.lproj/
│        │  └─ Localizable.strings
│        ├─ zh-Hant.lproj/
│        │  └─ Localizable.strings
│        └─ Strings.dict

這樣做可以確保 .lproj 檔會被正確地讀取與載入。若放錯層級,Xcode 雖然不會報錯,但翻譯字串就是不會出現。

正確的呼叫方式

在 Package 內呼叫多國語言字串時,別忘了指定 bundle: .module。否則 Swift 會自動去主專案尋找對應字串,導致載不到 Package 內的翻譯。

extension String {
    var localized: String {
        NSLocalizedString(
            self,
            tableName: "Localizable",
            bundle: .module,    // 關鍵:指定為 package 的 bundle
            value: self,
            comment: ""
        )
    }
}

使用時只要 "Some.Localized.String.Key".localized 就能正確取回包內的翻譯字串。這樣做讓 package 在任何專案中都能獨立運作,無需依賴主專案的語言設定。


問題二:如何使用圖片與動畫資源?

圖片與多國語言的處理方式類似,同樣要注意資料夾結構載入方式

圖片資源結構

官方建議將圖片檔(.xcassets)放在 Resources 目錄底下。若你使用像 Lottie 這類第三方函式庫,也可以把相關 JSON 資源放在同個目錄。

MyPackage/
├─ Sources/
│  └─ MyLibrary/
│     └─ Resources/
│        ├─ Images.xcassets/
│        │  ├─ Avatar.imageset/
│        │  ├─ Error.imageset/
│        │  └─ ...
│        └─ Lottie/
│           ├─ greeting.json
│           └─ ...

呼叫圖片的方式

有兩種常見方式可以載入圖片:

  1. UIImage(resource: .xxx)
  2. UIImage(named: "xxx", in: .module, with: nil)

而如果你使用 Lottie 動畫,則要記得加上 bundle

LottieAnimationView(name: "greeting", bundle: .module)

這樣才能確保動畫資源來自 package,而不是主專案。


問題三:Package.swift 要怎麼設定?

最後一步,就是在 Package.swift 中設定好本地化與資源處理。這一步如果漏掉,前面做的一切可能都不會生效。

let package = Package(
    name: "MyLibrary",
    defaultLocalization: "en",  // 一定要設定預設語言
    platforms: [
        .iOS(.v16)
    ],
    products: [
        .library(
            name: "MyLibrary",
            targets: ["MyLibrary"]
        ),
    ],
    dependencies: [
        .package(url: "https://github.com/airbnb/lottie-spm.git", from: "4.5.2"),
    ],
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: [
                .product(name: "Lottie", package: "lottie-spm"),
            ],
            resources: [
                .process("Resources")  // 使用 .process 讓 Xcode 處理資源檔
            ]
        ),
    ]
)

這裡的兩個重點是:

  • defaultLocalization:沒有這行,多國語言會無法自動套用。
  • .process("Resources"):讓 Xcode 知道要把資源包含進 target。

結語:讓 Feature 真正成為可重用的模組

當一個功能變得越來越複雜時,把它獨立成 Swift Package 不只是整潔問題,更是 架構與維護性的升級

模組化能讓你:

  • 更容易進行單元測試;
  • 快速重用或移植到新專案;
  • 明確定義每個功能的邊界與責任。

只要依照上面的步驟設定語言與資源,就能建立一個乾淨、可移植、可重用的 Feature Package。當你下一次打開 Xcode 時,或許就會開始想:「這個功能,其實也該是一個獨立的 package 吧?」