Skip to content

Modules

alanwang edited this page Feb 25, 2026 · 3 revisions

模块设计

模块概览

SwiftMTP 项目采用模块化设计,每个模块职责明确,通过协议定义接口,实现高内聚低耦合。

SwiftMTP/
├── App/                    # 应用入口模块
├── Models/                 # 数据模型模块
├── Services/               # 业务逻辑模块
│   ├── MTP/               # MTP 服务模块
│   └── Protocols/         # 协议定义模块
├── Views/                  # 视图模块
├── Config/                 # 配置模块
└── Resources/              # 资源模块

App 模块

SwiftMTPApp

文件:SwiftMTPApp.swift

职责:

  • 应用入口点
  • 初始化管理器
  • 设置语言环境

关键代码:

@main
struct SwiftMTPApp: App {
    @StateObject private var languageManager = LanguageManager.shared

    init() {
        // 初始化语言环境
        LanguageManager.shared.loadSavedLanguage()
    }

    var body: some Scene {
        WindowGroup {
            MainWindowView()
                .environmentObject(LanguageManager.shared)
        }
    }
}

Models 模块

Device

文件:Device.swift

职责:

  • 表示 MTP 设备
  • 设备信息封装

属性:

struct Device: Identifiable, Hashable, Sendable {
    let id: UUID
    let deviceIndex: Int
    let name: String
    let manufacturer: String
    let model: String
    let serialNumber: String
    let batteryLevel: Int?
    var storageInfo: [StorageInfo]
    var mtpSupportInfo: MTPSupportInfo?
    var isConnected: Bool

    // 计算属性
    var displayName: String { get }       // 显示名称(优先使用设备名称,否则使用制造商+型号)
    var displayModel: String { get }      // 显示型号(避免信息重复)
    var totalCapacity: UInt64 { get }     // 总存储容量
    var totalFreeSpace: UInt64 { get }    // 总可用空间
}

MTPSupportInfo:

struct MTPSupportInfo: Identifiable, Codable, Sendable {
    let id: UUID
    let mtpVersion: String
    let deviceVersion: String
    let vendorExtension: String
}

StorageInfo:

struct StorageInfo: Identifiable, Codable, Sendable {
    let id: UUID
    let storageId: UInt32
    let maxCapacity: UInt64
    let freeSpace: UInt64
    let description: String

    // 计算属性
    var usedSpace: UInt64 { get }         // 已使用空间
    var usagePercentage: Double { get }   // 使用百分比(0-100)
}

FileItem

文件:FileItem.swift

职责:

  • 表示文件或文件夹
  • 文件信息封装

属性:

struct FileItem: Identifiable, Hashable, Comparable, Sendable {
    let id: UUID
    let objectId: UInt32
    let parentId: UInt32
    let storageId: UInt32
    let name: String
    let path: String
    let size: UInt64
    let modifiedDate: Date?
    let isDirectory: Bool
    let fileType: String
    var children: [FileItem]?

    // 计算属性
    var formattedSize: String { get }     // 格式化文件大小(如 "1.5 MB")
    var formattedDate: String { get }     // 格式化修改日期(如 "2024-01-14 10:30")
    var sortableDate: Date { get }        // 用于排序的日期(无时返回 1970-01-01)
    var fileExtension: String { get }     // 文件扩展名
}

特性:

  • 遵循 Comparable 协议,默认按名称排序
  • 使用 nonisolated init 支持跨 Actor 边界创建
  • 名称安全处理(空名称默认为 "Unknown")

TransferTask

文件:TransferTask.swift

职责:

  • 表示传输任务
  • 传输状态管理

属性:

@MainActor
class TransferTask: Identifiable, ObservableObject {
    let id: UUID
    let type: TransferType
    let fileName: String
    let sourceURL: URL
    let destinationPath: String
    let totalSize: UInt64

    @Published var transferredSize: UInt64 = 0
    @Published var status: TransferStatus = .pending
    @Published var speed: Double = 0
    @Published var startTime: Date?
    @Published var endTime: Date?

