Swift 模組與引用 (Modules & Imports)

在大型專案開發中,了解模組 (Module) 的概念至關重要。Swift 使用模組來組織程式碼、管理存取權限並實現程式碼重用。

什麼是模組 (Module)?

模組是 Swift 程式碼分發 (Code Distribution) 的單位。

  • 一個 Framework (如 SwiftUI, UIKit, Foundation)。
  • 一個 Application (你的 App Target)。
  • 一個 Swift Package 的 Library。

每個模組都有自己的命名空間 (Namespace),這意味著不同模組可以有相同名稱的類別或函式,而不會產生衝突。

引入語法 (import)

最基本的使用方式是引入整個模組:

import SwiftUI
import CoreLocation

這會將該模組中所有 publicopen 的 API 暴露給當前檔案。

引入特定符號 (Importing Specific Symbols)

如果你只想要引入模組中的某一個特定功能,可以使用特定的關鍵字。這在大型模組中可以讓依賴關係更清晰。

語法格式:import [kind] [module].[symbol]

關鍵字用途範例
class引入類別import class UIKit.UIButton
struct引入結構import struct SwiftUI.Text
func引入函式import func Darwin.sqrt
enum引入列舉import enum Combine.Publishers
protocol引入協定import protocol Swift.Codable
var引入變數import var Swift.dump

這樣做的好處是只引入你需要的。例如,你只想要使用 Darwin 中的 sqrt 函式,而不想要其他的數學符號污染你的全域命名空間:

import func Darwin.sqrt

let result = sqrt(4.0)
// let p = pow(2.0, 3.0) // 錯誤:pow 沒有被引入

處理名稱衝突 (Name Conflicts)

當兩個不同的模組擁有相同名稱的型別時,就會發生衝突。

例如,假設你有兩個 Framework:DrawingKitGeometryKit,它們都有一個 Point 結構。

import DrawingKit
import GeometryKit

// 錯誤:編譯器不知道你要用哪一個 Point
// let p = Point(x: 0, y: 0) 

解決方案 1:使用全名 (Fully Qualified Name)

直接加上模組名稱作為前綴:

let p1 = DrawingKit.Point(x: 0, y: 0)
let p2 = GeometryKit.Point(x: 0, y: 0)

解決方案 2:使用 typealias 重新命名

如果你不想每次都打全名,可以在檔案開頭取個彆名:

import DrawingKit
import GeometryKit

typealias DrawPoint = DrawingKit.Point
typealias GeoPoint = GeometryKit.Point

let p1 = DrawPoint(x: 0, y: 0)

常見的真實案例:Image

SwiftUI 和 UIKit 都有 Image 相關的型別,雖然名稱不完全一樣 (Image vs UIImage),但有時候我們需要明確轉換:

import SwiftUI
import UIKit // 為了使用 UIImage

struct ContentView: View {
    var body: some View {
        // 使用 SwiftUI 的 Image,初始化時傳入 UIKit 的 UIImage
        SwiftUI.Image(uiImage: UIKit.UIImage(named: "icon")!)
    }
}

進階:重新匯出 (@_exported)

這是一個 Swift 的隱藏特性 (雖然尚未正式標準化,但在許多開源庫中被廣泛使用)。

當你在模組 A 中 import 模組 B 時,預設情況下,使用模組 A 的人看不見模組 B 的內容。

如果你希望使用者只要 import A,就能自動獲得 import B 的效果,可以使用 @_exported

// MyCustomKit.swift
@_exported import SwiftUI 

// 這樣一來,只要其他檔案 import MyCustomKit,
// 就會自動擁有 SwiftUI 的所有功能,不需要再寫 import SwiftUI

這常用於建立「雨傘框架 (Umbrella Framework)」,將多個小 Library 包裝成一個大 Library 方便使用。

單元測試專用 (@testable)

當我們在寫 Unit Test 時,專案結構通常分為:

  1. App Target (主程式)
  2. Test Target (測試程式) —— 這是一個獨立的模組!

根據存取控制規則,Test Target 只能存取 App Target 中 public 的成員。但我們通常想測試 internal 的邏輯。

這時需要使用 @testable import

// MyTests.swift
import XCTest
@testable import MyApp // 關鍵!

class MyTests: XCTestCase {
    func testInternalLogic() {
        // 即使 User 是 internal 的,這裡也能存取
        let user = User(name: "Test") 
        XCTAssertEqual(user.name, "Test")
    }
}

它會暫時提升模組間的權限,讓測試目標可以看見並存取原本是 internal 的內容。

總結

  • Module 是程式碼分發的單位。
  • 使用 Specific Import (如 import class) 可以避免汙染命名空間。
  • 遇到 名稱衝突 時,使用模組名稱前綴 (Module.Type) 或 typealias 解決。
  • @testable import 是 Unit Test 的必備工具。