Article / 2026/05/29

Swift Bundle 完全指南

🚀 Swift Bundle 完全指南

目录


概述

Bundle 是 Foundation 框架中的核心类,表示代码和资源的目录结构,用于定位和加载应用程序的资源文件、可执行代码和本地化字符串。

Bundle 的作用

  • 📦 管理应用程序的资源文件(图片、音频、视频、JSON 等)
  • 🌍 处理本地化资源和多语言支持
  • ℹ️ 提供应用程序和框架的元数据(版本号、标识符等)
  • 🔧 加载动态库、框架和插件
  • 🎯 在运行时访问编译后的资源

获取 Bundle 实例

1. main - 主 Bundle ⭐ 最常用

// 获取应用程序的主 bundle
let mainBundle = Bundle.main

// 基本信息
print("Bundle ID: \(mainBundle.bundleIdentifier ?? "Unknown")")
print("App Path: \(mainBundle.bundlePath)")

参数aClassName: String - 类的完全限定名称
返回AnyClass? - 找到的类,失败返回 nil


3. executableArchitectures - 可执行文件架构

// 获取 bundle 支持的 CPU 架构
if let architectures = Bundle.main.executableArchitectures {
    for arch in architectures {
        if let archValue = arch as? Int {
            switch archValue {
            case NSBundleExecutableArchitectureARM64:
                print("Supports ARM64")
            case NSBundleExecutableArchitectureX86_64:
                print("Supports x86_64")
            default:
                print("Architecture: \(archValue)")
            }
        }
    }
}

类型[NSNumber]?
说明:bundle 支持的处理器架构列表


实用方法

1. url(forResource:withExtension:subdirectory:localization:) - 高级资源定位

// 同时指定子目录和本地化
if let imageURL = Bundle.main.url(
    forResource: "welcome_banner",
    withExtension: "png",
    subdirectory: "Images/Onboarding",
    localization: "zh-Hans"
) {
    let image = UIImage(contentsOf: imageURL)
}

2. 检查资源是否存在

extension Bundle {
    func resourceExists(name: String, type: String) -> Bool {
        return path(forResource: name, ofType: type) != nil
    }
    
    func urlExists(forResource name: String, withExtension ext: String) -> Bool {
        return url(forResource: name, withExtension: ext) != nil
    }
}

// 使用
if Bundle.main.resourceExists(name: "config", type: "json") {
    print("Config file exists")
}

3. 获取所有特定类型的资源

extension Bundle {
    func allResources(ofType type: String, inDirectory directory: String? = nil) -> [URL] {
        return urls(forResourcesWithExtension: type, subdirectory: directory) ?? []
    }
}

// 使用
let allImages = Bundle.main.allResources(ofType: "png", inDirectory: "Images")
print("Found \(allImages.count) images")

常见使用场景

场景 1:加载 JSON 配置文件

struct AppConfig: Codable {
    let apiBaseURL: String
    let apiKey: String
    let timeout: Int
}

func loadConfig() -> AppConfig? {
    guard let url = Bundle.main.url(forResource: "config", withExtension: "json") else {
        print("Config file not found")
        return nil
    }
    
    do {
        let data = try Data(contentsOf: url)
        let decoder = JSONDecoder()
        let config = try decoder.decode(AppConfig.self, from: data)
        return config
    } catch {
        print("Failed to load config: \(error)")
        return nil
    }
}

// 使用
if let config = loadConfig() {
    print("API URL: \(config.apiBaseURL)")
}

场景 2:批量加载图片资源

class ImageManager {
    static let shared = ImageManager()
    private var imageCache: [String: UIImage] = [:]
    
    func preloadImages(inDirectory directory: String) {
        let imageURLs = Bundle.main.urls(
            forResourcesWithExtension: "png",
            subdirectory: directory
        ) ?? []
        
        for url in imageURLs {
            if let image = UIImage(contentsOfFile: url.path) {
                let imageName = url.deletingPathExtension().lastPathComponent
                imageCache[imageName] = image
                print("Preloaded: \(imageName)")
            }
        }
    }
    
    func image(named name: String) -> UIImage? {
        return imageCache[name]
    }
}

// 使用
ImageManager.shared.preloadImages(inDirectory: "Onboarding")
let image = ImageManager.shared.image(named: "welcome")

场景 3:多语言支持

// Localizable.strings (en)
// "welcome_title" = "Welcome";
// "welcome_message" = "Welcome to %@!";

// Localizable.strings (zh-Hans)
// "welcome_title" = "欢迎";
// "welcome_message" = "欢迎来到%@!";

class LocalizationManager {
    static func localizedString(_ key: String, comment: String = "") -> String {
        return NSLocalizedString(key, comment: comment)
    }
    
    static func localizedString(_ key: String, _ args: CVarArg...) -> String {
        let format = NSLocalizedString(key, comment: "")
        return String(format: format, arguments: args)
    }
}

// 使用
let title = LocalizationManager.localizedString("welcome_title")
let message = LocalizationManager.localizedString("welcome_message", "MyApp")

// 输出(中文环境):
// title = "欢迎"
// message = "欢迎来到MyApp!"

场景 4:加载音频资源

import AVFoundation

class AudioManager {
    private var audioPlayers: [String: AVAudioPlayer] = [:]
    
    func loadSound(named name: String, type: String) -> Bool {
        guard let url = Bundle.main.url(forResource: name, withExtension: type) else {
            print("Sound file not found: \(name).\(type)")
            return false
        }
        
        do {
            let player = try AVAudioPlayer(contentsOf: url)
            player.prepareToPlay()
            audioPlayers[name] = player
            return true
        } catch {
            print("Failed to load sound: \(error)")
            return false
        }
    }
    