    // 计算属性
    var progress: Double { get }                    // 传输进度(0.0 - 1.0)
    var formattedProgress: String { get }           // 格式化的进度百分比字符串(如 "45.5%")
    var formattedSpeed: String { get }              // 格式化的传输速度字符串(如 "1.5 MB/s")
    var estimatedTimeRemaining: String { get }      // 预计剩余时间字符串(本地化)
}

方法:

  • updateProgress(transferred:speed:):更新传输进度和速度
  • updateStatus(_:):更新传输状态,自动处理开始/结束时间

TransferType:

enum TransferType: String, Codable, Sendable {
    case upload = "upload"
    case download = "download"

    /// 传输类型的本地化显示名称
    var displayName: String {
        switch self {
        case .upload: return "上传"
        case .download: return "下载"
        }
    }
}

TransferStatus:

enum TransferStatus: Codable, Equatable, Sendable {
    case pending           // 等待中
    case transferring      // 传输中
    case paused           // 已暂停
    case completed        // 已完成
    case failed(String)   // 失败(包含错误信息)
    case cancelled        // 已取消

    /// 判断状态是否为活跃状态(正在传输或等待中)
    var isActive: Bool {
        switch self {
        case .transferring, .pending:
            return true
        default:
            return false
        }
    }
}

AppError

文件:AppError.swift

职责:

  • 定义应用错误类型
  • 提供错误信息

错误类型:

  • MTPError:MTP 设备相关错误
  • FileSystemError:文件系统相关错误
  • TransferError:文件传输相关错误
  • ConfigurationError:配置相关错误

AppLanguage

文件:AppLanguage.swift

职责:

  • 定义支持的语言
  • 语言枚举

支持的语言:

enum AppLanguage: String, CaseIterable, Identifiable {
    case system = "system"
    case english = "en"
    case chineseSimplified = "zh-Hans"
    case japanese = "ja"
    case korean = "ko"
    case russian = "ru"
    case french = "fr"
    case german = "de"
}

Services 模块

DeviceManager

文件:Services/MTP/DeviceManager.swift

职责:

  • 设备扫描
  • 设备连接管理
  • 设备选择

协议:DeviceManaging

关键属性:

@MainActor
class DeviceManager: ObservableObject, DeviceManaging {
    static let shared = DeviceManager()

    @Published var devices: [Device] = []
    @Published var selectedDevice: Device?
    @Published var isScanning: Bool = false
    @Published var connectionError: String?
    @Published var hasScannedOnce: Bool = false
    @Published var showManualRefreshButton: Bool = false

    private var scanTask: Task<Void, Never>?
    private let deviceIdCache = NSCache<NSNumber, UUIDWrapper>()
}

关键方法:

  • startScanning():启动设备扫描
  • stopScanning():停止设备扫描
  • scanDevices():扫描设备
  • selectDevice(_:):选择设备
  • manualRefresh():手动刷新

扫描策略:

  • 默认间隔:3 秒
  • 最大间隔:30 秒
  • 指数退避:连续失败后增加间隔

FileSystemManager

文件:Services/MTP/FileSystemManager.swift

职责:

  • 文件列表获取
  • 文件缓存管理
  • 文件夹操作

协议:FileSystemManaging

关键属性:

actor FileSystemManager {
    static let shared = FileSystemManager()

    private let fileCache = NSCache<NSString, CacheEntryWrapper>()
    private var deviceCacheKeys: [UUID: Set<String>] = [:]
}

关键方法:

  • getFileList(for:parentId:storageId:):获取文件列表
  • getRootFiles(for:):获取根目录文件
  • getChildrenFiles(for:parent:):获取子文件
  • clearCache():清空缓存
  • clearCache(for:):清空指定设备缓存

缓存策略:

  • 过期时间:60 秒
  • 最大条目:1000
  • 最大大小:50MB

FileTransferManager

