用 SwiftUI 组织一个离线阅读应用
围绕稳定资料ID设计SwiftUI列表、导航、离线仓库和下载状态,区分视图状态、磁盘数据与可取消异步任务。
在SwiftUI工程中实现资料列表、按ID导航、保存阅读位置,并验证重排、删除、断网和重启后的状态。
离线阅读应用需要同时管理资料清单、正在看的资料和下载任务。用数组下标表示“第二篇”,列表排序后就可能打开另一篇;只把正文留在视图状态中,退出后又无法阅读。本课为这两个问题建立明确的数据与导航边界。
案例资料使用 N-1、N-2 等合成 ID。首个版本只支持应用自己提供的文本和用户主动下载,不承诺后台持续执行,也不加入账户同步。目标是做出可以在设备上复核的阅读流程。
资料身份与下载状态分别建模
每条资料记录包含稳定 ID、标题、内容版本、相对存储位置和最后阅读位置。标题可以修改,列表顺序可以改变,ID 不随之变化。正文文件与索引记录必须对应,不能因为索引存在就认定正文可读;读取前检查文件存在及版本是否适用。
下载状态可以表达为未下载、下载中、可离线、失败。失败状态保留可展示的原因和重试条件,进度与资料身份关联。阅读位置属于长期数据;列表展开、提示框显示等只是短期界面状态。先决定哪些状态需要跨启动保存,再选具体存储实现。
将“当前选中 ID”与“下载任务 ID”分开。用户可以在阅读一篇已保存资料时下载另一篇,不能让一个 loading 布尔值把整页覆盖。下载完成事件应携带资料 ID 与请求版本,防止晚到结果覆盖更新后的正文。
SwiftUI 从状态描述界面
SwiftUI 的视图描述当前应呈现的界面,框架管理视图身份和更新。视图值的重新创建不等于创建新的业务资料。列表使用资料 ID 来表达身份,导航传递 ID,再由仓库取内容,避免把整个可变记录复制到多个页面后各自修改。
@State 可以拥有某个视图所需的局部状态,但它不是磁盘数据库。长期阅读位置要在仓库中保存并在启动时加载。模型观察方式及导航 API 的可用性取决于目标系统和工具链;选择一种与最低部署版本兼容的方式,不要混用不同年代示例却忽略可用性诊断。
页面可分为 LibraryView、ReaderView 与下载状态区域。前者发送打开和下载事件,后者发送位置变化,仓库负责查询、保存和恢复。这样更容易判断一个错误应显示在某条资料旁,还是阻止整个资料库初始化。
在 Xcode 建立最小阅读工程
创建使用 SwiftUI 的应用工程,确认运行目标、签名配置和最低系统版本。先把两篇教学文本加入工程资源,并创建资料模型与仓库。第一轮实现只从资源读正文,验证列表、详情和返回导航,不必先接网络。
第二轮为仓库加入应用容器内的持久化:启动读索引,选择资料时读正文,位置改变后按明确节奏保存。写入失败要反馈,不能提前显示“已保存”。索引版本改变时提供迁移或安全回退;无法解析旧文件应报告状态,保留原数据供恢复。
第三轮用受控测试地址提供资料内容,加入下载按钮和失败重试。先写临时内容,校验成功后提交为有效副本,再更新索引状态。若失败,只清理自己创建且确认无用的临时副本,不触碰已有可阅读版本。用模拟器完成界面检查后,再在目标设备验证退出与重开。
异步任务需要所有者与提交条件
Swift 的 async 与 await 表达可挂起操作,遇到 await 不等于自动切到后台线程。UI 相关模型的修改应遵守主 actor 隔离;大量解析或其他耗时工作需要单独评估执行位置,避免堵住界面。actor 的保护也不意味着跨 await 的业务条件永远不变。
每个下载任务要有创建者和结束路径。用户取消后停止提交结果,取消是协作式的,代码仍需检查取消状态。页面离开时任务是否取消取决于产品要求:仅用于预览的工作可以随页面结束,需要保留的下载则由仓库或任务管理器拥有。
提交前再次检查资料仍存在、请求版本仍有效、任务没有被替代。错误区分网络失败、内容无效和存储失败,以便提供不同恢复动作。已经保留旧版本时可以继续阅读旧内容,但应显示它的状态,而不是让用户误以为更新已完成。
用离线模型检查选中项
下面代码验证选中项跟随稳定 ID。重排后仍找到原资料;删除该资料后,选择被置空。它是普通 Python 数据模型,没有运行 SwiftUI、导航容器或 iOS 文件系统。
items = [{"id": "N-1", "title": "First"}, {"id": "N-2", "title": "Second"}]
selected = "N-2"
items.reverse()
assert next(x for x in items if x["id"] == selected)["title"] == "Second"
items = [x for x in items if x["id"] != "N-2"]
selected = selected if any(x["id"] == selected for x in items) else None
assert selected is None
print(selected)
实际输出 None。第一条断言在倒序列表中仍找到标题 Second,第二条检查删除后的选择清理。若改成保存下标,重排可能静默改变选择。真实工程还要在导航路径中移除失效 ID,并向阅读页说明资料已不可用。
恢复与错误怎样进入页面
启动时先加载索引,显示加载、空库、成功或错误状态。空库是正常情况,可提供导入或下载入口;索引损坏才是恢复问题。正文丢失时将那条资料标成不可离线,并保留标题,允许用户重新取得内容。
进程退出后不要继续显示旧的“下载中”标签却没有任务。启动恢复应把未完成任务标为等待重试或重新创建,并验证已有临时数据。保存阅读位置使用资料 ID 和内容版本;新版段落结构改变时,旧位置可能需要回退到可用位置,而不是盲目应用数字偏移。
阅读位置写入可以合并连续的小变化,但离开页面或应用生命周期变化时要处理未完成保存。保留最后确认值,界面可区分仍在等待写入的当前位置。
练习:完成设备验收
实现两篇资料的排序、打开、位置保存与本地副本删除。加入一次必定失败的下载路径,界面应保留原资料并允许重试。删除当前阅读资料时退出失效详情或显示明确的缺失状态,不发生数组越界。
验收依次重排列表、切换详情、断网阅读、关闭再打开、取消下载并重新下载。检查选中 ID、正文版本和存储状态是否一致。记录实际运行的工具链与设备;本文的 Python 断言仅验证身份契约,完整应用的持久化与并发行为仍由这些工程测试确认。
资料来源与延伸阅读
资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。