来源:AI开发日志公众号专辑「Build Your Own X With AI」
原文链接:https://mp.weixin.qq.com/s?__biz=MzUxMjg3MjE2OA==&mid=2247485830&idx=1&sn=62795e67ba353cb5fe3357572441ecb1&chksm=f95c9201ce2b1b173f42f1c02d5920c9f71c0e6e6d58615e7c1d3ac4b2f750ff615ae04bb156#rd

需求文档

介绍

此功能将为 WiKi 应用添加离线维基百科下载和管理功能。用户可以浏览 Kiwix 镜像站点上可用的 .zim 维基百科文件,下载所需的语言版本,并在本地离线查看维基百科内容。这将使用户能够在没有网络连接的情况下访问维基百科知识。

需求

需求 1

用户故事: 作为用户,我希望能够浏览可用的离线维基百科版本,以便选择我需要的语言和版本进行下载。

验收标准

当用户打开下载页面时,系统应显示从 https://www.mirrorservice.org/sites/download.kiwix.org/zim/wikipedia/ 获取的可用 .zim 文件列表

当系统获取文件列表时,系统应显示每个文件的语言、大小、发布日期和描述信息

当网络连接不可用时,系统应显示适当的错误消息并提供重试选项

当文件列表加载时,系统应显示加载指示器

需求 2

用户故事: 作为用户,我希望能够下载选定的 .zim 维基百科文件,以便离线使用。

验收标准

当用户选择一个 .zim 文件时,系统应开始下载该文件到本地存储

当下载进行中时,系统应显示下载进度条和当前下载速度

当下载完成时,系统应通知用户下载成功并更新文件状态

当下载失败时,系统应显示错误消息并提供重试选项

当用户取消下载时,系统应停止下载并清理部分下载的文件

需求 3

用户故事: 作为用户,我希望能够管理已下载的维基百科文件,以便有效利用设备存储空间。

验收标准

当用户查看下载列表时,系统应显示所有已下载文件的状态(已下载、下载中、暂停、失败)

当用户长按已下载的文件时,系统应提供删除选项

当用户删除文件时,系统应从本地存储中移除该文件并更新列表

当系统检测到存储空间不足时,系统应警告用户并建议删除不需要的文件

需求 4

用户故事: 作为用户,我希望能够搜索和筛选可用的维基百科版本,以便快速找到我需要的内容。

验收标准

当用户在搜索框中输入关键词时,系统应实时筛选显示匹配的 .zim 文件

当用户选择语言筛选器时,系统应只显示该语言的维基百科版本

当用户按文件大小排序时,系统应按升序或降序重新排列文件列表

当没有匹配的搜索结果时,系统应显示”未找到匹配项”的消息

需求 5

用户故事: 作为用户,我希望能够离线查看已下载的维基百科内容,以便在没有网络的情况下获取知识。

验收标准

当用户点击已下载的 .zim 文件时,系统应打开该维基百科的主页

当用户在离线维基百科中搜索时,系统应在本地 .zim 文件中查找匹配的文章

当用户点击文章链接时,系统应在应用内显示该文章内容

当 .zim 文件损坏或无法读取时,系统应显示错误消息并建议重新下载

设计文档

概述

离线维基百科下载器功能将为 WiKi 应用添加完整的离线维基百科管理系统。该功能包括从 Kiwix 镜像站点获取可用文件列表、下载管理、本地存储和离线内容查看。设计采用 MVVM 架构模式,确保代码的可维护性和可测试性。

架构

整体架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐

│   Presentation  │    │    Business     │    │      Data       │

│     Layer       │◄──►│     Logic       │◄──►│     Layer       │

│   (SwiftUI)     │    │   (ViewModels)  │    │ (Repositories)  │

└─────────────────┘    └─────────────────┘    └─────────────────┘

核心组件

DownloadListView

主要的下载管理界面

WikipediaFileService

处理文件列表获取和下载逻辑

ZimFileManager

管理本地 .zim 文件的存储和访问

WikipediaReader

处理 .zim 文件的读取和内容展示

组件和接口

  1. 数据模型

WikipediaFile

structWikipediaFile: Identifiable, Codable {

let id =UUID()

let name: String

let url: URL

let size: Int64

let language: String

let description: String

let publishDate: Date

var downloadStatus: DownloadStatus= .notDownloaded

var downloadProgress: Double=0.0

}

enumDownloadStatus: String, CaseIterable {

case notDownloaded =”未下载”

case downloading =”下载中”

case paused =”已暂停”

case completed =”已完成”

case failed =”下载失败”

}