文件:

  • Services/MTP/FileTransferManager.swift
  • Services/MTP/FileTransferManager+DirectoryUpload.swift

职责:

  • 文件上传/下载
  • 传输任务管理
  • 进度跟踪

协议:FileTransferManaging

关键属性:

class FileTransferManager: ObservableObject {
    static let shared = FileTransferManager()

    @Published var activeTasks: [TransferTask] = []
    @Published var completedTasks: [TransferTask] = []

    private let transferQueue = DispatchQueue(label: "com.swiftmtp.transfer", qos: .userInitiated)
    private let taskLock = NSLock()
}

关键方法:

  • uploadFile(to:sourceURL:parentId:storageId:):上传文件
  • downloadFile(from:fileItem:to:shouldReplace:):下载文件
  • cancelTask(_:):取消任务
  • cancelAllTasks():取消所有任务
  • clearCompletedTasks():清除已完成任务

安全验证:

  • 路径安全验证
  • 文件存在性检查
  • 文件大小限制
  • 存储空间验证

LanguageManager

文件:Services/LanguageManager.swift

职责:

  • 语言管理
  • 本地化字符串获取
  • 语言切换

协议:LanguageManaging

关键属性:

class LanguageManager: ObservableObject {
    static let shared = LanguageManager()

    @Published var currentLanguage: AppLanguage {
        didSet {
            saveLanguage()
            updateBundle()
            notifyLanguageChanged()
        }
    }

    private var bundle: Bundle = .main
}

关键方法:

  • localizedString(for:):获取本地化字符串
  • loadSavedLanguage():加载保存的语言
  • saveLanguage():保存语言设置
  • updateBundle():更新语言包

LocalizationManager

文件:Services/LocalizationManager.swift

职责:

  • 提供类型安全的本地化字符串访问
  • 定义本地化字符串键

使用示例:

enum L10n {
    enum MainWindow {
        static var deviceList: String {
            LanguageManager.shared.localizedString(for: "deviceList")
        }
    }
}

// 使用
Text(L10n.MainWindow.deviceList)

Protocols 模块

DeviceManaging

文件:Services/Protocols/DeviceManaging.swift

定义:

@MainActor
protocol DeviceManaging: ObservableObject {
    var devices: [Device] { get set }
    var selectedDevice: Device? { get set }
    var isScanning: Bool { get set }
    var connectionError: String? { get set }
    var hasScannedOnce: Bool { get set }
    var showManualRefreshButton: Bool { get set }

    func updateScanInterval()
    func startScanning()
    func stopScanning()
    func scanDevices()
    func selectDevice(_ device: Device)
    func manualRefresh()
}

FileSystemManaging

文件:Services/Protocols/FileSystemManaging.swift

定义:

protocol FileSystemManaging {
    func getFileList(for device: Device, parentId: UInt32, storageId: UInt32) -> [FileItem]
    func getRootFiles(for device: Device) -> [FileItem]
    func getChildrenFiles(for device: Device, parent: FileItem) -> [FileItem]
    func clearCache()
    func forceClearCache()
    func clearCache(for device: Device)
}

FileTransferManaging

文件:Services/Protocols/FileTransferManaging.swift

定义:

@MainActor
protocol FileTransferManaging: ObservableObject {
    var activeTasks: [TransferTask] { get set }
    var completedTasks: [TransferTask] { get set }

    func downloadFile(from device: Device, fileItem: FileItem, to destinationURL: URL, shouldReplace: Bool)
    func uploadFile(to device: Device, sourceURL: URL, parentId: UInt32, storageId: UInt32)
    func cancelTask(_ task: TransferTask)
    func cancelAllTasks()
    func clearCompletedTasks()
}

LanguageManaging

文件:Services/Protocols/LanguageManaging.swift

定义:

@MainActor
protocol LanguageManaging: ObservableObject {
    var currentLanguage: AppLanguage { get set }

    func localizedString(for key: String) -> String
    func loadSavedLanguage()
    func saveLanguage()
}

Views 模块

MainWindowView