    func playSound(named name: String) {
        audioPlayers[name]?.play()
    }
}

// 使用
let audioManager = AudioManager()
audioManager.loadSound(named: "click", type: "mp3")
audioManager.playSound(named: "click")

场景 5:读取文本文件

extension Bundle {
    func loadTextFile(named name: String, type: String) -> String? {
        guard let path = self.path(forResource: name, ofType: type) else {
            return nil
        }
        
        return try? String(contentsOfFile: path, encoding: .utf8)
    }
    
    func loadTextFileURL(named name: String, withExtension ext: String) -> String? {
        guard let url = self.url(forResource: name, withExtension: ext) else {
            return nil
        }
        
        return try? String(contentsOf: url, encoding: .utf8)
    }
}

// 使用
if let licenseText = Bundle.main.loadTextFile(named: "LICENSE", type: "txt") {
    print("License:\n\(licenseText)")
}

if let readmeContent = Bundle.main.loadTextFileURL(named: "README", withExtension: "md") {
    print(readmeContent)
}

场景 6:在框架中使用 Bundle

// MyFramework.swift
public class MyFramework {
    // 获取框架的 bundle
    static var bundle: Bundle {
        return Bundle(for: MyFramework.self)
    }
    
    // 加载框架资源
    public static func loadFrameworkImage(named name: String) -> UIImage? {
        return UIImage(named: name, in: bundle, compatibleWith: nil)
    }
    
    // 加载框架中的本地化字符串
    public static func localizedString(_ key: String) -> String {
        return NSLocalizedString(key, bundle: bundle, comment: "")
    }
    
    // 加载框架中的 XIB
    public static func loadView<T: UIView>(_ viewType: T.Type) -> T? {
        let nibName = String(describing: viewType)
        let nib = UINib(nibName: nibName, bundle: bundle)
        let views = nib.instantiate(withOwner: nil, options: nil)
        return views.first as? T
    }
}

// 使用框架
let icon = MyFramework.loadFrameworkImage(named: "framework_icon")
let title = MyFramework.localizedString("framework_title")
let customView = MyFramework.loadView(CustomView.self)

场景 7:动态插件系统

protocol PluginProtocol {
    var name: String { get }
    var version: String { get }
    func execute()
}

class PluginManager {
    private var loadedPlugins: [String: PluginProtocol] = [:]
    
    func loadPlugin(at path: String) -> Bool {
        guard let bundle = Bundle(path: path) else {
            print("Failed to load bundle at: \(path)")
            return false
        }
        
        do {
            try bundle.loadAndReturnError()
            
            guard let pluginClass = bundle.principalClass as? PluginProtocol.Type else {
                print("Principal class does not conform to PluginProtocol")
                return false
            }
            
            let plugin = pluginClass.init()
            loadedPlugins[plugin.name] = plugin
            print("Loaded plugin: \(plugin.name) v\(plugin.version)")
            return true
            
        } catch {
            print("Failed to load plugin: \(error)")
            return false
        }
    }
    
    func executePlugin(named name: String) {
        loadedPlugins[name]?.execute()
    }
    
    func listPlugins() -> [String] {
        return Array(loadedPlugins.keys)
    }
}

// 使用
let pluginManager = PluginManager()
pluginManager.loadPlugin(at: "/path/to/MyPlugin.bundle")
pluginManager.executePlugin(named: "MyPlugin")

场景 8:版本检查和更新提示

class VersionManager {
    static let shared = VersionManager()
    
    var currentVersion: String {
        return Bundle.main.appVersion ?? "1.0.0"
    }
    
    var buildNumber: String {
        return Bundle.main.buildNumber ?? "1"
    }
    
    func checkForUpdate(completion: @escaping (Bool, String?) -> Void) {
        // 从服务器获取最新版本
        fetchLatestVersion { latestVersion in
            guard let latest = latestVersion else {
                completion(false, nil)
                return
            }
            
            let needsUpdate = self.compareVersions(
                current: self.currentVersion,
                latest: latest
            )
            
            completion(needsUpdate, latest)
        }
    }
    
    private func compareVersions(current: String, latest: String) -> Bool {
        return current.compare(latest, options: .numeric) == .orderedAscending
    }
    
    private func fetchLatestVersion(completion: @escaping (String?) -> Void) {
        // 实际实现从服务器获取版本号
        // 这里只是示例
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            completion("1.1.0")
        }
    }
}

// 使用
VersionManager.shared.checkForUpdate { needsUpdate, latestVersion in
    if needsUpdate {
        print("New version available: \(latestVersion ?? "Unknown")")
        // 显示更新提示
    }
}

场景 9:环境配置管理

enum Environment {
    case development
    case staging
    case production
    
    static var current: Environment {
        #if DEBUG
        return .development
        #else
        if let env = Bundle.main.infoDictionary?["Environment"] as? String {
            switch env {
            case "Staging": return .staging
            case "Production": return .production
            default: return .development
            }
        }
        return .production
        #endif
    }
    
    var apiBaseURL: String {
        switch self {
        case .development:
            return "https://dev-api.example.com"
        case .staging:
            return "https://staging-api.example.com"
        case .production:
            return "https://api.example.com"
        }
    }
    
    var isDebugEnabled: Bool {
        return self == .development
    }
}