DownloadTask

classDownloadTask: ObservableObject {

let file: WikipediaFile

@Publishedvar progress: Double=0.0

@Publishedvar status: DownloadStatus= .notDownloaded

@Publishedvar downloadSpeed: String=””

@Publishedvar error: Error?

privatevar urlSessionTask: URLSessionDownloadTask?

}

  1. 服务层

WikipediaFileService

protocolWikipediaFileServiceProtocol {

funcfetchAvailableFiles() asyncthrows -> [WikipediaFile]

funcdownloadFile(_file: WikipediaFile) -> DownloadTask

funccancelDownload(forfile: WikipediaFile)

funcpauseDownload(forfile: WikipediaFile)

funcresumeDownload(forfile: WikipediaFile)

}

classWikipediaFileService: NSObject, WikipediaFileServiceProtocol, URLSessionDownloadDelegate {

privatelet session: URLSession

privatevar activeTasks: [UUID: DownloadTask] = [:]

// 实现网络请求和下载管理

}

ZimFileManager

protocolZimFileManagerProtocol {

funcgetLocalFiles() -> [WikipediaFile]

funcdeleteFile(_file: WikipediaFile) throws

funcgetFileSize(_file: WikipediaFile) -> Int64

funcisFileValid(_file: WikipediaFile) -> Bool

funcgetStorageInfo() -> StorageInfo

}

structStorageInfo {

let totalSpace: Int64

let availableSpace: Int64

let usedSpace: Int64

}

  1. 视图模型

DownloadListViewModel

classDownloadListViewModel: ObservableObject {

@Publishedvar availableFiles: [WikipediaFile] = []

@Publishedvar downloadedFiles: [WikipediaFile] = []

@Publishedvar isLoading =false

@Publishedvar searchText =””

@Publishedvar selectedLanguage: String?

@Publishedvar sortOption: SortOption= .name

@Publishedvar errorMessage: String?

privatelet fileService: WikipediaFileServiceProtocol

privatelet zimManager: ZimFileManagerProtocol

// 业务逻辑方法

funcloadAvailableFiles()

funcdownloadFile(_file: WikipediaFile)

funcdeleteFile(_file: WikipediaFile)

funcfilteredFiles() -> [WikipediaFile]

}

enumSortOption: String, CaseIterable {

case name =”名称”

case size =”大小”

case date =”日期”

case language =”语言”

}

  1. 用户界面组件

DownloadListView

structDownloadListView: View {

@StateObjectprivatevar viewModel =DownloadListViewModel()

@Stateprivatevar showingDownloaded =false

var body: someView {

NavigationView {

VStack {

SearchAndFilterBar()

SegmentedControl() // 可用文件 / 已下载文件

FileListView()

}

}

}

}

FileRowView

structFileRowView: View {

let file: WikipediaFile

let onDownload: () -> Void

let onDelete: () -> Void

var body: someView {

HStack {

FileInfoView(file: file)

Spacer()

ActionButtonsView(file: file, onDownload: onDownload, onDelete: onDelete)

}

}

}

数据模型

本地存储结构

Documents/

├── WikipediaFiles/

│   ├── downloaded/

│   │   ├── wikipedia_zh_all_2024-01.zim

│   │   └── wikipedia_en_all_2024-01.zim

│   └── metadata/

│       └── files_metadata.json

网络数据解析

从 Kiwix 镜像站点解析 HTML 页面,提取 .zim 文件信息:

文件名和下载链接

文件大小

最后修改日期

通过文件名推断语言和描述

错误处理

网络错误处理

enumNetworkError: LocalizedError {

case noInternetConnection

case serverUnavailable

case invalidResponse

case downloadFailed(Error)

var errorDescription: String? {

switchself {

case .noInternetConnection:

return”网络连接不可用,请检查网络设置”

case .serverUnavailable:

return”服务器暂时不可用,请稍后重试”

case .invalidResponse:

return”服务器响应无效”

case .downloadFailed(let error):

return”下载失败:(error.localizedDescription)”

}

}

}

存储错误处理

enumStorageError: LocalizedError {

case insufficientSpace

case fileCorrupted

case permissionDenied

case fileNotFound

var errorDescription: String? {

switchself {

case .insufficientSpace:

return”存储空间不足,请删除一些文件后重试”

case .fileCorrupted:

return”文件已损坏,建议重新下载”

case .permissionDenied:

return”没有文件访问权限”

case .fileNotFound:

return”文件未找到”

}

}

}

测试策略

单元测试

WikipediaFileService