文件:Views/MainWindowView.swift

职责:

  • 主窗口布局
  • 导航结构

结构:

struct MainWindowView: View {
    @StateObject private var deviceManager = DeviceManager.shared
    @ObservedObject var languageManager = LanguageManager.shared

    var body: some View {
        NavigationSplitView {
            DeviceListView()
                .environmentObject(deviceManager)
        } detail: {
            if let selectedDevice = deviceManager.selectedDevice {
                FileBrowserView(device: selectedDevice)
            } else {
                ContentUnavailableView(...)
            }
        }
    }
}

DeviceListView

文件:Views/DeviceListView.swift

职责:

  • 设备列表显示
  • 设备选择交互

功能:

  • 显示已连接的 MTP 设备
  • 设备扫描状态指示
  • 手动刷新按钮

FileBrowserView

文件:

  • Views/FileBrowserView.swift
  • Views/FileBrowserView+Actions.swift
  • Views/FileBrowserView+ToolbarDrop.swift
  • Views/TableDoubleClickModifier.swift

职责:

  • 文件浏览界面
  • 文件操作交互

功能:

  • 面包屑导航
  • 文件表格显示
  • 拖拽上传
  • 右键菜单
  • 工具栏按钮

FileTransferView

文件:Views/FileTransferView.swift

职责:

  • 传输任务显示
  • 任务管理交互

功能:

  • 活动任务列表
  • 已完成任务列表
  • 任务取消
  • 清除已完成任务

SettingsView

文件:Views/SettingsView.swift

职责:

  • 设置界面
  • 配置管理

功能:

  • 语言选择
  • 下载位置
  • 传输设置
  • 高级选项
  • 关于信息

Components 模块

DeviceRowView

文件:Views/Components/DeviceRowView.swift

职责:

  • 设备行显示
  • 设备信息展示

功能:

  • 设备图标
  • 设备名称
  • 设备型号
  • 电池电量
  • 存储信息

TransferTaskRowView

文件:Views/Components/TransferTaskRowView.swift

职责:

  • 传输任务行显示
  • 任务进度展示

功能:

  • 文件图标
  • 文件名称
  • 传输进度
  • 传输速度
  • 任务状态

LiquidGlassView

文件:Views/Components/LiquidGlassView.swift

职责:

  • Liquid Glass 效果组件
  • 现代化 UI 效果

功能:

  • 背景扩展效果
  • 自定义徽章
  • 玻璃效果按钮

Config 模块

AppConfiguration

文件:Config/AppConfiguration.swift

职责:

  • 应用配置常量
  • 配置管理

配置项:

struct AppConfiguration {
    // MTP 协议常量
    static let rootDirectoryId: UInt32 = 0xFFFFFFFF

    // 文件传输常量
    static let maxFileSize: UInt64 = 10 * 1024 * 1024 * 1024  // 10GB

    // 设备扫描常量
    static let defaultScanInterval: TimeInterval = 3.0
    static let connectedDeviceScanInterval: TimeInterval = 5.0
    static let maxScanInterval: TimeInterval = 30.0
    static let maxFailuresBeforeManualRefresh: Int = 3

    // 缓存配置
    static let cacheExpirationInterval: TimeInterval = 60.0
    static let fileCacheCountLimit: Int = 1000
    static let fileCacheTotalCostLimit: Int = 50 * 1024 * 1024  // 50MB
    static let deviceCacheCountLimit: Int = 100
    static let deviceCacheTotalCostLimit: Int = 10 * 1024 * 1024  // 10MB

    // 日志配置
    static let enableDebugLogging: Bool  // DEBUG 模式下为 true

    // UserDefaults Keys
    static let languageKey = "appLanguage"
    static let scanIntervalKey = "scanInterval"
    static let autoCheckUpdatesKey = "autoCheckUpdates"
    static let lastUpdateCheckKey = "lastUpdateCheckDate"