// 使用
print("Current environment: \(Environment.current)")
print("API URL: \(Environment.current.apiBaseURL)")

场景 10:资源完整性检查

class ResourceValidator {
    static func validateRequiredResources() -> [String] {
        var missingResources: [String] = []
        
        let requiredResources: [(String, String)] = [
            ("config", "json"),
            ("AppIcon", "png"),
            ("Localizable", "strings")
        ]
        
        for (name, type) in requiredResources {
            if Bundle.main.path(forResource: name, ofType: type) == nil {
                missingResources.append("\(name).\(type)")
            }
        }
        
        return missingResources
    }
    
    static func printResourceReport() {
        let missing = validateRequiredResources()
        
        if missing.isEmpty {
            print("✅ All required resources are present")
        } else {
            print("❌ Missing resources:")
            missing.forEach { print("   - \($0)") }
        }
        
        // 打印所有可用资源统计
        let images = Bundle.main.paths(forResourcesOfType: "png", inDirectory: nil)
        let jsons = Bundle.main.paths(forResourcesOfType: "json", inDirectory: nil)
        let sounds = Bundle.main.paths(forResourcesOfType: "mp3", inDirectory: nil)
        
        print("\n📊 Resource Statistics:")
        print("   Images (PNG): \(images.count)")
        print("   JSON files: \(jsons.count)")
        print("   Sound files (MP3): \(sounds.count)")
    }
}

// 使用(在应用启动时)
#if DEBUG
ResourceValidator.printResourceReport()
#endif

最佳实践

1. 使用扩展封装常用功能

extension Bundle {
    // 简化版本信息访问
    var appVersion: String? {
        return infoDictionary?["CFBundleShortVersionString"] as? String
    }
    
    var buildNumber: String? {
        return infoDictionary?["CFBundleVersion"] as? String
    }
    
    // 简化资源加载
    func loadJSON<T: Decodable>(_ type: T.Type, from file: String) -> T? {
        guard let url = url(forResource: file, withExtension: "json"),
              let data = try? Data(contentsOf: url) else {
            return nil
        }
        return try? JSONDecoder().decode(type, from: data)
    }
    
    // 检查资源存在性
    func hasResource(named name: String, ofType type: String) -> Bool {
        return path(forResource: name, ofType: type) != nil
    }
}

2. 统一资源管理

struct Resources {
    // 配置文件
    struct Config {
        static let main = "config"
        static let server = "server_config"
    }
    
    // 图片目录
    struct Images {
        static let icons = "Images/Icons"
        static let backgrounds = "Images/Backgrounds"
    }
    
    // 音频文件
    struct Audio {
        static let click = "click"
        static let success = "success"
        static let error = "error"
    }
}

// 使用
if let configURL = Bundle.main.url(forResource: Resources.Config.main, withExtension: "json") {
    // 加载配置
}

3. 类型安全的资源访问

enum Asset {
    case image(String)
    case json(String)
    case audio(String, String) // name, extension
    
    var url: URL? {
        switch self {
        case .image(let name):
            return Bundle.main.url(forResource: name, withExtension: "png")
        case .json(let name):
            return Bundle.main.url(forResource: name, withExtension: "json")
        case .audio(let name, let ext):
            return Bundle.main.url(forResource: name, withExtension: ext)
        }
    }
    
    func load() -> Data? {
        guard let url = url else { return nil }
        return try? Data(contentsOf: url)
    }
}

// 使用
let configData = Asset.json("config").load()
let imageURL = Asset.image("logo").url

4. 错误处理

enum BundleError: Error {
    case resourceNotFound(String)
    case invalidData
    case decodingFailed(Error)
}

extension Bundle {
    func loadResource(named name: String, withExtension ext: String) throws -> Data {
        guard let url = url(forResource: name, withExtension: ext) else {
            throw BundleError.resourceNotFound("\(name).\(ext)")
        }
        
        do {
            return try Data(contentsOf: url)
        } catch {
            throw BundleError.invalidData
        }
    }
    
    func decodeJSON<T: Decodable>(_ type: T.Type, from file: String) throws -> T {
        let data = try loadResource(named: file, withExtension: "json")
        
        do {
            return try JSONDecoder().decode(type, from: data)
        } catch {
            throw BundleError.decodingFailed(error)
        }
    }
}

// 使用
do {
    let config = try Bundle.main.decodeJSON(AppConfig.self, from: "config")
    print("Config loaded: \(config)")
} catch BundleError.resourceNotFound(let file) {
    print("Resource not found: \(file)")
} catch BundleError.decodingFailed(let error) {
    print("Failed to decode: \(error)")
} catch {
    print("Unexpected error: \(error)")
}

5. 缓存管理

class ResourceCache {
    static let shared = ResourceCache()
    
    private var cache: [String: Any] = [:]
    private let queue = DispatchQueue(label: "com.app.resource.cache")
    
    func loadImage(named name: String) -> UIImage? {
        let key = "image_\(name)"
        
        return queue.sync {
            // 检查缓存
            if let cached = cache[key] as? UIImage {
                return cached
            }
            
            // 从 bundle 加载
            guard let url = Bundle.main.url(forResource: name, withExtension: "png"),
                  let image = UIImage(contentsOfFile: url.path) else {
                return nil
            }
            
            // 存入缓存
            cache[key] = image
            return image
        }
    }
    
    func clearCache() {
        queue.sync {
            cache.removeAll()
        }
    }
}

// 使用
let image = ResourceCache.shared.loadImage(named: "logo")

6. 调试辅助