测试网络请求、下载逻辑

ZimFileManager

测试文件管理操作

DownloadListViewModel

测试业务逻辑和状态管理

集成测试

网络层集成

测试实际的网络请求和响应处理

文件系统集成

测试文件下载、存储和删除流程

UI集成

测试用户交互和状态更新

UI测试

下载流程

测试完整的文件下载用户流程

搜索和筛选

测试搜索功能和筛选器

错误处理

测试各种错误场景的用户体验

性能测试

大文件下载

测试大型 .zim 文件的下载性能

内存使用

监控下载过程中的内存使用情况

并发下载

测试多个文件同时下载的性能

测试数据

使用模拟的 Kiwix 服务器响应进行单元测试

创建小型测试 .zim 文件用于集成测试

使用真实的网络环境进行端到端测试

实施计划

[x] 1. 建立项目结构和核心数据模型

创建 Models、Services、ViewModels 和 Views 目录结构

实现 WikipediaFile 和 DownloadStatus 数据模型

创建 DownloadTask 类用于管理下载状态

需求: 1.1, 2.1, 3.1

[x] 2. 实现网络服务层

使用 URLSession 实现文件下载

添加下载进度跟踪

实现下载暂停和恢复功能

处理下载失败和重试逻辑

需求: 2.1, 2.2, 2.4, 2.5

创建 HTML 解析器获取 .zim 文件列表

解析文件名、大小、日期等信息

实现语言识别逻辑

编写解析功能的单元测试

需求: 1.1, 1.2

定义网络服务接口

实现基础的网络请求功能

添加错误处理机制

需求: 1.1, 1.3

[x] 2.1 创建 WikipediaFileService 协议和基础实现

[x] 2.2 实现 Kiwix 镜像站点数据解析

[x] 2.3 实现文件下载功能

[x] 3. 创建本地文件管理系统

添加文件删除功能

实现文件完整性验证

创建存储信息查询接口

编写文件管理的单元测试

需求: 3.2, 3.3

创建本地文件存储结构

实现文件元数据管理

添加存储空间检查功能

需求: 3.1, 3.4

[x] 3.1 实现 ZimFileManager

[x] 3.2 实现文件操作功能

[x] 4. 开发核心视图模型

添加下载任务管理

实现下载状态更新

处理并发下载限制

编写视图模型的单元测试

需求: 2.1, 2.2, 2.3

实现文件列表状态管理

添加搜索和筛选逻辑

集成网络服务和文件管理器

需求: 4.1, 4.2, 4.3

[x] 4.1 创建 DownloadListViewModel

[x] 4.2 实现下载管理逻辑

[x] 5. 构建用户界面组件

实现搜索栏组件

添加语言筛选器

创建排序选项界面

需求: 4.1, 4.2, 4.3, 4.4

创建 FileRowView 显示文件信息

添加下载进度条

实现操作按钮(下载/删除/暂停)

需求: 1.2, 2.2, 3.2

实现 DownloadListView 主界面

添加分段控制器(可用/已下载)

创建加载状态指示器

需求: 1.1, 1.4, 3.1

[x] 5.1 创建主要列表视图

[x] 5.2 实现文件行视图组件

[x] 5.3 开发搜索和筛选界面

[x] 6. 实现离线内容查看功能

创建文章显示视图

实现内部链接导航

添加搜索结果显示

处理损坏文件的错误情况

需求: 5.1, 5.3, 5.4

研究和集成 ZIM 文件读取库

实现基础的文件内容提取

添加文章搜索功能

需求: 5.1, 5.2

[x] 6.1 创建 ZIM 文件读取器

[x] 6.2 开发离线维基百科查看器

[-] 7. 添加错误处理和用户反馈

实现下载完成通知

添加存储空间警告

创建操作确认对话框

需求: 2.3, 3.4

创建统一的错误处理机制

添加用户友好的错误消息

实现错误恢复建议

需求: 1.3, 2.4, 3.4, 5.4

[x] 7.1 实现全局错误处理

[-] 7.2 添加用户通知和反馈

[ ] 8. 集成测试和优化

优化大文件下载性能

减少内存使用

测试并发下载场景

修复发现的问题

需求: 2.1, 2.2

测试完整的下载流程

验证文件管理操作

测试网络错误处理

需求: 所有需求

[ ] 8.1 编写集成测试

[ ] 8.2 性能优化和最终调试

[ ] 9. 更新主应用集成

修改 ContentView 集成新的下载功能

更新应用导航结构

确保与现有代码的兼容性

需求: 所有需求

image-1