    // 更新检查配置
    static let githubRepoOwner = "wang93wei"
    static let githubRepoName = "SwiftMTP"
    static let githubWikiURL = "https://github.com/wang93wei/SwiftMTP/wiki"
    static let updateCheckTimeout: TimeInterval = 30.0
    static let autoCheckInterval: TimeInterval = 24 * 60 * 60  // 24小时

    // 安全配置
    static let maxPathLength: Int = 4096

    // UI 配置
    static let defaultWindowWidth: CGFloat = 1200
    static let defaultWindowHeight: CGFloat = 800
    static let navigationColumnMinWidth: CGFloat = 200
    static let navigationColumnIdealWidth: CGFloat = 250
    static let navigationColumnMaxWidth: CGFloat = 300

    // 验证方法
    static func isValidFileSize(_ fileSize: UInt64) -> Bool
    static func isValidScanInterval(_ interval: TimeInterval) -> Bool
    static func isValidPathLength(_ path: String) -> Bool
}

Resources 模块

本地化资源

结构:

Resources/
├── Base.lproj/
│   ├── Localizable.strings
│   └── InfoPlist.strings
├── zh-Hans.lproj/
│   ├── Localizable.strings
│   └── InfoPlist.strings
├── ja.lproj/
│   ├── Localizable.strings
│   └── InfoPlist.strings
├── ko.lproj/
│   ├── Localizable.strings
│   └── InfoPlist.strings
├── ru.lproj/
│   ├── Localizable.strings
│   └── InfoPlist.strings
├── fr.lproj/
│   ├── Localizable.strings
│   └── InfoPlist.strings
└── de.lproj/
    ├── Localizable.strings
    └── InfoPlist.strings

图标资源

文件:

  • AppIcon.appiconset:应用图标
  • SwiftMTP_Logo.svg:应用 Logo

模块依赖关系

Views
  ↓
Services
  ↓
Models
  ↓
Config
  ↓
Resources

模块通信

View → ViewModel

// View 调用 ViewModel 方法
Button(action: {
    deviceManager.scanDevices()
}) {
    Text("刷新")
}

// View 观察 ViewModel 状态
List(deviceManager.devices) { device in
    DeviceRowView(device: device)
}

ViewModel → Model

// ViewModel 使用 Model
func scanDevices() {
    guard let jsonPtr = Kalam_Scan() else { return }
    defer { Kalam_FreeString(jsonPtr) }

    let jsonString = String(cString: jsonPtr)
    let kalamDevices = try JSONDecoder().decode([KalamDevice].self, from: data)

    self.devices = kalamDevices.map { Device(from: $0) }
}

ViewModel → Service

// ViewModel 调用 Service
let files = await FileSystemManager.shared.getFileList(
    for: device,
    parentId: parentId,
    storageId: storageId
)

模块测试

测试策略

  • 单元测试:测试单个模块
  • 集成测试:测试模块间交互
  • UI 测试:测试用户界面

测试文件

  • DeviceManagerTests.swift:设备管理器测试
  • FileSystemManagerTests.swift:文件系统管理器测试
  • FileTransferManagerTests.swift:文件传输管理器测试
  • DeviceTests.swift:设备模型测试
  • FileItemTests.swift:文件项模型测试
  • TransferTaskTests.swift:传输任务模型测试
  • AppLanguageTests.swift:应用语言测试
  • LanguageManagerTests.swift:语言管理器测试

模块扩展性

添加新功能

  1. 在 Models 中定义新的数据模型
  2. 在 Services 中实现业务逻辑
  3. 在 Protocols 中定义协议接口
  4. 在 Views 中创建用户界面
  5. 在 Resources 中添加本地化字符串

添加新语言

  1. 在 Resources 中创建新的 .lproj 文件夹
  2. 添加 Localizable.strings 文件
  3. 在 AppLanguage 中添加新的语言枚举
  4. 翻译所有本地化字符串

添加新协议

  1. 在 Protocols 中定义协议接口
  2. 在相应的 Manager 中实现协议
  3. 在测试中创建 Mock 实现
  4. 更新文档