#if DEBUG
extension Bundle {
    func printAllResources() {
        print("=== Bundle Resources ===")
        print("Bundle Path: \(bundlePath)")
        
        let types = ["png", "jpg", "json", "mp3", "txt", "plist"]
        for type in types {
            let resources = paths(forResourcesOfType: type, inDirectory: nil)
            if !resources.isEmpty {
                print("\n\(type.uppercased()) files (\(resources.count)):")
                resources.forEach { path in
                    let name = URL(fileURLWithPath: path).lastPathComponent
                    print("  - \(name)")
                }
            }
        }
    }
}

// 在开发模式下打印所有资源
Bundle.main.printAllResources()
#endif

7. 单元测试支持

class BundleTests: XCTestCase {
    func testRequiredResourcesExist() {
        let requiredResources: [(String, String)] = [
            ("config", "json"),
            ("Localizable", "strings")
        ]
        
        for (name, type) in requiredResources {
            XCTAssertNotNil(
                Bundle.main.path(forResource: name, ofType: type),
                "\(name).\(type) not found in bundle"
            )
        }
    }
    
    func testBundleInfo() {
        XCTAssertNotNil(Bundle.main.bundleIdentifier, "Bundle ID is nil")
        XCTAssertNotNil(Bundle.main.appVersion, "App version is nil")
        XCTAssertNotNil(Bundle.main.buildNumber, "Build number is nil")
    }
    
    func testConfigFileIsValid() throws {
        let config = try Bundle.main.decodeJSON(AppConfig.self, from: "config")
        XCTAssertFalse(config.apiBaseURL.isEmpty, "API URL is empty")
        XCTAssertGreaterThan(config.timeout, 0, "Timeout must be positive")
    }
}

性能优化建议

1. 延迟加载资源

class LazyResourceLoader {
    private lazy var config: AppConfig? = {
        return try? Bundle.main.decodeJSON(AppConfig.self, from: "config")
    }()
    
    private lazy var largeImage: UIImage? = {
        guard let url = Bundle.main.url(forResource: "large_image", withExtension: "png") else {
            return nil
        }
        return UIImage(contentsOfFile: url.path)
    }()
}

2. 异步加载大文件

extension Bundle {
    func loadLargeFile(named name: String, 
                      withExtension ext: String,
                      completion: @escaping (Data?) -> Void) {
        DispatchQueue.global(qos: .utility).async {
            guard let url = self.url(forResource: name, withExtension: ext) else {
                DispatchQueue.main.async {
                    completion(nil)
                }
                return
            }
            
            let data = try? Data(contentsOf: url)
            DispatchQueue.main.async {
                completion(data)
            }
        }
    }
}

// 使用
Bundle.main.loadLargeFile(named: "video", withExtension: "mp4") { data in
    guard let videoData = data else {
        print("Failed to load video")
        return
    }
    print("Video loaded: \(videoData.count) bytes")
}

3. 批量预加载

class ResourcePreloader {
    static func preloadEssentialResources(completion: @escaping () -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            // 预加载配置
            _ = try? Bundle.main.decodeJSON(AppConfig.self, from: "config")
            
            // 预加载关键图片
            let essentialImages = ["logo", "placeholder", "icon"]
            for imageName in essentialImages {
                _ = Bundle.main.url(forResource: imageName, withExtension: "png")
            }
            
            DispatchQueue.main.async {
                completion()
            }
        }
    }
}

// 在应用启动时调用
ResourcePreloader.preloadEssentialResources {
    print("Essential resources preloaded")
}

常见问题和解决方案

问题 1:资源文件找不到

// ❌ 错误:文件名包含扩展名
let path = Bundle.main.path(forResource: "config.json", ofType: "json") // nil

// ✅ 正确方法 1:分开指定
let path = Bundle.main.path(forResource: "config", ofType: "json")

// ✅ 正确方法 2:不指定扩展名
let path = Bundle.main.path(forResource: "config.json", ofType: nil)

问题 2:框架资源访问失败

// ❌ 错误:使用主 bundle
let image = UIImage(named: "framework_icon") // nil

// ✅ 正确:使用框架的 bundle
let bundle = Bundle(for: MyFrameworkClass.self)
let image = UIImage(named: "framework_icon", in: bundle, compatibleWith: nil)

问题 3:本地化字符串不生效

// 确保 Localizable.strings 文件在正确的 .lproj 目录下
// 检查文件是否添加到 target
// 验证本地化设置

// 调试本地化
print("Preferred languages: \(Locale.preferredLanguages)")
print("Bundle localizations: \(Bundle.main.localizations)")
print("Current localization: \(Bundle.main.preferredLocalizations.first ?? "none")")

总结

最常用的 API:

  1. Bundle.main - 获取主 bundle
  2. path(forResource:ofType:) - 获取资源路径
  3. url(forResource:withExtension:) - 获取资源 URL
  4. NSLocalizedString - 本地化字符串
  5. infoDictionary - 访问 Info.plist

记住要点:

  • 🎯 优先使用 URL API 而不是路径字符串
  • 🌍 正确处理本地化资源
  • 📦 在框架中使用正确的 bundle
  • ⚡ 对大资源使用延迟加载
  • 🔍 使用类型安全的资源访问模式
  • ✅ 添加资源完整性检查

附录:快速参考

// 获取 Bundle
Bundle.main                          // 主 bundle
Bundle(for: MyClass.self)           // 类所在 bundle
Bundle(path: "/path/to/bundle")     // 指定路径

// 资源加载
.path(forResource:ofType:)          // 获取路径
.url(forResource:withExtension:)    // 获取 URL
.paths(forResourcesOfType:inDirectory:)  // 批量路径
.urls(forResourcesWithExtension:subdirectory:)  // 批量 URL

// 信息访问
.bundleIdentifier                    // Bundle ID
.infoDictionary                      // Info.plist
.appVersion                          // 版本号(扩展)
.buildNumber                         // 构建号(扩展)

// 本地化
NSLocalizedString(_:comment:)        // 本地化字符串
.localizedString(forKey:value:table:)  // 完整方法
.preferredLocalizations              // 首选语言
.localizations                       // 所有语言

// 路径属性
.bundleURL / .bundlePath            // Bundle 路径
.resourceURL / .resourcePath        // 资源路径
.executableURL / .executablePath    // 可执行文件路径

文档版本:1.0
最后更新:2025-10-10
适用于:Swift 5.0+, iOS 13.0+, macOS 10.15+


本文档涵盖了 Swift Bundle 的所有主要 API 和常见使用场景。建议根据实际项目需求选择合适的方法。类型Bundle(类属性)
返回:应用程序的主 bundle
使用场景

  • 读取应用资源文件(图片、音频、配置文件)
  • 获取应用版本号和构建号
  • 加载本地化字符串
  • 读取 Info.plist 信息

示例

// 获取应用版本
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
    print("App Version: \(version)")
}

// 加载资源文件
if let path = Bundle.main.path(forResource: "config", ofType: "json") {
    let data = try? Data(contentsOf: URL(fileURLWithPath: path))
}

2. init(for:) - 为指定类获取 Bundle

// 获取包含指定类的 bundle
let bundle = Bundle(for: MyViewController.self)

// 从该 bundle 加载资源
if let imagePath = bundle.path(forResource: "logo", ofType: "png") {
    let image = UIImage(contentsOfFile: imagePath)
}

参数aClass: AnyClass - 任意类类型
返回:包含该类的 bundle,如果找不到则返回主 bundle
使用场景

  • 在框架或库中使用,确保从正确的 bundle 加载资源
  • 创建可复用的 UI 组件库
  • 单元测试中加载测试资源

示例

// 在自定义框架中使用
class MyFrameworkClass {
    static func loadIcon() -> UIImage? {
        let bundle = Bundle(for: MyFrameworkClass.self)
        return UIImage(named: "icon", in: bundle, compatibleWith: nil)
    }
}

3. init?(path:) - 通过路径创建 Bundle

// 从指定路径创建 bundle
if let bundle = Bundle(path: "/Applications/MyApp.app/Contents/PlugIns/MyPlugin.bundle") {
    print("Plugin bundle loaded")
    // 从插件加载资源
    bundle.loadNibNamed("PluginView", owner: self, options: nil)
}

参数path: String - bundle 的文件系统路径
返回Bundle? - 成功返回 bundle,失败返回 nil
使用场景

  • 加载插件系统的 bundle
  • 动态加载外部框架
  • 访问非标准位置的 bundle

4. init?(url:) - 通过 URL 创建 Bundle

// 使用 URL 创建 bundle
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let pluginURL = documentsURL.appendingPathComponent("MyPlugin.bundle")

if let bundle = Bundle(url: pluginURL) {
    print("Bundle created from URL: \(bundle.bundleURL)")
}

参数url: URL - bundle 的 URL
返回Bundle? - 成功返回 bundle,失败返回 nil
使用场景

  • 使用现代 URL API 加载 bundle
  • 与文件管理器 API 配合使用

5. allBundles - 所有已加载的 Bundle

// 获取应用程序中所有已加载的 bundles
let allBundles = Bundle.allBundles

for bundle in allBundles {
    print("Bundle: \(bundle.bundleIdentifier ?? "Unknown")")
    print("Path: \(bundle.bundlePath)")
}

类型[Bundle](类属性)
返回:当前进程中所有已加载的 bundle 数组
使用场景

  • 调试和日志记录
  • 检查已加载的所有模块
  • 资源冲突检测

6. allFrameworks - 所有已加载的框架 Bundle

// 获取所有框架 bundles
let frameworks = Bundle.allFrameworks

for framework in frameworks {
    print("Framework: \(framework.bundleIdentifier ?? "Unknown")")
}

类型[Bundle](类属性)
返回:所有已加载的框架 bundle 数组
使用场景

  • 检查依赖的框架
  • 框架版本检测

核心属性

1. bundleURL - Bundle 的 URL

let url = Bundle.main.bundleURL
print("Bundle URL: \(url)")
// 输出:file:///var/.../YourApp.app/

类型URL
说明:bundle 目录的完整 URL
用途

  • 获取 bundle 的完整路径
  • 构建相对路径
  • 文件操作

2. bundlePath - Bundle 的路径字符串

let path = Bundle.main.bundlePath
print("Bundle Path: \(path)")
// 输出:/var/.../YourApp.app

类型String
说明:bundle 目录的文件系统路径
用途:与需要字符串路径的旧 API 配合使用


3. bundleIdentifier - Bundle 标识符

if let identifier = Bundle.main.bundleIdentifier {
    print("Bundle ID: \(identifier)")
    // 输出:com.company.appname
}

类型String?
说明:bundle 的唯一标识符(Info.plist 中的 CFBundleIdentifier)
用途

  • 识别应用或框架
  • 用户偏好设置的 key
  • App Groups 配置

4. resourceURL - 资源目录 URL

if let resourceURL = Bundle.main.resourceURL {
    print("Resources at: \(resourceURL)")
}

类型URL?
说明:包含资源文件的目录 URL
用途

  • 访问所有资源文件
  • 批量处理资源

5. resourcePath - 资源目录路径

if let resourcePath = Bundle.main.resourcePath {
    print("Resources path: \(resourcePath)")
}

类型String?
说明:资源目录的文件系统路径


6. executableURL - 可执行文件 URL

if let executableURL = Bundle.main.executableURL {
    print("Executable: \(executableURL.lastPathComponent)")
}

类型URL?
说明:bundle 的主可执行文件 URL
用途

  • 获取应用程序二进制文件位置
  • 代码签名验证

7. executablePath - 可执行文件路径

if let executablePath = Bundle.main.executablePath {
    print("Executable path: \(executablePath)")
}

类型String?


8. privateFrameworksURL / privateFrameworksPath

// 私有框架目录
if let privateFrameworksURL = Bundle.main.privateFrameworksURL {
    print("Private frameworks: \(privateFrameworksURL)")
}

类型URL? / String?
说明:bundle 内私有框架的位置


9. sharedFrameworksURL / sharedFrameworksPath

// 共享框架目录(macOS)
if let sharedURL = Bundle.main.sharedFrameworksURL {
    print("Shared frameworks: \(sharedURL)")
}

类型URL? / String?
说明:共享框架目录(主要用于 macOS)


10. sharedSupportURL / sharedSupportPath

// 共享支持文件目录(macOS)
if let sharedSupportURL = Bundle.main.sharedSupportURL {
    print("Shared support: \(sharedSupportURL)")
}

类型URL? / String?
说明:共享支持文件目录(macOS)


11. builtInPlugInsURL / builtInPlugInsPath

// 内置插件目录
if let pluginsURL = Bundle.main.builtInPlugInsURL {
    print("Built-in plugins: \(pluginsURL)")
}

类型URL? / String?
说明:bundle 内置插件目录


12. appStoreReceiptURL

// App Store 收据位置
if let receiptURL = Bundle.main.appStoreReceiptURL {
    print("Receipt at: \(receiptURL)")
    
    // 检查收据是否存在
    if FileManager.default.fileExists(atPath: receiptURL.path) {
        print("Receipt exists")
    }
}

类型URL?
说明:App Store 购买收据的 URL
用途

  • 验证应用购买
  • 收据验证

13. infoDictionary - Info.plist 字典

// 获取 Info.plist 中的所有信息
if let infoDictionary = Bundle.main.infoDictionary {
    // 应用名称
    if let appName = infoDictionary["CFBundleName"] as? String {
        print("App Name: \(appName)")
    }
    
    // 版本号
    if let version = infoDictionary["CFBundleShortVersionString"] as? String {
        print("Version: \(version)")
    }
    
    // 构建号
    if let build = infoDictionary["CFBundleVersion"] as? String {
        print("Build: \(build)")
    }
    
    // 自定义键
    if let apiKey = infoDictionary["API_KEY"] as? String {
        print("API Key: \(apiKey)")
    }
}

类型[String: Any]?
说明:Info.plist 的字典表示
常用键

  • CFBundleName - 应用名称
  • CFBundleIdentifier - Bundle ID
  • CFBundleShortVersionString - 版本号
  • CFBundleVersion - 构建号
  • CFBundleDisplayName - 显示名称
  • 自定义配置项

14. localizedInfoDictionary - 本地化的 Info.plist

// 获取当前语言的本地化信息
if let localizedInfo = Bundle.main.localizedInfoDictionary {
    if let displayName = localizedInfo["CFBundleDisplayName"] as? String {
        print("Localized Name: \(displayName)")
    }
}

类型[String: Any]?
说明:当前本地化语言的 Info.plist 信息


15. preferredLocalizations - 首选本地化

// 应用支持的首选本地化列表
let preferred = Bundle.main.preferredLocalizations
print("Preferred localizations: \(preferred)")
// 输出:["zh-Hans", "en"]

类型[String]
说明:按优先级排序的本地化语言列表


16. localizations - 所有本地化

// 所有可用的本地化
let allLocalizations = Bundle.main.localizations
print("Available localizations: \(allLocalizations)")
// 输出:["en", "zh-Hans", "zh-Hant", "ja", "ko"]

类型[String]
说明:bundle 包含的所有本地化语言


17. developmentLocalization - 开发语言

// 开发时使用的主要语言
if let devLocalization = Bundle.main.developmentLocalization {
    print("Development language: \(devLocalization)")
    // 输出:Base 或 en
}

类型String?
说明:bundle 的开发语言(通常是 Base 或 en)


路径相关 API

1. path(forResource:ofType:) - 获取资源路径 ⭐ 常用

// 基本用法
if let jsonPath = Bundle.main.path(forResource: "config", ofType: "json") {
    print("Config file at: \(jsonPath)")
    let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath))
}

// 不指定扩展名(文件名包含扩展名)
if let path = Bundle.main.path(forResource: "config.json", ofType: nil) {
    print("Found: \(path)")
}

// 文件不存在时返回 nil
if Bundle.main.path(forResource: "nonexistent", ofType: "txt") == nil {
    print("File not found")
}

参数

  • name: String? - 资源文件名(不含扩展名)
  • ext: String? - 文件扩展名

返回String? - 文件的完整路径,找不到返回 nil

使用场景

  • 加载 JSON 配置文件
  • 读取文本文件
  • 访问数据文件

2. path(forResource:ofType:inDirectory:) - 在子目录中查找

// 在指定子目录中查找资源
if let imagePath = Bundle.main.path(
    forResource: "logo",
    ofType: "png",
    inDirectory: "Images/Branding"
) {
    let image = UIImage(contentsOfFile: imagePath)
}

// 查找嵌套目录
if let soundPath = Bundle.main.path(
    forResource: "click",
    ofType: "mp3",
    inDirectory: "Sounds/UI"
) {
    // 播放音效
}

参数

  • name: String? - 资源文件名
  • ext: String? - 文件扩展名
  • subpath: String? - 子目录路径

返回String?


3. url(forResource:withExtension:) - 获取资源 URL ⭐ 推荐

// 使用 URL(现代推荐方式)
if let configURL = Bundle.main.url(forResource: "config", withExtension: "json") {
    let data = try? Data(contentsOf: configURL)
    let decoder = JSONDecoder()
    // 解析 JSON...
}

// 加载图片
if let imageURL = Bundle.main.url(forResource: "hero", withExtension: "jpg") {
    let image = UIImage(contentsOf: imageURL)
}

参数

  • name: String? - 资源文件名
  • ext: String? - 文件扩展名

返回URL? - 文件的 URL

优势:URL 比字符串路径更现代、更安全


4. url(forResource:withExtension:subdirectory:) - 在子目录中查找 URL

// 在子目录中查找
if let videoURL = Bundle.main.url(
    forResource: "intro",
    withExtension: "mp4",
    subdirectory: "Videos/Tutorials"
) {
    // 播放视频
    let player = AVPlayer(url: videoURL)
}

参数

  • name: String? - 资源文件名
  • ext: String? - 文件扩展名
  • subpath: String? - 子目录路径

5. paths(forResourcesOfType:inDirectory:) - 批量获取同类型文件

// 获取所有 JSON 文件
let jsonPaths = Bundle.main.paths(forResourcesOfType: "json", inDirectory: nil)
for path in jsonPaths {
    print("JSON file: \(URL(fileURLWithPath: path).lastPathComponent)")
}

// 获取特定目录下的所有图片
let imagePaths = Bundle.main.paths(forResourcesOfType: "png", inDirectory: "Images")
print("Found \(imagePaths.count) PNG images")

参数

  • ext: String? - 文件扩展名
  • subpath: String? - 子目录路径(nil 表示整个 bundle)

返回[String] - 所有匹配文件的路径数组

使用场景

  • 批量加载资源
  • 遍历所有配置文件
  • 资源清单生成

6. urls(forResourcesWithExtension:subdirectory:) - 批量获取 URL

// 获取所有音频文件的 URL
if let audioURLs = Bundle.main.urls(forResourcesWithExtension: "mp3", subdirectory: "Audio") {
    for url in audioURLs {
        print("Audio: \(url.lastPathComponent)")
    }
}

// 加载所有本地化字符串文件
if let stringsURLs = Bundle.main.urls(forResourcesWithExtension: "strings", subdirectory: nil) {
    print("Found \(stringsURLs.count) .strings files")
}

参数

  • ext: String? - 文件扩展名
  • subpath: String? - 子目录路径

返回[URL]? - URL 数组


7. url(forResource:withExtension:subdirectory:localization:) - 特定本地化资源

// 获取特定语言的资源
if let zhStringsURL = Bundle.main.url(
    forResource: "Localizable",
    withExtension: "strings",
    subdirectory: nil,
    localization: "zh-Hans"
) {
    print("Chinese strings: \(zhStringsURL)")
}

// 获取英文版图片
if let enImageURL = Bundle.main.url(
    forResource: "welcome",
    withExtension: "png",
    subdirectory: "Images",
    localization: "en"
) {
    let image = UIImage(contentsOf: enImageURL)
}

参数

  • name: String? - 资源名
  • ext: String? - 扩展名
  • subpath: String? - 子目录
  • localizationName: String? - 本地化名称(如 “zh-Hans”, “en”)

资源加载 API

1. loadNibNamed(_:owner:options:) - 加载 XIB 文件

// 加载 XIB 文件
if let views = Bundle.main.loadNibNamed("CustomView", owner: self, options: nil) {
    if let customView = views.first as? CustomView {
        self.view.addSubview(customView)
    }
}

// 在自定义类中加载
class MyCustomView: UIView {
    static func loadFromNib() -> MyCustomView? {
        let bundle = Bundle(for: MyCustomView.self)
        let nib = bundle.loadNibNamed("MyCustomView", owner: nil, options: nil)
        return nib?.first as? MyCustomView
    }
}

参数

  • name: String - XIB 文件名(不含 .xib 扩展名)
  • owner: Any? - File’s Owner 对象
  • options: [UINib.OptionsKey: Any]? - 加载选项

返回[Any]? - XIB 中的顶级对象数组


2. load() / loadAndReturnError(_:) - 加载 Bundle 代码

// 加载 bundle 的可执行代码(用于插件)
if let pluginBundle = Bundle(path: "/path/to/plugin.bundle") {
    do {
        try pluginBundle.loadAndReturnError()
        print("Plugin loaded successfully")
        
        // 获取插件的主类
        if let pluginClass = pluginBundle.principalClass as? PluginProtocol.Type {
            let plugin = pluginClass.init()
            plugin.execute()
        }
    } catch {
        print("Failed to load plugin: \(error)")
    }
}

说明:动态加载 bundle 的可执行代码
使用场景

  • 插件系统
  • 动态加载框架
  • 运行时扩展

3. unload() - 卸载 Bundle

// 卸载之前加载的 bundle
let success = pluginBundle.unload()
if success {
    print("Bundle unloaded")
}

返回Bool - 是否成功卸载
注意:主 bundle 不能被卸载


4. preflightAndReturnError(_:) - 预检查 Bundle

// 在加载前检查 bundle 是否有效
if let bundle = Bundle(path: "/path/to/bundle") {
    do {
        try bundle.preflightAndReturnError()
        print("Bundle is valid and can be loaded")
    } catch {
        print("Bundle preflight failed: \(error)")
    }
}

说明:检查 bundle 是否可以被加载,但不实际加载


本地化相关 API

1. localizedString(forKey:value:table:) - 获取本地化字符串 ⭐ 常用

// 基本用法
let greeting = Bundle.main.localizedString(
    forKey: "greeting",
    value: "Hello",
    table: nil
)
print(greeting) // 根据系统语言输出 "你好" 或 "Hello"

// 使用特定的 strings 文件
let errorMsg = Bundle.main.localizedString(
    forKey: "network_error",
    value: "Network Error",
    table: "Errors"  // 使用 Errors.strings
)

// 带默认值
let message = Bundle.main.localizedString(
    forKey: "unknown_key",
    value: "Default Message",  // key 不存在时使用此值
    table: nil
)

参数

  • key: String - 本地化键
  • value: String? - 默认值(key 不存在时返回)
  • tableName: String? - .strings 文件名(nil 使用 Localizable.strings)

返回String - 本地化后的字符串


2. NSLocalizedString 宏 - 简化本地化 ⭐ 最常用

// 等同于 Bundle.main.localizedString(forKey:value:table:)
let title = NSLocalizedString("app_title", comment: "应用标题")

// 带默认值
let message = NSLocalizedString("welcome_message", 
                                value: "Welcome!", 
                                comment: "欢迎消息")

// 使用特定表
let error = NSLocalizedString("error_title", 
                             tableName: "Errors", 
                             bundle: .main, 
                             comment: "错误标题")

使用场景

  • UI 文本本地化
  • 错误消息
  • 提示信息

3. 本地化字符串文件示例

// Localizable.strings (English)
"greeting" = "Hello";
"app_title" = "My App";
"welcome_message" = "Welcome to our app!";

// Localizable.strings (简体中文)
"greeting" = "你好";
"app_title" = "我的应用";
"welcome_message" = "欢迎使用我们的应用!";

// Errors.strings (English)
"network_error" = "Network connection failed";
"invalid_input" = "Invalid input";

// Errors.strings (简体中文)
"network_error" = "网络连接失败";
"invalid_input" = "输入无效";

Bundle 信息

获取版本信息

extension Bundle {
    /// 应用版本号(如 1.0.0)
    var appVersion: String? {
        return infoDictionary?["CFBundleShortVersionString"] as? String
    }
    
    /// 构建号(如 123)
    var buildNumber: String? {
        return infoDictionary?["CFBundleVersion"] as? String
    }
    
    /// 完整版本字符串(如 "1.0.0 (123)")
    var fullVersion: String {
        let version = appVersion ?? "Unknown"
        let build = buildNumber ?? "Unknown"
        return "\(version) (\(build))"
    }
    
    /// 应用名称
    var appName: String? {
        return infoDictionary?["CFBundleName"] as? String
    }
    
    /// 显示名称(用户看到的名称)
    var displayName: String? {
        return infoDictionary?["CFBundleDisplayName"] as? String
    }
}

// 使用
print("App: \(Bundle.main.displayName ?? "Unknown")")
print("Version: \(Bundle.main.fullVersion)")

获取自定义配置

// Info.plist 中添加自定义键值
// <key>API_BASE_URL</key>
// <string>https://api.example.com</string>

extension Bundle {
    var apiBaseURL: String? {
        return infoDictionary?["API_BASE_URL"] as? String
    }
    
    var apiKey: String? {
        return infoDictionary?["API_KEY"] as? String
    }
    
    var isDebugMode: Bool {
        #if DEBUG
        return true
        #else
        return infoDictionary?["DEBUG_MODE"] as? Bool ?? false
        #endif
    }
}

// 使用
if let apiURL = Bundle.main.apiBaseURL {
    print("API URL: \(apiURL)")
}

类和可执行文件

1. principalClass - 主类

// 获取 bundle 的主类(Info.plist 中的 NSPrincipalClass)
if let principalClass = Bundle.main.principalClass {
    print("Principal class: \(principalClass)")
}

// 在插件中使用
if let pluginBundle = Bundle(path: "/path/to/plugin.bundle"),
   let pluginClass = pluginBundle.principalClass as? PluginProtocol.Type {
    let plugin = pluginClass.init()
    plugin.run()
}

2. classNamed(_:) - 通过名称获取类

// 通过字符串名称获取类
if let viewControllerClass = Bundle.main.classNamed("MyApp.HomeViewController") as? UIViewController.Type {
    let vc = viewControllerClass.init()
    navigationController?.pushViewController(vc, animated: true)
}

// 从框架加载类
let frameworkBundle = Bundle(for: SomeFrameworkClass.self)
if let customClass = frameworkBundle.classNamed("MyFramework.CustomView") {
    print("Class loaded: \(customClass)")
}

**

Giscus 未启用:请在 src/site.config.ts 中配置 repoId 与 categoryId。