Compare commits
26
Commits
0.0.4
..
3e60bf1869
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e60bf1869 | ||
|
|
e01cbc169d | ||
|
|
529663829f | ||
|
|
d830311894 | ||
|
|
735c14c824 | ||
|
|
85cf92161c | ||
|
|
9392027106 | ||
|
|
c61115cab3 | ||
|
|
95cead5863 | ||
|
|
68d9363f0a | ||
|
|
52f803e8db | ||
|
|
1422461224 | ||
|
|
a17164d1a9 | ||
|
|
1bde945d36 | ||
|
|
5a19cfde14 | ||
|
|
7c16647306 | ||
|
|
d56e99a83f | ||
|
|
fed34da246 | ||
|
|
72c1f8d89c | ||
|
|
822949f6cc | ||
|
|
6a0a1223b1 | ||
|
|
adc24d3f36 | ||
|
|
8ccb7157b2 | ||
|
|
063a493b18 | ||
|
|
d7fcda345d | ||
|
|
d5a7755702 |
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: commit-git-changes
|
||||
description: 审查当前 Git 工作区,安全选择本次任务文件,生成包含问题背景、根因和修改方案的详细中文提交说明,并提交及按用户要求推送到 Git 服务器。默认不执行构建或测试;用户要求“提交代码”“提交并推送”“同步到 Git”“上传当前修改”或要求生成详细 commit message 时使用。
|
||||
---
|
||||
|
||||
# 提交 Git 改动
|
||||
|
||||
## 目标
|
||||
|
||||
在不混入无关修改、不泄露敏感信息、不改写远端历史的前提下,完成“审查—暂存—提交—推送—确认”闭环,并用提交说明解释为什么修改以及如何解决,而不只是罗列文件。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 读取仓库级 `AGENTS.md`、当前分支、远端和工作区状态。
|
||||
2. 检查暂存与未暂存差异:
|
||||
- 使用 `git status --short` 确认全部改动。
|
||||
- 使用 `git diff` 和 `git diff --cached` 理解真实行为变化。
|
||||
- 必要时查看相关源码和测试,不能仅依据文件名编写提交说明。
|
||||
3. 划定本次提交范围:
|
||||
- 只纳入当前用户任务直接相关的文件。
|
||||
- 保留用户已有或其他任务产生的无关修改。
|
||||
- 优先显式 `git add <path...>`;仅在确认所有改动均属本次提交时使用 `git add -A`。
|
||||
- 发现密钥、令牌、证书、个人配置、大型生成物或疑似敏感数据时停止提交并说明。
|
||||
4. 不默认执行构建、测试或项目级验证;仅在用户明确要求时执行。
|
||||
5. 暂存后再次检查:
|
||||
- 执行 `git diff --cached --check`。
|
||||
- 执行 `git diff --cached --stat` 和 `git diff --cached`。
|
||||
- 确认暂存区没有无关文件、调试残留和意外格式变化。
|
||||
6. 编写详细中文提交说明并创建提交。
|
||||
7. 用户要求提交到服务器或同步远端时推送:
|
||||
- 有 upstream 时推送当前分支。
|
||||
- 没有 upstream 且远端、目标分支明确时,使用 `git push -u <remote> <branch>`。
|
||||
- 远端或目标分支不明确时先询问,不猜测。
|
||||
- 禁止 force push;除非用户明确要求且风险已说明。
|
||||
8. 提交后确认提交哈希、提交标题、当前分支、推送结果和剩余未提交改动。
|
||||
|
||||
## 提交说明规范
|
||||
|
||||
提交说明使用“标题 + 空行 + 详细正文”的结构。
|
||||
|
||||
### 标题
|
||||
|
||||
- 用一句中文概括用户可感知的问题和修复结果。
|
||||
- 优先写清触发条件、异常表现和结果,例如:
|
||||
`修复无训练点普通课程完成后重进播放页回退为未完成`
|
||||
- 使用明确动词,如“修复”“新增”“调整”“重构”“移除”。
|
||||
- 不使用“修改代码”“优化问题”“更新若干内容”等空泛描述。
|
||||
- 保持单行,不以句号结尾,不添加无依据的工单号或模块前缀。
|
||||
|
||||
### 正文
|
||||
|
||||
用完整段落解释以下内容:
|
||||
|
||||
1. **问题背景与影响**:什么场景触发、用户看到什么、影响哪些路径。
|
||||
2. **根因**:原有数据流、状态条件或实现约束为什么导致问题。
|
||||
3. **修改方案**:关键行为如何改变,为什么选择该方案,必要时说明兼容和边界处理。
|
||||
|
||||
正文应描述行为和因果关系,不要把 `git diff --stat` 改写成文件清单。多个紧密相关的修改可以分段说明,但不要堆砌逐文件 bullet。
|
||||
|
||||
若用户明确要求并实际执行了构建、测试或其他验证,可以在正文末尾如实补充;未执行时不需要添加“验证:未执行”之类的说明。
|
||||
|
||||
### 示例
|
||||
|
||||
```text
|
||||
修复无训练点普通课程完成后重进播放页回退为未完成
|
||||
|
||||
课件完成态本地记录的保存不区分训练点,但读取端仅在存在
|
||||
trainPointModel 时才创建合并器,导致班级、训练和项目均为空的普通课程
|
||||
重进播放页后无法读取本地完成记录。
|
||||
|
||||
改为始终创建完成态合并器:无训练点时使用空维度合并;由于该场景没有
|
||||
服务端完成态可供对账,将本地记录作为唯一持久来源并跳过超期回收,保持
|
||||
重新进入播放页后的完成状态稳定。
|
||||
|
||||
```
|
||||
|
||||
根据真实改动改写示例内容,禁止照抄未发生的场景或验证结论。
|
||||
|
||||
## 提交与推送约束
|
||||
|
||||
- 不使用 `git reset --hard`、`git checkout --`、`git clean` 等破坏性命令处理工作区。
|
||||
- 不使用 `--amend`、rebase、历史改写或强制推送,除非用户明确授权。
|
||||
- 不跳过 hooks;hook 失败时先分析原因,不使用 `--no-verify` 绕过。
|
||||
- 提交失败后保留暂存区,修复可控问题并重试;不要重复创建等价提交。
|
||||
- 推送被拒绝时先获取并解释分支差异,不自动合并、变基或覆盖远端。
|
||||
- 若没有可提交差异,直接说明,不创建空提交。
|
||||
|
||||
## 交付
|
||||
|
||||
报告提交哈希和标题、推送的远端分支,以及仍留在工作区的未提交修改。若只完成本地提交而未推送,必须明确说明。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "提交 Git 代码"
|
||||
short_description: "安全审查改动并生成详细中文说明,完成 Git 提交与远端推送"
|
||||
default_prompt: "使用 $commit-git-changes 审查当前改动,生成详细中文提交说明并提交到 Git 服务器。"
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: discuss-sdk-feature-solution
|
||||
description: 讨论 ReadViewSDK 的新功能、修复或重构方案,核验 EPUB/PDF 阅读器源码,比较实现路径并收敛为可直接开发的计划。用户说“先讨论”“先给方案”“比较方案”“不要改代码”,或接口、缓存、渲染路径、持久化兼容存在会改变实现方式的灰区时使用;路径已明确且用户要求直接修改时不要使用。
|
||||
---
|
||||
|
||||
# SDK 功能方案讨论
|
||||
|
||||
## 目标
|
||||
|
||||
在编码前用仓库事实收敛决策,输出最小、可验证、能交给开发 Skill 直接执行的方案。本 Skill 默认不修改业务代码。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 检查工作区并定位目标模块、入口、相邻实现和对应文档。
|
||||
2. 区分已锁定决定、阻塞决定、实现假设、后续项与非范围。
|
||||
3. 按根目录 `AGENTS.md` 读取所需文档;结论必须由真实路径、类型、方法和调用链支撑。
|
||||
4. 方案未定时给出 2–4 个可行路径,比较改动面、公开 API、缓存/持久化、渲染路径、性能、回归和维护成本,并明确推荐一个。
|
||||
5. 用户已选定方案时只细化该方案,不继续横向发散。
|
||||
6. 方案至少覆盖:
|
||||
- EPUB:`.textReflowable`、`.webInteractive`、`.webFixedLayout` 中受影响的路径
|
||||
- PDF:宿主图片 Provider 与内置 PDFKit Provider 中受影响的路径
|
||||
- 公共 API、Codable/磁盘格式、缓存版本和 CocoaPods 集成影响
|
||||
- 异步乱序、取消、生命周期、内存压力和失败降级
|
||||
7. 收敛为文件级任务、数据/状态流、成功标准、验证方式、风险和回滚点。
|
||||
|
||||
## 输出
|
||||
|
||||
默认在对话中给出:
|
||||
|
||||
- 需求、范围与非范围
|
||||
- 已确认决定、阻塞项和必要假设
|
||||
- 方案对比或已选方案拆解
|
||||
- 涉及文件、调用链、复用点和兼容策略
|
||||
- 风险、回滚点与验证清单
|
||||
- 建议交给 `start-sdk-feature-dev-lite` 或 `start-sdk-feature-dev` 的执行摘要
|
||||
|
||||
用户要求落文档,或方案需要跨会话执行时,写入 `Doc/FeatureSolution/`,并在存在文档索引时同步 `Doc/index.md`。
|
||||
|
||||
## 边界
|
||||
|
||||
- 不为未确认的未来需求预埋抽象。
|
||||
- 必要改动、可选优化和后续治理必须分开。
|
||||
- 未核验的系统 API、第三方能力、文件格式或缓存行为不得写成事实。
|
||||
- 文档与源码冲突时以当前源码为事实,并明确指出文档待同步项。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "SDK 功能方案讨论"
|
||||
short_description: "核验当前仓库事实,比较实现路径并输出可直接开发的完整方案"
|
||||
default_prompt: "Use $discuss-sdk-feature-solution to compare implementation paths and produce an executable handoff."
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: generate-module-code-style-doc
|
||||
description: 基于真实源码生成或更新 ReadViewSDK 指定模块的代码组织、命名、分层、UI、状态、缓存和复用规范,适用于 EPUBCore、EPUBTextRendering、ReaderView、EPUBUI、RDPDFReaderView 或 Demo。全局规范使用 generate-sdk-code-style-doc。
|
||||
---
|
||||
|
||||
# 生成模块代码规范
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 确定目标模块、关注范围和输出文件;优先增量更新 `Doc/` 中现有对应文档。
|
||||
2. 读取目标模块源码、对应 code reference、`Doc/CONVENTIONS.md` 和必要架构章节。
|
||||
3. 抽样入口、协议、模型、View/Cell、Controller、Coordinator/Service、缓存和 Extension;不存在的层级不要补造。
|
||||
4. 提炼真实且重复出现的模式,并区分当前约定、特殊例外及原因、尚未落地的建议。
|
||||
5. 记录目录职责、命名、可见性、依赖方向、状态管理、布局、回调、缓存、失败处理、公共 API 和跨模块边界。
|
||||
6. 对 EPUB 模块说明适用的渲染路径;对 PDF 模块说明自定义 Provider 与 PDFKit 路径差异。
|
||||
7. 增量更新文档,检查与全局规范、podspec、架构和源码是否冲突。
|
||||
8. 文档新增、移动或重命名时同步 `Doc/index.md`。
|
||||
|
||||
## 边界
|
||||
|
||||
- 只描述目标模块,不把局部模式提升为全局事实。
|
||||
- 不虚构理想分层、协议或不存在的调用链。
|
||||
- 默认不修改业务代码;发现问题时作为建议或 finding 单独列出。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "生成模块代码规范"
|
||||
short_description: "基于真实源码生成指定 EPUB、PDF 或 Demo 模块的局部规范文档"
|
||||
default_prompt: "Use $generate-module-code-style-doc to document the coding patterns of this ReadViewSDK module."
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: generate-module-implementation-logic
|
||||
description: 从 ReadViewSDK 真实源码生成或更新指定模块或场景的入口、调用链、数据流、状态流、渲染流、缓存、持久化、异步回调和异常分支文档。用户要求梳理实现逻辑、调用链、分页或缓存流程时使用;代码风格规范改用对应 code-style Skill。
|
||||
---
|
||||
|
||||
# 生成模块实现逻辑
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 确定目标模块、用户场景和输出路径;优先复用 `Doc/` 中现有专题或 code reference。
|
||||
2. 读取目标入口、现有文档,以及 `AGENTS.md` 路由出的架构、API 或测试资料。
|
||||
3. 用引用搜索沿真实关系追踪:
|
||||
- 页面、控制器、协议或服务入口及触发条件
|
||||
- 数据模型、状态所有者和线程/队列
|
||||
- EPUB 解析/排版/分页/章节窗口,或 PDF Provider/渲染/OCR/标注
|
||||
- 内存与磁盘缓存、键、版本、驱逐和恢复
|
||||
- 通知、回调、delegate、持久化和跨模块依赖
|
||||
- 空值、失败、取消、重试、超时、迟到结果和兼容分支
|
||||
4. 对关键链路至少双向核验一次:从入口跟到结果,再从结果或回调查回调用方。
|
||||
5. 区分当前实现、历史兼容层、仅文档中的计划和待确认事实。
|
||||
6. 增量更新文档,保留正确内容,删除或标记失效链路。
|
||||
7. 结论引用真实文件、类型和方法;不确定信息明确标为待确认。
|
||||
8. 文档新增、移动或重命名时同步 `Doc/index.md`。
|
||||
|
||||
## 推荐结构
|
||||
|
||||
- 适用范围与模块职责
|
||||
- 入口和主要对象
|
||||
- 主流程与数据/状态/渲染变化
|
||||
- 缓存、持久化和并发模型
|
||||
- 异常、取消、重试和兼容处理
|
||||
- 维护风险与验证要点
|
||||
|
||||
纯文档任务不修改业务代码;源码暴露的缺陷应单独报告,不把建议写成已实现事实。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "生成模块实现逻辑"
|
||||
short_description: "梳理指定阅读器模块的入口、调用链、状态流、缓存及异常分支"
|
||||
default_prompt: "Use $generate-module-implementation-logic to document this module's verified implementation flow."
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: generate-sdk-code-style-doc
|
||||
description: 基于 ReadViewSDK 当前 EPUB、PDF 和 Demo 源码生成或更新全局代码规范、目录职责、命名、分层、UIKit/SnapKit、并发、缓存、公共 API 与测试约定。用户要求项目级编码规范或更新 Doc/CONVENTIONS.md 时使用;单模块规范改用 generate-module-code-style-doc。
|
||||
---
|
||||
|
||||
# 生成 SDK 全局代码规范
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 明确输出路径;未指定时增量更新 `Doc/CONVENTIONS.md`。
|
||||
2. 读取现有目标文档、`Doc/ARCHITECTURE.md`、`Doc/index.md` 和两个 podspec。
|
||||
3. 从 EPUBCore、EPUBTextRendering、ReaderView、EPUBUI、RDPDFReaderView 与 Demo/UITests 抽样真实源码。
|
||||
4. 把结论分为:
|
||||
- 多模块源码验证的现行约定
|
||||
- 仅适用于 EPUB、PDF 或 Demo 的局部模式
|
||||
- 明确标记为建议、尚未成为项目事实的待统一项
|
||||
5. 覆盖目录职责、命名与可见性、公共 API、UIKit/SnapKit、异步与取消、缓存/持久化、错误处理、测试、注释和 CocoaPods 约定。
|
||||
6. 增量保留仍正确内容,删除失效描述;使用真实路径、类型和方法作为证据。
|
||||
7. 新增、移动或重命名文档时同步 `Doc/index.md`,最后检查链接和源码一致性。
|
||||
|
||||
## 边界
|
||||
|
||||
- 不把 EPUB 的局部模式提升为 PDF 的全局规范,反之亦然。
|
||||
- 不以旧 `.claude/skills` 或过期文档替代当前源码事实。
|
||||
- 不把个人偏好写成现行规范。
|
||||
- 文档任务默认不修改业务代码;发现缺陷时单独报告。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "生成 SDK 全局规范"
|
||||
short_description: "从当前 EPUB 与 PDF 源码证据生成或更新项目级代码规范文档"
|
||||
default_prompt: "Use $generate-sdk-code-style-doc to update the project-wide coding conventions from source evidence."
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: review-swift-ios-sdk
|
||||
description: 对 ReadViewSDK 的 Swift/iOS 代码进行专项审查,覆盖并发、主线程、生命周期、内存、缓存、渲染性能、可访问性、安全、公共 API 和持久化兼容。用户要求继续检查隐藏问题、代码审查、性能或内存审计时使用;普通功能开发和纯文档任务不要单独使用。
|
||||
---
|
||||
|
||||
# Swift/iOS SDK 专项审查
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 明确审查范围和修改授权;只要求审查时不得修改代码。
|
||||
2. 检查工作区和最近改动,读取目标代码、相邻实现及 `AGENTS.md` 路由出的文档。
|
||||
3. 沿入口、异步回调、持久化和渲染结果双向追踪,不只检查单个函数。
|
||||
4. 仅报告能由当前源码证明、可触发且值得行动的问题。
|
||||
|
||||
## 检查维度
|
||||
|
||||
### 并发与生命周期
|
||||
|
||||
- UI 是否回到主线程,迟到结果是否会覆盖新状态。
|
||||
- token、重试、超时和取消是否保证恰好一次完成。
|
||||
- 闭包、Task、通知、定时器、观察者、WKWebView 和 Vision 请求是否释放。
|
||||
- PDFKit、缓存字典和持久化文件是否在正确队列或锁内访问。
|
||||
|
||||
### 性能与内存
|
||||
|
||||
- 滚动、缩放、绘画和分页热点是否包含同步 IO、重复解码或重复排版。
|
||||
- EPUB 章节窗口、页图缓存、PDF 页面位图和绘画缓存是否有明确边界。
|
||||
- 内存警告后可见内容能否恢复,驱逐后迟到回调是否重新撑大缓存。
|
||||
- 图片像素成本、NSCache 限额、autoreleasepool 和大对象生命周期是否合理。
|
||||
|
||||
### SDK 兼容性
|
||||
|
||||
- `public` API、协议要求、初始化方法和默认行为是否意外 breaking。
|
||||
- Codable、文件名、缓存键、book identifier 和 schema version 是否可迁移。
|
||||
- EPUB 三种渲染路径和 PDF 两种 Provider 路径是否行为一致。
|
||||
- podspec 是否包含新增源码、资源、系统 framework 和依赖。
|
||||
|
||||
### 交互、安全与可访问性
|
||||
|
||||
- 缩放锚点、Cell 复用、双页布局、手势竞争和绘画图层是否稳定。
|
||||
- 图标按钮、自定义控件、动态字体和 VoiceOver 语义是否完整。
|
||||
- 日志、文件路径和错误信息是否泄露敏感数据。
|
||||
|
||||
## 输出
|
||||
|
||||
按严重程度列出 findings;每条包含位置、触发条件、用户影响、根因和最小修复建议。没有发现问题时明确说明,并列出尚未覆盖的运行时验证空白。
|
||||
|
||||
若用户同时要求修改,沿用对应开发 Skill 的实现、验证和交付规则。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Swift/iOS SDK 专项审查"
|
||||
short_description: "审查并发、生命周期、性能、内存及公开 API 风险"
|
||||
default_prompt: "Use $review-swift-ios-sdk to audit this ReadViewSDK code for iOS-specific risks."
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: start-sdk-feature-dev-lite
|
||||
description: 实现 ReadViewSDK 的单模块功能、Bug 修复、UI 调整、局部重构、编译修复或小范围 CocoaPods 变更,并完成必要验证。作为默认开发入口;跨 EPUB/PDF 模块、改变公共协议或持久化格式、需要迁移回滚或严格执行完整方案时使用 start-sdk-feature-dev。
|
||||
---
|
||||
|
||||
# SDK 轻量开发
|
||||
|
||||
## 目标
|
||||
|
||||
用最小改动完成边界清晰的任务,形成“实现—验证—交付”闭环。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 检查工作区,识别并保留用户已有修改。
|
||||
2. 定位目标源码、现有相邻模式和 `AGENTS.md` 路由出的文档。
|
||||
3. 若用户提供方案,完整读取并执行已锁定决定;源码事实使方案不可行时先说明冲突。
|
||||
4. 明确 In Scope、Out of Scope、成功标准和短执行清单。
|
||||
5. 按模块边界实施最小修改:
|
||||
- EPUB 解析与资源:`Sources/RDEpubReaderView/EPUBCore`
|
||||
- EPUB 文本排版:`Sources/RDEpubReaderView/EPUBTextRendering`
|
||||
- EPUB 翻页容器:`Sources/RDEpubReaderView/ReaderView`
|
||||
- EPUB 成品 UI:`Sources/RDEpubReaderView/EPUBUI`
|
||||
- PDF 阅读器:`Sources/RDPDFReaderView`
|
||||
- 集成与回归入口:`ReadViewDemo`
|
||||
6. 自检空值、失败分支、异步乱序、取消语义、主线程、生命周期、Cell 复用、缓存边界、内存警告和可访问性。
|
||||
7. 检查公共 API、持久化模型、缓存键/版本、资源路径及 podspec 是否被意外改变。
|
||||
8. 按风险执行构建或定向 UI 测试;编译错误应修复并重试。
|
||||
9. 代码行为显著变化时更新最接近的 `Doc/` 文档。
|
||||
|
||||
## 升级条件
|
||||
|
||||
出现以下任一情况时切换到 `start-sdk-feature-dev` 并说明:
|
||||
|
||||
- 同时改变 EPUB 与 PDF 阅读器,或改变多个 EPUB 层级的职责。
|
||||
- 修改宿主可见协议、公共初始化方法或需要兼容迁移。
|
||||
- 改变持久化格式、缓存架构、分页主流程或跨模块状态流。
|
||||
- 用户要求逐项执行完整开发方案。
|
||||
|
||||
## 交付
|
||||
|
||||
最终简洁说明完成内容、关键取舍、主要文件、验证结果、未覆盖风险和同步文档;不强制套固定模板。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "SDK 轻量开发"
|
||||
short_description: "完成局部功能、Bug 修复、小范围重构及对应构建验证闭环"
|
||||
default_prompt: "Use $start-sdk-feature-dev-lite to implement this scoped ReadViewSDK change and verify it."
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: start-sdk-feature-dev
|
||||
description: 实现 ReadViewSDK 的跨层或跨阅读器改造、分页/缓存/持久化系统性重构、公共 API 演进、复杂联调,或严格执行用户提供的完整方案文档。单模块功能、UI 微调、Bug 修复和局部改动优先使用 start-sdk-feature-dev-lite。
|
||||
---
|
||||
|
||||
# SDK 完整开发
|
||||
|
||||
## 目标
|
||||
|
||||
对复杂任务建立可追溯计划,控制接口兼容、依赖、迁移和回滚风险,完成实现、联调、验证与文档同步。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 检查工作区,区分本任务改动和用户已有改动。
|
||||
2. 明确主改动模块、受影响模块、In Scope、Out of Scope、联调依赖和完成标准。
|
||||
3. 按 `AGENTS.md` 读取架构、公共 API、目标子系统和测试文档,并用源码复核。
|
||||
4. 用户提供方案时:
|
||||
- 完整读取并提取锁定决定、任务清单、文件清单、限制和验收标准。
|
||||
- 逐项执行,不自行替换技术路径、数据模型或文件布局。
|
||||
- 方案与源码冲突时暂停,说明影响和最小修订建议。
|
||||
5. 没有方案时制定最小可落地计划,明确执行顺序、数据/状态流、风险、回滚点和验证。
|
||||
6. 实施时区分计划内修改、不可避免的“必要补齐”和可选后续项;会改变方案的偏差先确认。
|
||||
7. 做跨模块自检:
|
||||
- EPUB 四层依赖方向和三种渲染路径
|
||||
- PDF 自定义 Provider 与 PDFKit 直读路径
|
||||
- 公共 API 的源码兼容与行为兼容
|
||||
- Codable/磁盘格式迁移、缓存键、版本和失效策略
|
||||
- 异步 token、取消、迟到回调、主线程和生命周期
|
||||
- 大图、排版产物、章节窗口和内存警告处理
|
||||
- CocoaPods source/resource/framework 配置和 Demo 集成
|
||||
8. 同步架构、API、子系统或测试文档;文档新增或移动时更新索引。
|
||||
9. 执行构建和与风险相称的定向回归,记录无法自动覆盖的联调项。
|
||||
|
||||
## 方案控制
|
||||
|
||||
- 用户确认的方案不能被“更简单的建议”静默覆盖。
|
||||
- 不以顺手治理为由扩大重构范围。
|
||||
- 涉及线上数据、缓存迁移或公共 API 时必须写明兼容、回滚或降级方式。
|
||||
- 新增依赖、资源或公开类型前必须确认 podspec 和宿主集成影响。
|
||||
|
||||
## 交付
|
||||
|
||||
最终说明范围和计划完成情况、关键实现与偏差、改动模块、构建/测试结果、兼容与回滚策略、未覆盖风险和同步文档。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "SDK 完整开发"
|
||||
short_description: "执行跨阅读器模块改造、复杂联调、兼容评估与完整验证交付"
|
||||
default_prompt: "Use $start-sdk-feature-dev to implement this cross-module ReadViewSDK plan with full verification."
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -1,286 +0,0 @@
|
||||
---
|
||||
name: "Discuss SDK Feature Solution"
|
||||
description: "方案讨论优先 skill:用于 ReadViewSDK 新需求的实现方案分析、路径对比、取舍沟通、已定方案细化和实现交接。"
|
||||
argument-hint: "粘贴需求、目标模块、约束、已知备选方案;可附加:是否已确定方案 / 是否需要推荐方案 / 是否需要落方案文档。"
|
||||
---
|
||||
|
||||
# Discuss SDK Feature Solution
|
||||
|
||||
## Purpose
|
||||
|
||||
面向 ReadViewSDK 的"实现方案讨论入口" skill。
|
||||
用于在正式开发前,先把需求、约束、可选实现路径和关键取舍聊透;若方案已经确定,则只围绕已选方案继续细化,再交接给开发 skill 进入实现。
|
||||
|
||||
该 skill 借鉴 spec-driven 工作流思想:讨论阶段专门捕获灰区决策,计划阶段必须经过代码 / 文档事实核验,最终输出能直接喂给开发 skill 的结构化方案文档,避免"聊完还是不能开发"。
|
||||
|
||||
该 skill 的职责固定为:
|
||||
- 先理解需求与约束
|
||||
- 捕获灰区问题和实现假设
|
||||
- 用现有代码 / 文档核验方案可行性
|
||||
- 未定方案时:输出 2-4 个可行实现方案,并做对比
|
||||
- 已定方案时:只围绕确认方案展开实现讨论,不再继续扩展其他方案
|
||||
- 最后给出交接到开发 skill 的明确指令
|
||||
- 最终必须产出一份可直接供开发 skill 使用的开发详细文档,或一段可直接执行的开发详细描述
|
||||
|
||||
## Core Discussion Rules
|
||||
|
||||
- Rule 1 — Think Before Coding
|
||||
- 在提出方案前先核对代码和文档事实,不静默假设
|
||||
- 必须显式写出关键假设、主要取舍和已知风险
|
||||
- 遇到会改变实现路径的关键灰区,先提出并请求确认,不靠猜测补完整个方案
|
||||
- 若存在更简单且满足目标的实现路径,必须主动指出并优先推荐
|
||||
- Rule 2 — Simplicity First
|
||||
- 推荐方案优先选择最小可落地路径,而不是概念上更"完整"的设计
|
||||
- 不为了未来可能性预埋推测性扩展,不为单次使用引入抽象
|
||||
- 若某个方案明显过度设计,必须直接指出并解释为什么不推荐
|
||||
- Rule 3 — Surgical Changes
|
||||
- 方案只覆盖完成当前目标所必需的改动面
|
||||
- 不把无关重构、顺手统一风格或额外治理动作夹带进推荐方案
|
||||
- 若确有相邻改动依赖,必须明确标注"必要改动"与"可选优化"的边界
|
||||
- Rule 4 — Goal-Driven Execution
|
||||
- 讨论输出必须先定义成功标准和验证方式,再给实现交接建议
|
||||
- 不把讨论步骤本身当结果,重点是产出可执行、可验证的方案
|
||||
- 交接给开发 skill 时,必须让对方清楚"做到什么算完成"
|
||||
|
||||
## When To Use
|
||||
|
||||
当用户提出以下类型需求时使用:
|
||||
- "先讨论一下这个需求怎么实现"
|
||||
- "这个功能可能有多种实现方式,先分析方案"
|
||||
- "先别写代码,先帮我想实现路径"
|
||||
- "先比较几种方案,再决定怎么做"
|
||||
|
||||
适用场景:
|
||||
- 同一个需求可以通过多种技术路径实现
|
||||
- 需要权衡 SDK 分层(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI)、复用、维护成本、回归影响
|
||||
- 需要在实现前先明确推荐方案和待确认细节
|
||||
- 已经明确采用某个方案,但还需要继续细化实现边界、拆解落地路径和交接开发
|
||||
|
||||
不适用场景:
|
||||
- 用户已经明确要直接实现,且实现路径基本单一时,优先使用开发类 skill
|
||||
- 用户只想生成文档,不需要方案讨论时,优先使用文档类 skill
|
||||
|
||||
## Inputs
|
||||
|
||||
推荐输入:
|
||||
- 需求描述
|
||||
- 目标模块或 Feature(属于哪一层:EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI / Legacy)
|
||||
- 已知约束(兼容性、性能、API 兼容、CocoaPods 发布等)
|
||||
- 已有备选方案(如有)
|
||||
- 已确认方案(如已决定)
|
||||
- 是否需要推荐方案
|
||||
- 是否需要最终落方案文档
|
||||
|
||||
若信息不足:
|
||||
- 先通过代码和文档补齐可发现事实
|
||||
- 再围绕真正影响方案选择的灰区进行澄清
|
||||
- 若用户不想继续问答,必须显式列出"实现假设",并把假设写入方案或交接摘要
|
||||
|
||||
若用户已经明确指定"采用方案 X / 就按这个方案做":
|
||||
- 视为进入"已定方案模式"
|
||||
- 后续输出禁止继续罗列其他候选方案、备选实现或横向对比
|
||||
- 仅允许在必要时补充"当前方案的风险、前提、边界和实现细化"
|
||||
|
||||
## Scope / Required Context
|
||||
|
||||
默认先读:
|
||||
1. `Doc/ARCHITECTURE.md` — 四层架构、数据流、分页模式、位置模型、已知限制
|
||||
2. `Doc/CODING_STYLE.md` — 命名规范(RD/RDEPUB 前缀)、分层规则、extension 拆分规则、SS→RD 迁移计划
|
||||
3. `Doc/EPUB_MAINTENANCE.md` — 文件职责表、DTCoreText 渲染管线、常见排查场景
|
||||
|
||||
按需再读:
|
||||
- 涉及 EPUB 解析时:`Sources/RDReaderView/EPUBCore/` 下相关文件
|
||||
- 涉及文本渲染时:`Sources/RDReaderView/EPUBTextRendering/` 下相关文件
|
||||
- 涉及阅读器容器时:`Sources/RDReaderView/RDReaderView.swift` 及同级文件
|
||||
- 涉及 UI 层时:`Sources/RDReaderView/EPUBUI/` 下相关文件
|
||||
- 涉及遗留代码时:`Sources/RDReaderView/LegacyRDReaderController/` 下相关文件
|
||||
- 涉及 JS 桥接时:`Sources/RDReaderView/Resources/epub-bridge.js`
|
||||
|
||||
若文档与代码不一致:
|
||||
- 以代码事实为准做方案讨论
|
||||
- 在推荐方案中指出不一致点和可能影响
|
||||
|
||||
方案文档落地目录约束:
|
||||
- 讨论型方案文档统一落到 `Doc/FeatureSolution/`(若目录不存在则创建)
|
||||
- 不要把方案讨论文档写入其他目录
|
||||
|
||||
## SDK-Specific Discussion Rules
|
||||
|
||||
讨论时必须考虑的 SDK 特有约束:
|
||||
|
||||
- **分层边界**:方案必须明确落在哪一层,不得跨层引入反向依赖(上层依赖下层允许,反之不允许)
|
||||
- **Public API 兼容性**:若改动涉及公共接口(`RDReaderView`、`RDReaderDataSource`、`RDURLReaderController`、`RDEPUBReaderController` 等),必须评估对宿主 App 的 breaking change 影响
|
||||
- **渲染路径差异**:方案必须考虑三种渲染路径(`webFixedLayout` / `webInteractive` / `textReflowable`)的适用性,不能只覆盖单一路径
|
||||
- **CocoaPods 发布影响**:若方案涉及新增依赖、资源文件或模块结构调整,必须评估 podspec 变更
|
||||
- **RD 前缀规范**:所有新增类型必须使用 `RD` 前缀,EPUB 相关使用 `RDEPUB` 前缀
|
||||
- **Legacy 层边界**:不在 Legacy 层新增功能;若方案涉及 Legacy 代码,必须明确是迁移还是在新层实现
|
||||
|
||||
## GSD-Inspired Discussion Rules
|
||||
|
||||
讨论阶段要像 `discuss -> plan -> verify` 闭环一样,先捕获决策,再验证计划,而不是直接跳到实现建议。
|
||||
|
||||
必须执行:
|
||||
- 灰区捕获:先识别布局、接口形态、数据结构、错误态、路由、持久化、兼容性、回归范围等不明确点
|
||||
- 事实核验:方案落地前必须用本仓库代码和 `Doc/` 文档确认入口、复用点、约束和风险
|
||||
- 假设显式化:不能确认的问题必须写成 `Assumption`,不得藏在方案正文里
|
||||
- 阻塞分级:会改变实现路径的问题标记为 `Blocking Decision`,不会改变路径的问题标记为 `Follow-up`
|
||||
- 审计留痕:若落方案文档,必须包含"讨论结论 / 关键决策 / 假设 / 非范围 / 验证清单 / 开发交接指令"
|
||||
- 计划可执行:最终方案必须足够小,能被开发 skill 直接逐项执行
|
||||
|
||||
禁止行为:
|
||||
- 不得只输出宽泛建议,必须落到文件、类、方法、数据流或协议层面的执行点
|
||||
- 不得在用户已确认方案后继续展开新方案,除非用户明确要求重新比较
|
||||
- 不得把未经核验的包、SDK、接口能力写成已确认事实
|
||||
- 不得把需要用户拍板的关键决策伪装成默认实现
|
||||
|
||||
## Required Workflow
|
||||
|
||||
### Phase 1 — 识别需求、范围、约束、现状
|
||||
|
||||
- 明确目标价值、影响模块(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI / Legacy)、In Scope / Out of Scope
|
||||
- 识别当前项目里已有的实现模式、相邻能力和复用点
|
||||
- 判断这是单层需求还是跨层需求
|
||||
- 建立灰区清单,区分 `Blocking Decision` 与 `Follow-up`
|
||||
|
||||
### Phase 2 — 判断讨论模式
|
||||
|
||||
- 若方案未定:进入"多方案对比模式"
|
||||
- 若方案已定:进入"单方案细化模式"
|
||||
- 一旦用户已确认方案,后续同一轮讨论默认保持"单方案细化模式",除非用户明确要求重新打开方案比较
|
||||
|
||||
### Phase 3 — Research / Verify:核验代码与文档事实
|
||||
|
||||
- 对照当前仓库确认真实入口、调用链、数据模型、复用的 cell / view / handler / controller / protocol
|
||||
- 若方案涉及新增文件,必须确认所属目录层级和是否需要更新 podspec 的 source_files
|
||||
- 若方案涉及接口或数据字段,必须明确字段来源、空值策略和兼容策略
|
||||
- 若方案涉及 JS 桥接,必须确认 epub-bridge.js 的交互契约
|
||||
- 若发现文档和代码不一致,必须标注为风险或阻塞项
|
||||
- 若核验结果推翻原方案,必须暂停说明,不得继续包装成可执行方案
|
||||
|
||||
### Phase 4A — 多方案对比模式:列出多种实现路径
|
||||
|
||||
- 至少提出 2 个可行方案,推荐 2-4 个方案
|
||||
- 每个方案都要有清晰的实现方向,而不是抽象建议
|
||||
- 优先从现有项目模式和复用能力出发
|
||||
|
||||
### Phase 4B — 多方案对比模式:比较方案优缺点和影响范围
|
||||
|
||||
- 比较每个方案的:
|
||||
- 核心思路
|
||||
- 落在哪一层(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI)
|
||||
- 适用前提
|
||||
- 优点
|
||||
- 风险 / 成本
|
||||
- 对 SDK 分层、Public API、podspec、回归范围的影响
|
||||
- 明确哪些差异会真正影响后续实现和维护
|
||||
|
||||
### Phase 5A — 多方案对比模式:提出推荐方案,并列出待确认细节
|
||||
|
||||
- 必须明确推荐一个默认方案,不能只平铺选项
|
||||
- 待确认项只保留真正影响实现的关键决策
|
||||
- 默认产物为对话输出;若用户明确要求,再可选落到 `Doc/FeatureSolution/` 下方案文档
|
||||
|
||||
### Phase 5B — 单方案细化模式:围绕确认方案展开
|
||||
|
||||
- 不再输出"方案 A / 方案 B / 方案 C"式内容
|
||||
- 不再补充"其他也可以这样做"的备选实现,除非用户明确要求回到方案比较
|
||||
- 只讨论以下内容:
|
||||
- 当前方案的实现拆解(文件 / 类 / 方法 / 协议)
|
||||
- SDK 分层内的模块边界与职责分配
|
||||
- 关键数据流 / 状态流 / 渲染流
|
||||
- 复用点、依赖点、回归点
|
||||
- 对三种渲染路径的覆盖情况
|
||||
- 风险、前提、灰度方式、验证要点
|
||||
- 交接给开发 skill 的实现摘要
|
||||
|
||||
### Phase 6 — 输出实现交接建议
|
||||
|
||||
- 在方案确认后,明确下一步应交给哪个开发 skill:
|
||||
- 小中型、单层、MVP 优先:轻量开发 skill
|
||||
- 跨层、复杂联调、完整交付:完整开发 skill
|
||||
- 无论是否落文档,最终都必须产出开发交接载体,且二选一不能缺失:
|
||||
- 完整开发 skill:产出可直接执行的开发详细文档,默认落到 `Doc/FeatureSolution/`
|
||||
- 轻量开发 skill:产出可直接粘贴执行的开发详细描述,覆盖实现目标、范围、代码落点、关键步骤、验收标准和自测要点
|
||||
- 给出可直接粘贴给开发 skill 的实现摘要;若已有更完整的详细文档/描述,则摘要必须引用它而不是只给一句话概括
|
||||
- 若落方案文档,必须让开发指令引用该文档的真实路径,并提醒开发 skill 严格按计划执行
|
||||
|
||||
## Output Contract
|
||||
|
||||
默认输出结构按模式区分:
|
||||
|
||||
若方案未定:
|
||||
- A. 需求理解
|
||||
- B. 灰区问题与实现假设
|
||||
- C. 当前实现与约束
|
||||
- D. 可选方案对比
|
||||
- E. 推荐方案
|
||||
- F. 待确认实现细节
|
||||
- G. 实现交接建议
|
||||
|
||||
若方案已定:
|
||||
- A. 需求理解
|
||||
- B. 灰区问题与实现假设
|
||||
- C. 当前实现与约束
|
||||
- D. 确认方案拆解
|
||||
- E. 实现风险与关键细节
|
||||
- F. 实现交接建议
|
||||
|
||||
其中要求:
|
||||
- 未定方案时:
|
||||
- `B. 灰区问题与实现假设` 必须区分 Blocking Decision / Follow-up / Assumption
|
||||
- `D. 可选方案对比` 至少给出 2 个方案,推荐 2-4 个
|
||||
- `E. 推荐方案` 必须明确推荐一个默认方案
|
||||
- `F. 待确认实现细节` 只保留会改变实现路径的关键问题
|
||||
- `G. 实现交接建议` 必须明确下一步交给哪个开发 skill
|
||||
- `G. 实现交接建议` 必须附带可供开发 skill 直接使用的交接内容:
|
||||
- 若下一步是完整开发 skill,必须提供开发详细文档路径或完整文档正文
|
||||
- 若下一步是轻量开发 skill,必须提供开发详细描述正文
|
||||
- 已定方案时:
|
||||
- `B. 灰区问题与实现假设` 必须列出阻塞项、假设和后续项
|
||||
- `D. 确认方案拆解` 只能围绕确认方案展开,禁止附带其他方案信息
|
||||
- `E. 实现风险与关键细节` 只讨论该方案落地所需信息
|
||||
- `F. 实现交接建议` 必须明确下一步交给哪个开发 skill,并附带可直接执行的详细文档或详细描述
|
||||
|
||||
若用户要求落方案文档,文档必须包含:
|
||||
- 需求目标
|
||||
- 讨论结论
|
||||
- 关键决策
|
||||
- 实现假设
|
||||
- 非范围
|
||||
- SDK 分层落点(文件 / 类 / 方法 / 协议)
|
||||
- 数据流 / 状态流 / 渲染流
|
||||
- 开发任务清单
|
||||
- 验收标准
|
||||
- 自测清单
|
||||
- 待确认项
|
||||
- 给开发 skill 的执行指令
|
||||
|
||||
若未落方案文档,但下一步要交给轻量开发 skill,则最终输出中的"开发详细描述"至少必须包含:
|
||||
- 实现目标
|
||||
- In Scope / Out of Scope
|
||||
- SDK 分层落点(文件 / 类 / 方法 / 协议)
|
||||
- 实现步骤
|
||||
- 关键约束与复用点
|
||||
- 对三种渲染路径的覆盖说明
|
||||
- 验收标准
|
||||
- 自测清单
|
||||
- 待确认项或实现假设
|
||||
|
||||
## Validation
|
||||
|
||||
- 本 skill 默认只做方案讨论,不直接进入代码实现
|
||||
- 默认不修改仓库文件;只有用户明确要求时,才可选落方案文档到 `Doc/FeatureSolution/`
|
||||
- 即使不落方案文档,最终回复也必须包含一份可直接交给开发 skill 使用的详细交接内容,不能只停留在高层方案结论
|
||||
- 若本次调用只做讨论或只新增方案文档,可不执行构建验证
|
||||
- 若新增或移动方案文档,建议同步更新目录索引
|
||||
- 若后续进入代码实现,则由对应开发 skill 负责执行项目默认构建验证(`xcodebuild` 或 CocoaPods 集成验证)
|
||||
- 若用户已确认方案,则默认进入单方案细化模式,除非用户明确要求重新比较备选方案
|
||||
|
||||
## Maintenance Rules
|
||||
|
||||
- 该 skill 的职责是"讨论后交接实现",不和开发 skill、文档类 skill 重叠
|
||||
- 优先复用 `Doc/` 作为知识源,不新增独立 references 体系
|
||||
- 方案文档目录固定为 `Doc/FeatureSolution/`
|
||||
- 持续保持 `discuss -> research/verify -> plan handoff` 的轻量闭环,避免退化成只聊天不交付的建议列表
|
||||
- 若该 skill 的默认输出结构、交接规则或适用边界调整,必须同步更新对应文档
|
||||
- SDK 四层架构(EPUBCore → EPUBTextRendering → RDReaderView → EPUBUI)是方案讨论的核心参照框架,任何方案都必须明确标注落点层级
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
name: "Start SDK Feature Dev Lite"
|
||||
description: "轻量开发主控 skill:用于 ReadViewSDK 的小中型功能开发、单层改动、局部重构、文档同步与编译修复。"
|
||||
argument-hint: "粘贴需求、目标层级、验收标准;可附加:仅 MVP / 禁止新增依赖 / 指定渲染路径。"
|
||||
---
|
||||
|
||||
# Start SDK Feature Dev Lite
|
||||
|
||||
## Purpose
|
||||
|
||||
面向 ReadViewSDK 的轻量开发入口 skill。
|
||||
用于在现有项目约束下完成"小到中型"需求,遵循"先识别范围、再按需读文档、后实现、再验证、最后交付"的闭环。
|
||||
|
||||
该 skill 是默认开发入口,优先覆盖:
|
||||
- 单层内的新功能 MVP 实现
|
||||
- 小范围重构或代码整理
|
||||
- 文档同步更新
|
||||
- 编译错误定位与修复
|
||||
- 单个模块的 bug 修复或行为调整
|
||||
- podspec 小幅调整
|
||||
|
||||
默认吸收项目 `Doc/CODING_STYLE.md` 中的 Swift/iOS 通用规则;若任务重点落在并发、性能、可访问性或安全,再额外展开该专项的检查项。
|
||||
|
||||
## Core Execution Rules
|
||||
|
||||
- Rule 1 — Think Before Coding
|
||||
- 先核对代码和文档事实,不静默假设
|
||||
- 必须显式写出关键假设、主要取舍和已知风险
|
||||
- 遇到会改变实现路径的关键灰区,先暂停确认,不靠猜测继续推进
|
||||
- 若存在更简单且满足目标的实现路径,必须主动指出并优先采用
|
||||
- Rule 2 — Simplicity First
|
||||
- 只做满足需求和验收标准的最小实现
|
||||
- 不增加推测性功能,不为单次使用引入抽象
|
||||
- 若方案让资深工程师也会觉得过度设计,必须继续简化
|
||||
- Rule 3 — Surgical Changes
|
||||
- 只修改完成当前需求所必需的文件和代码
|
||||
- 不顺手重构无关代码,不扩散到相邻层做"顺便优化"
|
||||
- 非必要不改注释、格式或既有结构,新增代码保持现有风格
|
||||
- Rule 4 — Goal-Driven Execution
|
||||
- 先定义成功标准,再围绕成功标准实施和验证
|
||||
- 不给自己堆步骤,重点是闭环达到验收结果
|
||||
- 修改后必须验证,未验证通过前不能视为完成
|
||||
|
||||
## When To Use
|
||||
|
||||
当用户提出以下类型需求时使用:
|
||||
- "使用 start-sdk-feature-dev-lite 完成这个功能"
|
||||
- 在 ReadViewSDK 中开发单一功能或单层变更
|
||||
- 需要修复局部编译错误或行为问题
|
||||
- 需要同步更新文档
|
||||
- 需要小幅调整 podspec
|
||||
|
||||
不适用场景:
|
||||
- 需求跨多个 SDK 层级、涉及多阶段联调或需要完整项目级方案时,改用 `start-sdk-feature-dev`
|
||||
- 目标是只生成文档而不改业务代码时,优先使用文档类 skill
|
||||
- 目标是方案讨论而不进入实现时,优先使用 `discuss-sdk-feature-solution`
|
||||
|
||||
## Inputs
|
||||
|
||||
推荐输入:
|
||||
- 功能名称
|
||||
- 目标 / 用户价值
|
||||
- 范围(In Scope)
|
||||
- 非范围(Out of Scope)
|
||||
- 目标层级(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI)
|
||||
- 详细需求
|
||||
- 验收标准
|
||||
- 约束(兼容性 / 性能 / 禁止新增依赖 / 指定渲染路径)
|
||||
|
||||
若信息不足:
|
||||
- 先基于代码和文档补齐可发现事实
|
||||
- 再列最多 5 条关键假设
|
||||
- 基于假设继续推进 MVP,并在交付中标注待确认项
|
||||
|
||||
## Scope / Required Context
|
||||
|
||||
仅针对 ReadViewSDK 现有架构执行。
|
||||
|
||||
SDK 四层架构参照:
|
||||
- Layer 1 — EPUBCore(`Sources/RDReaderView/EPUBCore/`):EPUB 解析引擎
|
||||
- Layer 2 — EPUBTextRendering(`Sources/RDReaderView/EPUBTextRendering/`):文本渲染路径
|
||||
- Layer 3 — RDReaderView(`Sources/RDReaderView/` 根目录):分页阅读器容器
|
||||
- Layer 4 — EPUBUI(`Sources/RDReaderView/EPUBUI/`):开箱即用 UI
|
||||
- Legacy(`Sources/RDReaderView/LegacyRDReaderController/`):遗留代码,不在其上新增功能
|
||||
|
||||
分层依赖规则(上→下允许,下→上禁止):
|
||||
- EPUBUI → RDReaderView → EPUBTextRendering → EPUBCore
|
||||
|
||||
默认先读:
|
||||
1. `Doc/ARCHITECTURE.md` — 四层架构、数据流、分页模式、位置模型
|
||||
2. `Doc/CODING_STYLE.md` — 命名规范、分层规则、extension 拆分规则
|
||||
3. `Doc/EPUB_MAINTENANCE.md` — 文件职责表、渲染管线、排查场景
|
||||
|
||||
按需再读:
|
||||
- `Doc/FeatureSolution/*.md`(若有方案文档)
|
||||
- 涉及哪一层就读该层目录下的相关源码
|
||||
- 涉及 JS 桥接时:`Sources/RDReaderView/Resources/epub-bridge.js`
|
||||
- 涉及 podspec 时:`RDReaderView.podspec`
|
||||
|
||||
若文档与代码不一致:
|
||||
- 以代码事实为准完成本次实现
|
||||
- 在交付中标注不一致点,并指出建议更新的文档
|
||||
|
||||
## Required Workflow
|
||||
|
||||
### Phase 1 — 识别需求和范围
|
||||
|
||||
- 明确目标价值、影响层级(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI)、In Scope / Out of Scope
|
||||
- 优先做最小可落地实现,不做需求外重构
|
||||
- 若需求信息不足,先通过仓库事实补齐,再基于假设推进
|
||||
|
||||
### Phase 2 — 按需读取 Doc 与代码事实
|
||||
|
||||
- 先读默认 Doc(ARCHITECTURE / CODING_STYLE / EPUB_MAINTENANCE)
|
||||
- 根据需求类型补读对应层级的源码
|
||||
- 用源码确认真实入口、调用链和复用点,不只依赖文档
|
||||
|
||||
### Phase 3 — 形成最小实现方案
|
||||
|
||||
- 保持分层边界:
|
||||
- EPUB 解析逻辑在 EPUBCore
|
||||
- 文本渲染逻辑在 EPUBTextRendering
|
||||
- 分页容器逻辑在 RDReaderView
|
||||
- 开箱即用 UI 在 EPUBUI
|
||||
- 不在 Legacy 层新增功能
|
||||
- 命名、注释、日志风格遵循 `Doc/CODING_STYLE.md`
|
||||
- Swift / iOS 默认规则:
|
||||
- 新增 Swift 类型遵循 `RD` / `RDEPUB` 前缀和层级目录归属
|
||||
- 避免新增强制解包;确需使用时必须先收敛前置条件
|
||||
- 不为"现代化"而重写稳定代码;仅在本次需求明确受益时再迁移系统 API
|
||||
- 当前项目默认保持 UIKit + Auto Layout + 既有组件风格,SDK 层不使用 SnapKit
|
||||
- 注释和日志使用中文
|
||||
- Debug 输出放在 `#if DEBUG` 守卫下
|
||||
- 不在日志中输出敏感信息
|
||||
- 异步闭包默认 `[weak self]`,UI 更新回到主线程
|
||||
- 通知 / 定时器 / 回调在生命周期结束时必须清理
|
||||
- 数据模型用 `struct` + `Codable` + `Equatable`,服务对象用 `final class`
|
||||
- 可见性按最小原则:`private` > `fileprivate` > `internal` > `public`
|
||||
- 错误使用 `enum` + `LocalizedError`,禁止 force unwrap
|
||||
- 单文件若预计超过 600 行,需主动拆分扩展文件
|
||||
|
||||
### Phase 4 — 执行修改与文档同步
|
||||
|
||||
- 所有代码改动遵循最小改动原则
|
||||
- 修改代码时必须补充必要注释,重点说明关键逻辑、边界条件和不直观处理;不要省略应有注释,也不要添加无信息量的描述性注释
|
||||
- 默认补做轻量 SDK 自检:
|
||||
- 新增类型是否使用正确前缀和层级归属
|
||||
- 本次改动涉及的渲染路径是否正常工作
|
||||
- Public API 是否有意外 breaking change
|
||||
- podspec 是否需要更新(新增文件时)
|
||||
- 生命周期、通知/定时器/回调清理是否完整
|
||||
- 异步闭包是否考虑 `[weak self]`
|
||||
- 修改代码后必须同步更新受影响文档,不能只停留在代码实现
|
||||
- 若本次改动涉及 EPUB 维护相关,必须回写 `Doc/EPUB_MAINTENANCE.md`
|
||||
- 若本次是方案讨论文档交接落地,方案类文档统一放到 `Doc/FeatureSolution/`
|
||||
- 若新增、重命名或移动文档,必须同步更新目录索引
|
||||
|
||||
### Phase 5 — 构建验证与交付
|
||||
|
||||
- 完成修改后,按项目默认命令执行编译验证
|
||||
- 若出现编译错误,自动修复并重编译
|
||||
- 若遇构建锁问题,自动重试
|
||||
- 交付时固定输出:
|
||||
- A. 需求理解
|
||||
- B. 开发计划
|
||||
- C. 开发实施
|
||||
- D. 验证结果
|
||||
- E. 交付摘要
|
||||
|
||||
## Output Contract
|
||||
|
||||
最终回复必须完整输出以下 5 个板块,标题保持一致:
|
||||
- A. 需求理解
|
||||
- B. 开发计划
|
||||
- C. 开发实施
|
||||
- D. 验证结果
|
||||
- E. 交付摘要
|
||||
|
||||
各板块内容要求:
|
||||
- A:3-6 条,覆盖目标价值、范围、目标层级、限制
|
||||
- B:按"方案对齐 / MVP 实现 / 验证与交付"三阶段组织
|
||||
- C:描述实际改动、关键实现取舍,以及本次补充了哪些关键代码注释
|
||||
- D:给出构建结果、关键路径验证、未覆盖风险
|
||||
- E:列出改动文件、关键取舍、已同步文档与具体文档落点、后续建议
|
||||
|
||||
## Validation
|
||||
|
||||
若本次调用修改了任何 Swift / Objective-C / 工程配置 / podspec 文件,必须执行构建验证。
|
||||
|
||||
验证方式:
|
||||
- 若 Demo 工程可用:
|
||||
```bash
|
||||
xcodebuild build -workspace ReadViewSDKDemo/ReadViewSDKDemo.xcworkspace -scheme ReadViewSDKDemo -sdk iphonesimulator -derivedDataPath /private/tmp/readview-sdk-derived
|
||||
```
|
||||
- 若仅有 SDK 源码(无 workspace):
|
||||
```bash
|
||||
pod lib lint RDReaderView.podspec --allow-warnings
|
||||
```
|
||||
|
||||
验证规则:
|
||||
- 编译失败时必须自行修复并重试
|
||||
- 遇到 `database is locked` 等锁问题时自动重试,建议最多 5 次
|
||||
- 直到 `BUILD SUCCEEDED` 或 `pod lib lint passed` 才可交付
|
||||
- 如果本次仅修改文档或 skill 文件,可跳过构建,但要在交付中明确说明原因
|
||||
|
||||
## Maintenance Rules
|
||||
|
||||
- 优先复用 `Doc/`,不要把项目级规则复制进多个 skill 造成双份维护
|
||||
- 新增项目约束时,优先更新 `Doc/CODING_STYLE.md`、`Doc/ARCHITECTURE.md` 等主文档,再调整 skill
|
||||
- 轻量与完整要保持"轻重不同、规则不冲突",其中完整版应在轻量版闭环基础上扩展跨层与联调要求,而不是另起一套风格
|
||||
- 变更默认流程、输出格式或适用场景时,必须同步更新相关文档
|
||||
- 本 skill 持续作为默认开发入口,保持"轻量、清晰、可直接执行"
|
||||
- SDK 四层架构是执行的核心参照框架,单层改动也必须明确标注落点层级
|
||||
@@ -1,309 +0,0 @@
|
||||
---
|
||||
name: "Start SDK Feature Dev"
|
||||
description: "完整开发主控 skill:用于 ReadViewSDK 的跨层功能开发、多阶段联调、结构性重构与完整交付。"
|
||||
argument-hint: "粘贴完整需求,建议包含:背景、目标、范围、目标层级、交互说明、数据约束、验收标准、渲染路径覆盖要求。"
|
||||
---
|
||||
|
||||
# Start SDK Feature Dev
|
||||
|
||||
## Purpose
|
||||
|
||||
面向 ReadViewSDK 的完整开发主控 skill。
|
||||
用于复杂度高于单层修改的需求,遵循"先识别范围、再按需读文档、后对齐既定开发计划、再严格执行、再验证、最后交付"的闭环,并补充跨层联调、风险控制、回滚点和完整交付要求。
|
||||
|
||||
该 skill 优先覆盖:
|
||||
- 跨多个 SDK 层级(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI)的功能开发
|
||||
- 多阶段联调与完整交付
|
||||
- 结构性重构或系统性收敛
|
||||
- 涉及 EPUB 解析、渲染管线、阅读器容器、UI 层协同改造的需求
|
||||
- 需要更完整风险说明、文档同步和验收闭环的开发任务
|
||||
|
||||
默认吸收项目 `Doc/CODING_STYLE.md` 中的 Swift/iOS 通用规则;若任务重点落在并发、性能、可访问性或安全,再额外展开该专项的检查项。
|
||||
|
||||
## Core Execution Rules
|
||||
|
||||
- Rule 1 — Think Before Coding
|
||||
- 先核对代码、文档和计划事实,不静默假设
|
||||
- 必须显式写出关键假设、主要取舍和已知风险
|
||||
- 遇到会改变计划或实现路径的关键灰区,先暂停确认,不靠猜测继续推进
|
||||
- 若存在更简单且满足目标的实现路径,只能作为建议提出;有既定计划时不得自行改用
|
||||
- Rule 2 — Simplicity First
|
||||
- 只做满足需求、计划和验收标准的最小实现
|
||||
- 不增加推测性功能,不为单次使用引入抽象
|
||||
- 若方案让资深工程师也会觉得过度设计,必须继续简化或回到计划边界内
|
||||
- Rule 3 — Surgical Changes
|
||||
- 只修改完成当前需求和计划项所必需的文件与代码
|
||||
- 不顺手重构无关代码,不扩散到相邻层做"顺便优化"
|
||||
- 非必要不改注释、格式或既有结构,新增代码保持现有风格
|
||||
- Rule 4 — Goal-Driven Execution
|
||||
- 先定义成功标准,再围绕成功标准实施和验证
|
||||
- 计划只是约束,不是目标本身;目标是按计划完成验收闭环
|
||||
- 修改后必须验证,未验证通过前不能视为完成
|
||||
|
||||
## Plan Execution Rule
|
||||
|
||||
若用户提供了开发计划文档、方案文档或明确引用 `Doc/FeatureSolution/*.md` 中的开发方案,本 skill 必须把该文档视为本次实现的主计划来源。
|
||||
|
||||
严格执行规则:
|
||||
- 必须先完整读取用户指定的开发计划文档,再开始代码修改
|
||||
- 必须从计划中抽取任务清单、文件清单、实现边界、非范围和验收标准
|
||||
- 必须按计划逐项实现,不得自行更换方案、合并步骤、替换文件布局、改变数据模型设计或引入计划外抽象
|
||||
- 必须遵守计划中的"不做 / 不新增 / 不使用 / 暂不实现"等限制项
|
||||
- 若计划与代码事实冲突,或计划中某项无法直接落地,必须暂停并向用户说明冲突点、影响和可选处理方式;不得自行选择替代方案继续实现
|
||||
- 若发现更优实现方式,只能作为"建议"在交付或暂停说明中提出,不得在本次实现中自由采用
|
||||
- 若计划未覆盖某个必要细节,只允许做最小补齐;补齐内容必须在交付中明确标注为"计划未写明,按最小必要实现补齐"
|
||||
- 若用户要求"严格按照开发计划执行 / 不要自由发挥",必须把偏离计划视为阻塞项处理
|
||||
|
||||
## When To Use
|
||||
|
||||
当用户提出以下类型需求时使用:
|
||||
- "使用 start-sdk-feature-dev 实现这个需求"
|
||||
- 一个需求影响多个 SDK 层级(跨 EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI)
|
||||
- 需要完整计划、实现、联调、回归和文档交付
|
||||
- 需要跨 EPUB 解析、渲染、阅读器容器、UI 的协同改造
|
||||
- 需要明确阶段性计划、回滚点和完整验收说明
|
||||
|
||||
Lite 与 Full 的边界:
|
||||
- 轻量开发 skill:单层、小中型、MVP 优先
|
||||
- 本 skill(start-sdk-feature-dev):跨层、复杂联调、需要更完整方案和验收
|
||||
|
||||
## Inputs
|
||||
|
||||
推荐输入模板:
|
||||
- 功能名称
|
||||
- 背景与目标
|
||||
- 用户故事
|
||||
- 详细需求
|
||||
- 非范围
|
||||
- 目标层级(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI / Legacy)
|
||||
- 交互说明(含空态 / 错误态 / 加载态)
|
||||
- 渲染路径覆盖要求(webFixedLayout / webInteractive / textReflowable / 不限)
|
||||
- 数据与接口约束
|
||||
- 验收标准(Given-When-Then)
|
||||
- 兼容性要求(Public API 是否 breaking、podspec 变更等)
|
||||
- 性能与安全要求
|
||||
- 发布时间或优先级
|
||||
|
||||
若信息不足:
|
||||
- 先基于代码和文档补齐可发现事实
|
||||
- 再列最多 5 条关键假设
|
||||
- 若没有既定开发计划,可基于假设继续推进最小可落地实现,并在交付中标注待确认项
|
||||
- 若已有开发计划,信息不足时不得自行扩展方案;只能做计划内实现或暂停确认
|
||||
|
||||
## Scope / Required Context
|
||||
|
||||
仅针对 ReadViewSDK 现有架构执行。
|
||||
|
||||
SDK 四层架构参照:
|
||||
- Layer 1 — EPUBCore(`Sources/RDReaderView/EPUBCore/`):EPUB 解析引擎,ZIP 解压、OPF 解析、manifest/spine/TOC、资源 URL 解析、离屏 WKWebView 分页、阅读会话状态机、JS 桥接
|
||||
- Layer 2 — EPUBTextRendering(`Sources/RDReaderView/EPUBTextRendering/`):文本渲染路径,DTCoreText 转 NSAttributedString 后按字符范围分页
|
||||
- Layer 3 — RDReaderView(`Sources/RDReaderView/` 根目录 6 个文件):分页阅读器容器 UIView,支持 pageCurl / horizontalScroll / verticalScroll / horizontalCoverScroll 四种模式
|
||||
- Layer 4 — EPUBUI(`Sources/RDReaderView/EPUBUI/`):开箱即用 UI,工具栏、目录面板、高亮管理、设置面板、阅读位置持久化
|
||||
- Legacy(`Sources/RDReaderView/LegacyRDReaderController/`):遗留代码,不在其上新增功能
|
||||
|
||||
分层依赖规则:
|
||||
- 上层可依赖下层,下层不得依赖上层
|
||||
- EPUBUI 可依赖 RDReaderView、EPUBTextRendering、EPUBCore
|
||||
- RDReaderView 可依赖 EPUBTextRendering、EPUBCore
|
||||
- EPUBTextRendering 可依赖 EPUBCore
|
||||
- EPUBCore 不依赖任何上层
|
||||
|
||||
若确需跨层改动:
|
||||
- 必须在计划中写明影响范围
|
||||
- 必须在交付中写明回滚点或回退策略
|
||||
|
||||
默认先读:
|
||||
1. `Doc/ARCHITECTURE.md` — 四层架构、数据流、分页模式、位置模型、已知限制
|
||||
2. `Doc/CODING_STYLE.md` — 命名规范(RD/RDEPUB 前缀)、分层规则、extension 拆分、SS→RD 迁移计划
|
||||
3. `Doc/EPUB_MAINTENANCE.md` — 文件职责表、DTCoreText 渲染管线、常见排查场景
|
||||
|
||||
按需再读:
|
||||
- `Doc/FeatureSolution/*.md`(若有方案文档)
|
||||
- 涉及 EPUB 解析时:`Sources/RDReaderView/EPUBCore/` 下相关文件
|
||||
- 涉及文本渲染时:`Sources/RDReaderView/EPUBTextRendering/` 下相关文件
|
||||
- 涉及阅读器容器时:`Sources/RDReaderView/RDReaderView.swift` 及同级文件
|
||||
- 涉及 UI 层时:`Sources/RDReaderView/EPUBUI/` 下相关文件
|
||||
- 涉及遗留代码时:`Sources/RDReaderView/LegacyRDReaderController/` 下相关文件
|
||||
- 涉及 JS 桥接时:`Sources/RDReaderView/Resources/epub-bridge.js`
|
||||
- 涉及 podspec 时:`RDReaderView.podspec`
|
||||
|
||||
若文档与代码不一致:
|
||||
- 以代码事实为准落地
|
||||
- 在交付中说明不一致点和建议更新的文档项
|
||||
|
||||
若开发计划文档与代码不一致:
|
||||
- 不得直接以代码事实替换计划继续开发
|
||||
- 必须先判断不一致是否影响计划执行
|
||||
- 会影响计划执行时,暂停并向用户说明冲突点和建议调整项
|
||||
- 不影响计划执行时,按计划继续,并在交付中标注该不一致点
|
||||
|
||||
## SDK-Specific Execution Rules
|
||||
|
||||
### 命名与可见性
|
||||
- 所有新增类型必须使用 `RD` 前缀,EPUB 相关使用 `RDEPUB` 前缀
|
||||
- 新增类型归属正确层级目录,不得随意放置
|
||||
- 可见性按最小原则:`private` > `fileprivate` > `internal` > `public`
|
||||
- 数据模型用 `struct` + `Codable` + `Equatable`
|
||||
- 服务对象用 `final class`
|
||||
|
||||
### 代码组织
|
||||
- 单文件超过 600 行需主动拆分 `TypeName+Feature.swift` 扩展文件
|
||||
- 大型 Controller 超过 1000 行必须拆分
|
||||
- extension 文件承担独立子职责,不只做"行数搬运"
|
||||
|
||||
### 内存与并发
|
||||
- 异步闭包默认 `[weak self]`
|
||||
- UI 更新必须回到主线程
|
||||
- 通知 / 定时器 / 回调在 deinit 或生命周期结束时必须清理
|
||||
|
||||
### 渲染路径覆盖
|
||||
- 新增功能必须评估对三种渲染路径的影响:
|
||||
- `webFixedLayout`:固定版式 EPUB(漫画、绘本)— WKWebView 渲染
|
||||
- `webInteractive`:可重排 + 交互脚本 EPUB — WKWebView 渲染
|
||||
- `textReflowable`:纯文本可重排 EPUB — DTCoreText 渲染
|
||||
- 若功能仅适用于部分路径,必须在交付中明确标注适用范围和不适用路径的处理方式
|
||||
|
||||
### Public API 兼容性
|
||||
- 涉及公共接口(`RDReaderView`、`RDReaderDataSource`、`RDURLReaderController`、`RDEPUBReaderController` 等)的改动,必须评估 breaking change
|
||||
- 若存在 breaking change,必须在交付中标注并给出迁移指引
|
||||
- 新增 public API 需考虑 Objective-C 互操作(`@objc`、`@objcMembers`)
|
||||
|
||||
### CocoaPods 发布影响
|
||||
- 新增源文件:确认 podspec `source_files` glob 是否已覆盖
|
||||
- 新增资源文件:确认 podspec `resource` / `resource_bundles` 是否已覆盖
|
||||
- 新增依赖:确认 podspec `dependency` 是否已声明,评估对宿主 App 的依赖传递影响
|
||||
- 模块目录结构调整:必须同步更新 podspec
|
||||
|
||||
### JS 桥接
|
||||
- 涉及 `epub-bridge.js` 的改动,必须确认 JS ↔ Swift 消息契约的一致性
|
||||
- JS 侧新增消息类型时,Swift 侧必须有对应处理分支
|
||||
- 修改已有消息类型时,必须评估向后兼容
|
||||
|
||||
### 错误处理
|
||||
- 使用 `enum` + `LocalizedError` 定义错误类型
|
||||
- 禁止 force unwrap(`!`),确需使用时必须先收敛前置条件
|
||||
- 关键路径的错误必须向调用方传递,不得静默吞掉
|
||||
|
||||
## Required Workflow
|
||||
|
||||
### Phase 1 — 识别需求和范围
|
||||
|
||||
- 明确目标价值、影响层级(EPUBCore / EPUBTextRendering / RDReaderView / EPUBUI / Legacy)、In Scope / Out of Scope
|
||||
- 判断是否涉及跨层、跨渲染路径、跨模块联调
|
||||
- 若用户提供开发计划文档,先确认本次执行的唯一计划来源,并提取计划任务清单
|
||||
- 优先做计划内最小可落地实现,不做需求外重构
|
||||
- 若需求信息不足,先通过仓库事实补齐,再基于假设推进
|
||||
|
||||
### Phase 2 — 按需读取 Doc 与代码事实
|
||||
|
||||
- 若用户指定开发计划文档,必须先完整读取该文档
|
||||
- 先读默认 Doc(ARCHITECTURE / CODING_STYLE / EPUB_MAINTENANCE)
|
||||
- 根据需求类型补读各层源码和相关文档
|
||||
- 用源码确认真实入口、调用链、数据流、状态流和复用点,不只依赖文档
|
||||
|
||||
### Phase 3 — 对齐并锁定开发计划
|
||||
|
||||
- 若已有开发计划,必须按计划锁定实现路径,不重新设计方案
|
||||
- 若没有开发计划,才允许形成最小可落地方案:先复用现有模式与能力,再考虑新增抽象
|
||||
- 保持分层边界:
|
||||
- EPUB 解析逻辑在 EPUBCore
|
||||
- 文本渲染逻辑在 EPUBTextRendering
|
||||
- 分页容器逻辑在 RDReaderView
|
||||
- 开箱即用 UI 在 EPUBUI
|
||||
- 不在 Legacy 层新增功能
|
||||
- 命名、注释、日志风格遵循 `Doc/CODING_STYLE.md`
|
||||
- Swift / iOS 默认规则:
|
||||
- 新增 Swift 类型遵循 `RD` / `RDEPUB` 前缀和层级目录归属
|
||||
- 避免新增强制解包;确需使用时必须先收敛前置条件
|
||||
- 不为"现代化"而重写稳定代码;仅在本次需求明确受益时再迁移系统 API
|
||||
- 当前项目默认保持 UIKit + Auto Layout + 既有组件风格,SDK 层不使用 SnapKit
|
||||
- 注释和日志使用中文
|
||||
- Debug 输出放在 `#if DEBUG` 守卫下
|
||||
- 不在日志中输出敏感信息
|
||||
- 单文件预计超过 600 行需主动拆分,超过 1000 行必须拆分
|
||||
- 若为跨层需求,计划中必须写清:
|
||||
- 主改动层级
|
||||
- 被影响层级
|
||||
- 渲染路径覆盖范围
|
||||
- 联调依赖点
|
||||
- 关键风险
|
||||
- 回滚点或降级方式
|
||||
- 开始编码前必须形成内部执行清单,清单项必须能追溯到开发计划;计划外项只能标记为"必要补齐"或"待确认",不能直接实施
|
||||
|
||||
### Phase 4 — 执行修改与文档同步
|
||||
|
||||
- 所有代码改动遵循计划内最小改动原则
|
||||
- 严格按开发计划清单逐项修改;不得临时改换实现方式或增加计划外功能
|
||||
- 若执行中需要偏离计划,必须暂停确认,不能先改后说明
|
||||
- 修改代码时必须补充必要注释,重点说明关键逻辑、边界条件和不直观处理
|
||||
- 默认补做 SDK 自检:
|
||||
- 检查新增类型是否使用正确前缀和层级归属
|
||||
- 检查三种渲染路径的覆盖情况
|
||||
- 检查 Public API 是否有意外 breaking change
|
||||
- 检查 podspec 是否需要更新
|
||||
- 检查 JS 桥接消息契约是否一致
|
||||
- 关键页面或组件检查生命周期、通知/定时器/回调清理是否完整
|
||||
- 新增 UI 检查长文本、图标按钮语义、重要状态是否只靠颜色表达
|
||||
- 异步闭包默认考虑 `[weak self]`,UI 更新回到主线程
|
||||
- 修改代码后必须同步更新受影响文档,不能只停留在代码实现
|
||||
- 若本次改动涉及 EPUB 维护相关,必须回写 `Doc/EPUB_MAINTENANCE.md`
|
||||
- 若本次是方案讨论文档交接落地,方案类文档统一放到 `Doc/FeatureSolution/`
|
||||
- 若新增、重命名或移动文档,必须同步更新目录索引
|
||||
|
||||
### Phase 5 — 构建验证与交付
|
||||
|
||||
- 完成修改后,按项目默认命令执行编译验证
|
||||
- 若出现编译错误,自动修复并重编译
|
||||
- 若遇构建锁问题,自动重试(建议最多 5 次)
|
||||
- 若是跨层需求,除构建外还应补充关键联调路径说明
|
||||
- 交付时固定输出:
|
||||
- A. 需求理解
|
||||
- B. 开发计划
|
||||
- C. 开发实施
|
||||
- D. 验证结果
|
||||
- E. 交付摘要
|
||||
|
||||
## Output Contract
|
||||
|
||||
最终回复必须完整输出以下 5 个固定板块:
|
||||
- A. 需求理解
|
||||
- B. 开发计划
|
||||
- C. 开发实施
|
||||
- D. 验证结果
|
||||
- E. 交付摘要
|
||||
|
||||
各板块内容要求:
|
||||
- A:3-8 条,覆盖目标价值、范围、目标层级、渲染路径覆盖、交互流、限制
|
||||
- B:按"计划来源 / 计划任务清单 / 执行顺序 / 验证与交付"组织;若有开发计划文档,必须逐条对应计划项
|
||||
- C:描述实现过程、复用点、关键取舍,以及本次补充了哪些关键代码注释;若有计划未写明但必须补齐的内容,必须单独标注
|
||||
- D:给出编译结果、关键路径验证、自动重试/自动修复情况、未覆盖风险
|
||||
- E:列出改动文件、计划符合情况、关键取舍、回滚点或降级方式、已同步文档与具体文档落点、后续建议
|
||||
|
||||
## Validation
|
||||
|
||||
若本次调用修改了任何 Swift / Objective-C / 工程配置 / podspec 文件,必须执行构建验证。
|
||||
|
||||
验证方式:
|
||||
- 若 Demo 工程可用:
|
||||
```bash
|
||||
xcodebuild build -workspace ReadViewSDKDemo/ReadViewSDKDemo.xcworkspace -scheme ReadViewSDKDemo -sdk iphonesimulator -derivedDataPath /private/tmp/readview-sdk-derived
|
||||
```
|
||||
- 若仅有 SDK 源码(无 workspace):
|
||||
```bash
|
||||
pod lib lint RDReaderView.podspec --allow-warnings
|
||||
```
|
||||
|
||||
验证规则:
|
||||
- 若出现编译错误,必须自动修复并重编译
|
||||
- 若遇到 `database is locked` 等构建锁问题,自动重试,建议最多 5 次
|
||||
- 直到 `BUILD SUCCEEDED` 或 `pod lib lint passed` 才可进入交付阶段
|
||||
- 若本次只改文档或 skill 文件,可跳过编译,但要在交付中明确说明
|
||||
|
||||
## Maintenance Rules
|
||||
|
||||
- 优先复用 `Doc/`,不要把项目级规则复制进多个 skill 造成双份维护
|
||||
- 新增项目约束时,优先更新 `Doc/CODING_STYLE.md`、`Doc/ARCHITECTURE.md` 等主文档,再调整 skill
|
||||
- 轻量与完整要保持"轻重不同、规则不冲突",其中完整版应在轻量版闭环基础上扩展跨层与联调要求,而不是另起一套风格
|
||||
- 变更默认流程、输出格式或适用边界时,必须同步更新相关文档
|
||||
- 本 skill 持续作为完整开发入口,保持"可执行、可联调、可回滚、可交付"
|
||||
- SDK 四层架构(EPUBCore → EPUBTextRendering → RDReaderView → EPUBUI)是执行的核心参照框架,所有改动必须明确标注落点层级和依赖方向
|
||||
+33
@@ -6,3 +6,36 @@ xcuserdata/
|
||||
.artifacts/
|
||||
.deriveddata/
|
||||
.swift-module-cache/
|
||||
ReadViewDemo/build/
|
||||
|
||||
# This repository checks in the Demo Pods project. Keep generated files for the
|
||||
# local RDEpubReaderView/RDPDFReaderView pods and vendored SnapKit visible even
|
||||
# when a global gitignore excludes Pods.
|
||||
!ReadViewDemo/Pods/
|
||||
!ReadViewDemo/Pods/Local Podspecs/
|
||||
!ReadViewDemo/Pods/Local Podspecs/RDEpubReaderView.podspec.json
|
||||
!ReadViewDemo/Pods/Local Podspecs/RDPDFReaderView.podspec.json
|
||||
!ReadViewDemo/Pods/Target Support Files/
|
||||
!ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/
|
||||
!ReadViewDemo/Pods/Target Support Files/RDEpubReaderView/**
|
||||
!ReadViewDemo/Pods/Target Support Files/RDPDFReaderView/
|
||||
!ReadViewDemo/Pods/Target Support Files/RDPDFReaderView/**
|
||||
!ReadViewDemo/Pods/SnapKit/
|
||||
!ReadViewDemo/Pods/SnapKit/**
|
||||
!ReadViewDemo/Pods/Target Support Files/SnapKit/
|
||||
!ReadViewDemo/Pods/Target Support Files/SnapKit/**
|
||||
|
||||
# macOS system files
|
||||
.DS_Store
|
||||
|
||||
# Local backup directories
|
||||
*手动解包备份*
|
||||
|
||||
# AI tool output
|
||||
_ssoft-output/
|
||||
.claude/*
|
||||
!.claude/skills
|
||||
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -1,3 +1,67 @@
|
||||
# AGENTS.md
|
||||
# ReadViewSDK AI 开发规则
|
||||
|
||||
- If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.
|
||||
## 适用范围与语言
|
||||
|
||||
- 默认处理当前仓库的 EPUB SDK、PDF SDK、Demo 和测试。
|
||||
- 计划、交付说明、`Doc/` 与项目 Markdown 使用中文。
|
||||
- 代码标识符、文件名、API 和配置键保持英文;代码注释、Docstring 和提交信息使用中文。
|
||||
- 只做当前目标所需的最小改动,保留工作区内与任务无关的未提交修改。
|
||||
- 任何写文件的 agent 必须先读取 `CONTEXT.md`。
|
||||
|
||||
## 自动选择项目 Skill
|
||||
|
||||
根据用户意图选择 `.agents/skills/` 中最匹配的 Skill:
|
||||
|
||||
- 先讨论、比较方案、不要改代码:`discuss-sdk-feature-solution`
|
||||
- 单模块功能、Bug 修复、UI 调整、局部重构、构建修复:`start-sdk-feature-dev-lite`
|
||||
- 跨层/跨阅读器改造、公共 API 或持久化迁移、复杂联调、完整方案执行:`start-sdk-feature-dev`
|
||||
- 隐藏问题、并发、生命周期、性能、内存、缓存或兼容性审查:`review-swift-ios-sdk`
|
||||
- 全局代码规范:`generate-sdk-code-style-doc`
|
||||
- 单模块代码规范:`generate-module-code-style-doc`
|
||||
- 模块调用链、状态流、分页或缓存实现逻辑:`generate-module-implementation-logic`
|
||||
- 审查改动、生成详细中文提交说明并提交或推送 Git:`commit-git-changes`
|
||||
|
||||
不要为简单任务叠加多个 Skill。用户明确指定 Skill 时优先使用;任务边界扩大时再切换或补充,并说明原因。
|
||||
|
||||
## 文档按需读取
|
||||
|
||||
先定位源码,再读取直接相关文档,不默认全文加载整个 `Doc/`:
|
||||
|
||||
| 任务 | 必读资料 |
|
||||
| --- | --- |
|
||||
| 任意代码修改 | `CONTEXT.md`、`Doc/CONVENTIONS.md` 相关章节 |
|
||||
| 分层或跨模块改造 | `Doc/ARCHITECTURE.md` |
|
||||
| 公共 API | `Doc/API_REFERENCE.md` 与真实 public 声明 |
|
||||
| EPUB 解析/排版/分页/UI | 对应 `Doc/*_CODE_REFERENCE.md` 或专题文档 |
|
||||
| PDF 阅读器 | `Sources/RDPDFReaderView` 源码;现有文档不足时以源码为准 |
|
||||
| 测试与回归 | `Doc/TESTING.md` |
|
||||
| 用户指定方案 | 指定文档,必须完整读取 |
|
||||
|
||||
`Doc/index.md` 用于路由。文档与源码冲突时以当前源码为事实,并指出待同步项。
|
||||
|
||||
## 模块边界
|
||||
|
||||
- EPUB:`EPUBCore` → `EPUBTextRendering` → `ReaderView` → `EPUBUI`,禁止下层反向依赖上层。
|
||||
- PDF:通用分页容器位于 `Sources/RDPDFReaderView/ReaderView`,成品能力位于 `Sources/RDPDFReaderView/Sources`。
|
||||
- Demo 和 UITests 只承担集成、样例与回归,不承载 SDK 核心业务。
|
||||
- 保持 UIKit、SnapKit 和当前 CocoaPods 结构;新增依赖、资源或系统 framework 时同步检查对应 podspec。
|
||||
- 新增类型遵循现有 `RD`、`RDEPUB`、`RDPDF` 前缀和最小可见性。
|
||||
|
||||
## SDK 兼容与质量
|
||||
|
||||
- 修改 public API 前评估宿主源码兼容、默认行为和迁移方式。
|
||||
- 修改 Codable、持久化文件、缓存键或 book identifier 时提供版本与失效/迁移策略。
|
||||
- UI 更新回主线程;异步任务处理取消、超时、迟到结果、恰好一次完成和生命周期。
|
||||
- EPUB 改动检查 `.textReflowable`、`.webInteractive`、`.webFixedLayout` 的影响。
|
||||
- PDF 改动检查宿主图片 Provider 与内置 PDFKit Provider 的影响。
|
||||
- 滚动、缩放、分页和绘画热点避免同步 IO、重复解码、无界缓存和大对象长期驻留。
|
||||
|
||||
## 验证
|
||||
|
||||
修改 Swift、工程配置或 podspec 后默认执行:
|
||||
|
||||
`xcodebuild -workspace ReadViewDemo/ReadViewDemo.xcworkspace -scheme ReadViewDemo -configuration Debug -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO build`
|
||||
|
||||
按风险补充 `Doc/TESTING.md` 中的定向 UI 测试。仅修改文档、规则或 Skill 时可跳过 Xcode 构建,但必须执行结构、引用和格式检查。
|
||||
|
||||
如果使用 XcodeBuildMCP,调用其工具前必须先读取已安装的 XcodeBuildMCP Skill,并先检查 session defaults。
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# ReadViewSDK 代码审查报告
|
||||
|
||||
> 审查日期:2026-06-26
|
||||
> 审查范围:Sources/RDReaderView 全部源码
|
||||
> 审查范围:Sources/RDEpubReaderView 全部源码
|
||||
> 审查方法:逐文件阅读 + 交叉验证 + 线程模型分析
|
||||
|
||||
---
|
||||
@@ -148,4 +148,4 @@ func currentTextPageSize() -> CGSize {
|
||||
| 🟡 P2 | P2-2 | javaScriptStringLiteral 实现依赖隐式假设 | 可维护性 |
|
||||
| 🔵 P3 | P3-1 | 后台线程同步 hop 主线程获取布局信息 | 线程模型 |
|
||||
|
||||
**整体评价**: SDK 架构设计良好,模块分层清晰,引用管理(unowned/weak)使用正确,分页取消机制有 `paginationToken` + `cancellationController` 兜底。主要值得修复的是三个性能类 P1 问题(主线程阻塞搜索、资源加载阻塞、缓存无清理),两个 P2 鲁棒性/可维护性问题,以及一个 P3 线程模型优化建议。
|
||||
**整体评价**: SDK 架构设计良好,模块分层清晰,引用管理(unowned/weak)使用正确,分页取消机制有 `paginationToken` + `cancellationController` 兜底。主要值得修复的是三个性能类 P1 问题(主线程阻塞搜索、资源加载阻塞、缓存无清理),两个 P2 鲁棒性/可维护性问题,以及一个 P3 线程模型优化建议。
|
||||
|
||||
+34
-30
@@ -8,7 +8,7 @@
|
||||
|
||||
## 1. RDEPUBReaderController — 入口控制器
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift` 及扩展
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift` 及扩展
|
||||
|
||||
### 1.1 初始化
|
||||
|
||||
@@ -180,7 +180,7 @@ func nativeTextSemanticSummary() -> String?
|
||||
|
||||
## 2. RDEPUBReaderDelegate — 委托协议
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBUI/RDEPUBReaderDelegate.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderDelegate.swift`
|
||||
|
||||
所有方法均有默认空实现,可按需实现。
|
||||
|
||||
@@ -232,7 +232,7 @@ public protocol RDEPUBReaderDelegate: AnyObject {
|
||||
|
||||
## 3. RDEPUBReaderPersistence — 持久化协议
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
||||
|
||||
```swift
|
||||
public protocol RDEPUBReaderPersistence: AnyObject {
|
||||
@@ -251,20 +251,24 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
||||
}
|
||||
```
|
||||
|
||||
默认实现 `RDEPUBUserDefaultsPersistence` 使用 UserDefaults 存储:
|
||||
默认实现 `RDEPUBUserDefaultsPersistence` 将阅读位置和全局设置存入 UserDefaults;
|
||||
书签与高亮(可能含书籍正文摘录)存入带文件保护且排除备份的 Application Support 文件。
|
||||
升级后的首次读取会自动迁移历史 UserDefaults 数据,并仅在新副本成功落盘后删除旧副本。
|
||||
不同的 `UserDefaults` 容器会自动使用独立的文件命名空间,避免多账号或测试 suite
|
||||
之间混用同一本书的标注;标准容器保留对旧版未分区文件的只读兼容。
|
||||
|
||||
| 数据 | Key 格式 |
|
||||
|------|----------|
|
||||
| 阅读位置 | `ssreader.epub.location.{bookID}` |
|
||||
| 书签 | `ssreader.epub.bookmarks.{bookID}` |
|
||||
| 高亮 | `ssreader.epub.highlights.{bookID}` |
|
||||
| 书签 | 受保护文件(历史键:`ssreader.epub.bookmarks.{bookID}`) |
|
||||
| 高亮 | 受保护文件(历史键:`ssreader.epub.highlights.{bookID}`) |
|
||||
| 全局设置 | `ssreader.epub.settings` |
|
||||
|
||||
---
|
||||
|
||||
## 4. RDEPUBReaderConfiguration — 配置模型
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift`
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
@@ -273,7 +277,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
||||
| `fontChoice` | `RDEPUBReaderFontChoice` | `.system` | 字体选择(system/serif/rounded/monospaced) |
|
||||
| `numberOfColumns` | `Int` | `1` | 每页列数(1 或 2) |
|
||||
| `columnGap` | `CGFloat` | `20` | 列间距 |
|
||||
| `displayType` | `RDReaderView.DisplayType` | `.pageCurl` | 翻页模式 |
|
||||
| `displayType` | `RDEpubReaderView.DisplayType` | `.pageCurl` | 翻页模式 |
|
||||
| `landscapeDualPageEnabled` | `Bool` | `true` | 横屏双页 |
|
||||
| `showsTableOfContents` | `Bool` | `true` | 显示目录 |
|
||||
| `allowsHighlights` | `Bool` | `true` | 允许高亮 |
|
||||
@@ -301,50 +305,50 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
||||
|
||||
## 5. 翻页容器协议
|
||||
|
||||
**文件**:`Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/ReaderView/RDEpubReaderViewProtocols.swift`
|
||||
|
||||
### 5.1 RDReaderPageProvider(推荐)
|
||||
### 5.1 RDEpubReaderPageProvider(推荐)
|
||||
|
||||
```swift
|
||||
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
|
||||
@objc public protocol RDEpubReaderPageProvider: NSObjectProtocol {
|
||||
|
||||
/// 总页数
|
||||
func numberOfPages(in readerView: RDReaderView) -> Int
|
||||
func numberOfPages(in readerView: RDEpubReaderView) -> Int
|
||||
|
||||
/// 返回指定页的视图
|
||||
/// - Parameters:
|
||||
/// - index: 页码索引(0-based)
|
||||
/// - reusableView: 可复用的旧视图(可能为 nil)
|
||||
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||
func readerView(_ readerView: RDEpubReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||
|
||||
/// 页面标识符(用于缓存去重)
|
||||
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
|
||||
@objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String?
|
||||
|
||||
/// 顶部工具栏视图
|
||||
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
|
||||
@objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||
|
||||
/// 底部工具栏视图
|
||||
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
|
||||
@objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 RDReaderDelegate
|
||||
### 5.2 RDEpubReaderDelegate
|
||||
|
||||
```swift
|
||||
@objc public protocol RDReaderDelegate: NSObjectProtocol {
|
||||
@objc public protocol RDEpubReaderDelegate: NSObjectProtocol {
|
||||
|
||||
/// 页面变化回调
|
||||
func pageNum(readerView: RDReaderView, pageNum: Int)
|
||||
func pageNum(readerView: RDEpubReaderView, pageNum: Int)
|
||||
|
||||
/// 屏幕方向即将变化
|
||||
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
|
||||
@objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 RDReaderPageNavigating
|
||||
### 5.3 RDEpubReaderPageNavigating
|
||||
|
||||
```swift
|
||||
public protocol RDReaderPageNavigating: AnyObject {
|
||||
public protocol RDEpubReaderPageNavigating: AnyObject {
|
||||
|
||||
/// 当前页码
|
||||
var currentPage: Int { get }
|
||||
@@ -357,19 +361,19 @@ public protocol RDReaderPageNavigating: AnyObject {
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 RDReaderDataSource(遗留)
|
||||
### 5.4 RDEpubReaderDataSource(遗留)
|
||||
|
||||
```swift
|
||||
@objc public protocol RDReaderDataSource: NSObjectProtocol {
|
||||
func pageCountOfReaderView(readerView: RDReaderView) -> Int
|
||||
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
|
||||
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
|
||||
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
|
||||
@objc public protocol RDEpubReaderDataSource: NSObjectProtocol {
|
||||
func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int
|
||||
func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||
func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String?
|
||||
@objc optional func topToolView(readerView: RDEpubReaderView) -> UIView?
|
||||
@objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView?
|
||||
}
|
||||
```
|
||||
|
||||
通过 `RDReaderLegacyDataSourceAdapter` 自动适配到 `RDReaderPageProvider`。
|
||||
通过 `RDEpubReaderLegacyDataSourceAdapter` 自动适配到 `RDEpubReaderPageProvider`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+25
-25
@@ -22,7 +22,7 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable
|
||||
│ ReadViewDemo (Demo App) │
|
||||
│ ViewController · LaunchAutomationPlan · UITests │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│ imports RDReaderView
|
||||
│ imports RDEpubReaderView
|
||||
┌──────────────────────────────▼──────────────────────────────┐
|
||||
│ EPUBUI 层 │
|
||||
│ RDEPUBReaderController · Coordinators · Settings · TextPage │
|
||||
@@ -30,8 +30,8 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────────▼──────────────────────────────┐
|
||||
│ RDReaderView 层 │
|
||||
│ RDReaderView · FlowLayout · PreloadController │
|
||||
│ RDEpubReaderView 层 │
|
||||
│ RDEpubReaderView · FlowLayout · PreloadController │
|
||||
│ SpreadResolver · TapRegionHandler · PagingController │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
@@ -54,7 +54,7 @@ ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable
|
||||
|---|---|---|
|
||||
| **EPUBCore** | EPUB 文件解析、资源管理、WebView 渲染、JS 桥接 | `RDEPUBParser`, `RDEPUBPublication`, `RDEPUBWebView` |
|
||||
| **EPUBTextRendering** | HTML→NSAttributedString 转换、排版、分页、全书构建 | `RDEPUBTextBookBuilder`, `RDEPUBCoreTextPageFrameFactory` |
|
||||
| **RDReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDReaderView`, `RDReaderFlowLayout`, `RDReaderPreloadController` |
|
||||
| **RDEpubReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDEpubReaderView`, `RDEpubReaderFlowLayout`, `RDEpubReaderPreloadController` |
|
||||
| **EPUBUI** | 阅读器控制器、协调器模式、设置面板、按需加载、磁盘缓存 | `RDEPUBReaderController`, `RDEPUBReaderPaginationCoordinator` |
|
||||
|
||||
---
|
||||
@@ -310,10 +310,10 @@ RDEPUBWebView (UIView)
|
||||
|
||||
---
|
||||
|
||||
## 7. 翻页容器架构(RDReaderView)
|
||||
## 7. 翻页容器架构(RDEpubReaderView)
|
||||
|
||||
```
|
||||
RDReaderView (UIView)
|
||||
RDEpubReaderView (UIView)
|
||||
│
|
||||
├─ 三种翻页模式:
|
||||
│ ├─ .pageCurl → UIPageViewController (翻页动画)
|
||||
@@ -321,14 +321,14 @@ RDReaderView (UIView)
|
||||
│ └─ .verticalScroll → UICollectionView (垂直滚动)
|
||||
│
|
||||
├─ 组合对象:
|
||||
│ ├─ RDReaderPagingController // 翻页状态管理、请求队列
|
||||
│ ├─ RDReaderPreloadController // 页面预加载、缓存管理
|
||||
│ ├─ RDReaderSpreadResolver // 双页展开计算
|
||||
│ └─ RDReaderTapRegionHandler // 点击区域分类(左/中/右)
|
||||
│ ├─ RDEpubReaderPagingController // 翻页状态管理、请求队列
|
||||
│ ├─ RDEpubReaderPreloadController // 页面预加载、缓存管理
|
||||
│ ├─ RDEpubReaderSpreadResolver // 双页展开计算
|
||||
│ └─ RDEpubReaderTapRegionHandler // 点击区域分类(左/中/右)
|
||||
│
|
||||
├─ 数据源协议:
|
||||
│ ├─ RDReaderPageProvider (新) // 格式无关,优先级高
|
||||
│ └─ RDReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
|
||||
│ ├─ RDEpubReaderPageProvider (新) // 格式无关,优先级高
|
||||
│ └─ RDEpubReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
|
||||
│
|
||||
└─ 手势流:
|
||||
点击 → TapRegionHandler → 分类(左/中/右)
|
||||
@@ -351,7 +351,7 @@ struct RDEPUBReaderConfiguration {
|
||||
var numberOfColumns: Int // 1 或 2
|
||||
var columnGap: CGFloat // 默认 20
|
||||
var theme: ReaderTheme // 6 种主题
|
||||
var displayType: RDReaderView.DisplayType // pageCurl/horizontal/vertical
|
||||
var displayType: RDEpubReaderView.DisplayType // pageCurl/horizontal/vertical
|
||||
var onDemandChapterWindowSize: Int // 默认 3(奇数,最小 3,最大 15)
|
||||
var metadataParsingConcurrency: Int // 默认 CPU 核心数
|
||||
var chapterWindowRadius: Int // 内存缓存窗口半径
|
||||
@@ -374,7 +374,7 @@ struct RDEPUBReaderConfiguration {
|
||||
## 9. 目录结构
|
||||
|
||||
```
|
||||
Sources/RDReaderView/
|
||||
Sources/RDEpubReaderView/
|
||||
├── EPUBCore/ # EPUB 解析与 WebView 渲染
|
||||
│ ├── Models/ # 数据模型
|
||||
│ │ ├── RDEPUBAnnotationModels.swift
|
||||
@@ -428,19 +428,19 @@ Sources/RDReaderView/
|
||||
│ ├── RDEPUBDTCoreTextRenderer.swift # DTCoreText 渲染器实现
|
||||
│ ├── RDEPUBChapterData.swift # 章节查询门面
|
||||
│ ├── RDEPUBTextIndexTable.swift # 全书索引表
|
||||
│ └── RDPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
|
||||
│ └── RDEpubPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
|
||||
│
|
||||
├── ReaderView/ # 通用翻页容器
|
||||
│ ├── Paging/ # 翻页子系统
|
||||
│ │ ├── RDReaderPagingController.swift
|
||||
│ │ ├── RDReaderPreloadController.swift
|
||||
│ │ ├── RDReaderSpreadResolver.swift
|
||||
│ │ └── RDReaderTapRegionHandler.swift
|
||||
│ ├── RDReaderView.swift # 主容器视图
|
||||
│ ├── RDReaderView+*.swift # 扩展(CollectionView/PageCurl/ToolView/ContentAccess)
|
||||
│ ├── RDReaderFlowLayout.swift # 自定义 CollectionView 布局
|
||||
│ ├── RDReaderContentCell.swift # 滚动模式 Cell
|
||||
│ └── RDReaderPageChildViewController.swift // 翻页模式子 VC
|
||||
│ │ ├── RDEpubReaderPagingController.swift
|
||||
│ │ ├── RDEpubReaderPreloadController.swift
|
||||
│ │ ├── RDEpubReaderSpreadResolver.swift
|
||||
│ │ └── RDEpubReaderTapRegionHandler.swift
|
||||
│ ├── RDEpubReaderView.swift # 主容器视图
|
||||
│ ├── RDEpubReaderView+*.swift # 扩展(CollectionView/PageCurl/ToolView/ContentAccess)
|
||||
│ ├── RDEpubReaderFlowLayout.swift # 自定义 CollectionView 布局
|
||||
│ ├── RDEpubReaderContentCell.swift # 滚动模式 Cell
|
||||
│ └── RDEpubReaderPageChildViewController.swift // 翻页模式子 VC
|
||||
│
|
||||
└── EPUBUI/ # 阅读器 UI
|
||||
├── ReaderController/ # 控制器与协调器
|
||||
@@ -494,7 +494,7 @@ Sources/RDReaderView/
|
||||
| **Builder** | `RDEPUBBookPageMap.Builder` 增量构建页码映射 |
|
||||
| **Strategy** | `RDEPUBTextRenderer` 协议,可替换渲染器实现 |
|
||||
| **Pipeline** | `RDEPUBTextTypesetterPipeline` 排版管线(8 个逻辑阶段,封装为 5-6 个顶层调用) |
|
||||
| **Adapter** | `RDReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
|
||||
| **Adapter** | `RDEpubReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
|
||||
| **三级缓存** | 内存 → 磁盘摘要 → 全书分页,逐级降级 |
|
||||
| **Token 取消** | `paginationToken` 确保过期异步任务不干扰新任务 |
|
||||
| **Frozen Parameters** | 后台任务冻结 `renderSignature`,避免运行中参数漂移 |
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# ReadViewSDK 代码审查报告
|
||||
|
||||
> 审查范围:`Sources/RDReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
|
||||
> 审查范围:`Sources/RDEpubReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
|
||||
> 审查维度:并发安全、内存管理、API 正确性、Swift/iOS 平台特定、架构与依赖
|
||||
> 审查日期:2026-06-26
|
||||
|
||||
@@ -134,7 +134,7 @@ let stillChanged = abs(currentSize.width - self.lastTextPaginationPageSize!.widt
|
||||
|
||||
### H-10 · 依赖版本未锁定
|
||||
|
||||
`RDReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
|
||||
`RDEpubReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
|
||||
|
||||
```ruby
|
||||
s.dependency 'ZIPFoundation', '~> 0.9' # ✅ 已锁定
|
||||
@@ -143,7 +143,7 @@ s.dependency 'SnapKit' # ❌ 无约束
|
||||
s.dependency 'SSAlertSwift' # ❌ 无约束
|
||||
```
|
||||
|
||||
**位置:** `RDReaderView.podspec:17-20`
|
||||
**位置:** `RDEpubReaderView.podspec:17-20`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -298,14 +298,14 @@ return builder.build()
|
||||
|
||||
**触发条件:** `landscapeDualPageEnabled && isLandscape && !verticalScroll`
|
||||
|
||||
**页面配对逻辑(RDReaderSpreadResolver):**
|
||||
**页面配对逻辑(RDEpubReaderSpreadResolver):**
|
||||
|
||||
- 有封面页:封面独占一屏,后续页面两两配对
|
||||
- 配对规则:`(coverIndex, nil)`, `(1, 2)`, `(3, 4)`, ...
|
||||
- 无封面页:标准偶奇配对
|
||||
- 配对规则:`(0, 1)`, `(2, 3)`, `(4, 5)`, ...
|
||||
|
||||
**封面感知布局(RDReaderFlowLayout):**
|
||||
**封面感知布局(RDEpubReaderFlowLayout):**
|
||||
|
||||
```
|
||||
封面页: [====全屏宽度====]
|
||||
@@ -314,7 +314,7 @@ return builder.build()
|
||||
|
||||
### 5.3 页面预加载
|
||||
|
||||
`RDReaderPreloadController` 管理两个缓存:
|
||||
`RDEpubReaderPreloadController` 管理两个缓存:
|
||||
- `preloadedPageViews` - 预渲染的页面视图
|
||||
- `pageCurlCachedViews` - 当前显示的页面视图
|
||||
|
||||
@@ -580,7 +580,7 @@ protocol RDEPUBReaderPersistence {
|
||||
|
||||
## 9. 纯文本 (.txt) 支持
|
||||
|
||||
`RDPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
|
||||
`RDEpubPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
|
||||
|
||||
1. **解码:** 依次尝试 UTF-8 → GB18030 → GBK
|
||||
2. **分章:** 正则匹配 `^(第[零一二三四五六七八九十百千万\d]+[章节回卷].*)$`
|
||||
|
||||
+13
-13
@@ -34,7 +34,7 @@ epubcfi(/6/4!ch01.xhtml/4/2/1:3)
|
||||
|
||||
顶层 CFI 模型,对应一个完整的 `epubcfi(...)` 字符串。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFI.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFI: Codable, Equatable, Hashable {
|
||||
@@ -66,7 +66,7 @@ public enum RDEPUBCFISideBias: String, Codable {
|
||||
|
||||
路径模型,由一组 `RDEPUBCFIStep` 组成,描述从根节点到目标节点的遍历序列。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIPath.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIPath: Codable, Equatable, Hashable {
|
||||
@@ -95,7 +95,7 @@ public struct RDEPUBCFIStep: Codable, Equatable, Hashable {
|
||||
|
||||
范围模型,表示文档中的一个连续区域,由父级 CFI 和起止 CFI 组成。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIRange.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
|
||||
@@ -116,7 +116,7 @@ public struct RDEPUBCFIRange: Codable, Equatable, Hashable {
|
||||
|
||||
将 CFI 字符串解析为结构化模型。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIParser.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIParser {
|
||||
@@ -154,7 +154,7 @@ print(cfi.textAssertion?.exact) // Optional("目标文本")
|
||||
|
||||
将 CFI 模型序列化为标准字符串。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFISerializer.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFISerializer {
|
||||
@@ -192,7 +192,7 @@ let serialized = RDEPUBCFISerializer.serialize(cfi)
|
||||
|
||||
将 CFI 解析为可直接用于资源定位的结果。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIResolver.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFIResolverResult: Equatable {
|
||||
@@ -228,7 +228,7 @@ print(result.chapterOffset) // Optional(80)
|
||||
|
||||
从已知的章节信息生成 CFI。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIGenerator.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIGenerator {
|
||||
@@ -274,7 +274,7 @@ public enum RDEPUBCFIGenerator {
|
||||
|
||||
从 HTML 源码中提取带有 `id` 属性的元素路径映射。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIDOMPathBuilder.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIDOMPathBuilder {
|
||||
@@ -296,7 +296,7 @@ public enum RDEPUBCFIDOMPathBuilder {
|
||||
|
||||
当 CFI 精确定位失败时(如 DOM 结构变更),恢复引擎通过多级降级策略尝试找到最佳匹配位置。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIRecoveryEngine.swift`
|
||||
|
||||
### 5.1 恢复置信度
|
||||
|
||||
@@ -361,7 +361,7 @@ let result = RDEPUBCFIRecoveryEngine.recover(
|
||||
|
||||
文本断言用于在 CFI 定位后验证所指位置的文本内容是否符合预期,增强定位鲁棒性。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFITextAssertion.swift`
|
||||
|
||||
```swift
|
||||
public struct RDEPUBCFITextAssertion: Codable, Equatable, Hashable {
|
||||
@@ -393,7 +393,7 @@ print(assertion?.suffix) // Optional("后续内容")
|
||||
|
||||
提供宽松解析接口,兼容非标准 CFI 格式。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFICompatibility.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFICompatibility {
|
||||
@@ -418,7 +418,7 @@ public enum RDEPUBCFICompatibility {
|
||||
|
||||
CFI 映射是章节级别的索引结构,将 CFI 路径映射到章节文本偏移量,是容错恢复引擎的核心数据源。
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIMap.swift`
|
||||
|
||||
### 8.1 RDEPUBCFIMap
|
||||
|
||||
@@ -506,7 +506,7 @@ public struct RDEPUBCFITokenAnchor: Codable, Equatable {
|
||||
|
||||
## 9. 错误类型(RDEPUBCFIError)
|
||||
|
||||
**文件**:`Sources/RDReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
|
||||
**文件**:`Sources/RDEpubReaderView/EPUBCore/CFI/RDEPUBCFIError.swift`
|
||||
|
||||
```swift
|
||||
public enum RDEPUBCFIError: Error, Equatable {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
- 渐进补全:后台逐步补全所有章节的页码信息
|
||||
|
||||
**关键文件**:
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/` 目录
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift`
|
||||
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/ChapterRuntime/` 目录
|
||||
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift`
|
||||
|
||||
---
|
||||
|
||||
|
||||
+21
-9
@@ -1,6 +1,18 @@
|
||||
# 代码库风险与关注点
|
||||
|
||||
**分析日期:** 2026-05-21
|
||||
**状态复核:** 2026-07-30(逐条对照源码,结论见下表;正文段落保留原始分析,勿当作现状)
|
||||
|
||||
| # | 项目 | 现状 |
|
||||
|---|------|------|
|
||||
| 1 | User Script Sandboxing 被关 | **部分处理**:已移除 Podfile 的全局强制覆盖;但显式打成 `YES` 会让 CocoaPods 的 `[CP] Embed Pods Frameworks` rsync 阶段被沙箱拒绝而构建失败(已实测)。要真正开启需先改掉 `use_frameworks!` 动态框架集成方式。 |
|
||||
| 2 | 外链未做 allowlist | **已修**:`RDEPUBReaderController+ExternalLinks.swift` 有 scheme allowlist + 二次确认弹窗 + delegate 否决。 |
|
||||
| 3 | Zip Slip 未加固 | **已修**:`RDEPUBParser+Archive.swift` 的 `validatedExtractionDestination` 做 standardized 前缀校验并拒绝 `..`/绝对路径。 |
|
||||
| 4 | 高亮/选中文本明文写 UserDefaults | **已修**:新增 `RDEPUBProtectedContentStore`,高亮与书签改写 Application Support 下带 file protection 的文件、排除备份,首次读取自动迁移并删除明文;另加 `clearReadingData(for:)` / `clearAllReadingContent()`。 |
|
||||
| 5 | Scheme handler 整文件读内存 | **原判断有误**:>512KB 早已走 `respondWithStreaming` 64KB 分块。本次补上 `maximumInMemoryResourceBytes`(默认 64MB)给走不了流式的加密 provider 分支兜底。 |
|
||||
| 6 | 分页 WKWebView 可能加载外部资源 | 未复核。 |
|
||||
| 7 | 解压缓存无限累积 | **已修**:`pruneExtractionCache(keeping:)` 先按 `extractionCacheMaximumAge`(30 天)再按 `extractionCacheMaximumTotalBytes`(512MB)做 LRU 淘汰;复用时 touch mtime;另有 `clearExtractionCache()`。 |
|
||||
| 8+ | 其余性能/可维护性项 | 未复核。 |
|
||||
|
||||
## 高风险:安全与隐私
|
||||
|
||||
@@ -105,15 +117,15 @@
|
||||
## Evidence(仓库路径)
|
||||
|
||||
- Build flags:`Podfile`
|
||||
- Pod metadata/toolchain:`RDReaderView.podspec`
|
||||
- Archive extraction:`Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift`
|
||||
- Resource path validation(post-extraction):`Sources/RDReaderView/EPUBCore/RDEPUBParser+Resources.swift`
|
||||
- Scheme handler reads full file bytes:`Sources/RDReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
|
||||
- WebView bridge + message handling:`Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
|
||||
- External link opening:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
|
||||
- UserDefaults persistence implementation:`Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
||||
- Hidden paginator web view:`Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift`
|
||||
- Debug logging:`Sources/RDReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
|
||||
- Pod metadata/toolchain:`RDEpubReaderView.podspec`
|
||||
- Archive extraction:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser+Archive.swift`
|
||||
- Resource path validation(post-extraction):`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser+Resources.swift`
|
||||
- Scheme handler reads full file bytes:`Sources/RDEpubReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift`
|
||||
- WebView bridge + message handling:`Sources/RDEpubReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift`
|
||||
- External link opening:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift`
|
||||
- UserDefaults persistence implementation:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderPersistence.swift`
|
||||
- Hidden paginator web view:`Sources/RDEpubReaderView/EPUBCore/RDEPUBPaginator.swift`
|
||||
- Debug logging:`Sources/RDEpubReaderView/EPUBCore/RDEPUBWebViewDebug.swift`
|
||||
- Vendored dependencies:`Pods/`、`ReadViewDemo/Pods/`
|
||||
|
||||
---
|
||||
|
||||
+23
-23
@@ -6,43 +6,43 @@
|
||||
|
||||
## 语言与工程约束
|
||||
|
||||
- **主要语言**:Swift(Podspec 声明 `s.swift_versions = ["5.10"]`,见 `RDReaderView.podspec`)
|
||||
- **最低系统版本**:Podspec `iOS 15.0`(`RDReaderView.podspec`),示例工程 Podfile/构建设置里常见为 `iOS 15.6`(`Podfile`)
|
||||
- **依赖管理**:CocoaPods(`Podfile`、`ReadViewDemo/Podfile`、`Podfile.lock`)
|
||||
- **主要语言**:Swift(Podspec 声明 `s.swift_versions = ["5.10"]`,见 `RDEpubReaderView.podspec`)
|
||||
- **最低系统版本**:Podspec `iOS 15.0`(`RDEpubReaderView.podspec`),示例工程 Podfile/构建设置里常见为 `iOS 15.6`(`ReadViewDemo/Podfile`)
|
||||
- **依赖管理**:CocoaPods(`ReadViewDemo/Podfile`、`ReadViewDemo/Podfile.lock`)
|
||||
|
||||
## 命名约定
|
||||
|
||||
**文件/类型命名(Swift):**
|
||||
- 以类型名为文件名的单文件组织较常见:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
|
||||
- 以类型名为文件名的单文件组织较常见:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
|
||||
- 大量使用前缀区分模块域:
|
||||
- `RD...`:阅读器 UI/控制器相关(如 `Sources/RDReaderView/RDReaderView.swift`、`Sources/RDReaderView/RDURLReaderController.swift`)
|
||||
- `RDEPUB...`:EPUB Core/UI/渲染相关(如 `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- Extension 文件使用 `+` 命名:`Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
|
||||
- `RD...`:阅读器 UI/控制器相关(如 `Sources/RDEpubReaderView/RDEpubReaderView.swift`、`Sources/RDEpubReaderView/RDEpubURLReaderController.swift`)
|
||||
- `RDEPUB...`:EPUB Core/UI/渲染相关(如 `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- Extension 文件使用 `+` 命名:`Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift`
|
||||
|
||||
**变量/函数命名:**
|
||||
- 基本遵循 Swift lowerCamelCase:`parse(epubURL:)`(`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`)
|
||||
- 常量多用 `static let`:`kRDEPUBHighlightAttributeName`(`Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift`)
|
||||
- 基本遵循 Swift lowerCamelCase:`parse(epubURL:)`(`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`)
|
||||
- 常量多用 `static let`:`kRDEPUBHighlightAttributeName`(`Sources/RDEpubReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift`)
|
||||
|
||||
## 代码风格与排版(从现有代码归纳)
|
||||
|
||||
**缩进与换行:**
|
||||
- 多数文件使用 4 空格缩进(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`、`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- 多数文件使用 4 空格缩进(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`、`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- 历史代码中更常见”强制换行/多行括号”风格
|
||||
|
||||
**空行与分组:**
|
||||
- UI 相关文件常用空行分隔属性/初始化/布局段落
|
||||
- `// MARK:` 用于分区组织(示例:`Sources/RDReaderView/RDReaderGestureController.swift`、`Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift`)
|
||||
- `// MARK:` 用于分区组织(示例:`Sources/RDEpubReaderView/RDEpubReaderGestureController.swift`、`Sources/RDEpubReaderView/EPUBTextRendering/RDEpubPlainTextBookBuilder.swift`)
|
||||
|
||||
**类型组织:**
|
||||
- 偏好用 `extension` 拆分职责/协议实现(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift` 的 `UITableViewDataSource/Delegate`)
|
||||
- API 暴露处使用 `public`、`public final class`、`public enum/struct`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- “对外只读、内部可写”常用 `public internal(set)`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`)
|
||||
- API 暴露处使用 `public`、`public final class`、`public enum/struct`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- “对外只读、内部可写”常用 `public internal(set)`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`)
|
||||
|
||||
## 导入与依赖使用
|
||||
|
||||
**import:**
|
||||
- UIKit/UI 文件:`import UIKit`(大量文件)
|
||||
- Core/模型文件:`import Foundation`(如 `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- Core/模型文件:`import Foundation`(如 `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`)
|
||||
- 三方依赖按需引入:
|
||||
- `SnapKit`:布局
|
||||
- `SSAlertSwift`:弹窗/提示
|
||||
@@ -52,15 +52,15 @@
|
||||
|
||||
## 错误处理与日志
|
||||
|
||||
- Core 解析层倾向用 `throws` + 自定义 `Error`(示例:`Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`、`Sources/RDReaderView/EPUBCore/RDEPUBModels.swift` 的 `RDEPUBParserError`)
|
||||
- Core 解析层倾向用 `throws` + 自定义 `Error`(示例:`Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`、`Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift` 的 `RDEPUBParserError`)
|
||||
- UI/控制器层常见 `guard` 早返回(示例:`ReadViewDemo/ReadViewDemo/ViewController.swift`)
|
||||
- 未检测到统一日志框架(未发现专用 logging package/config);出现时以系统 API/局部输出为主(需按具体文件核对)。
|
||||
|
||||
## 注释与文档
|
||||
|
||||
- **项目规则(强约束)**:代码标识符保持英文,但**代码注释/文档/提交信息使用中文**(见 `CONTEXT.md`)。
|
||||
- 历史文件常带 Xcode 头部注释块(示例:`Sources/RDReaderView/ReaderView/RDReaderView.swift`)。
|
||||
- 公共 API 处存在少量三斜线文档注释(示例:`Sources/RDReaderView/RDReaderView.swift` 的中文说明)。
|
||||
- 历史文件常带 Xcode 头部注释块(示例:`Sources/RDEpubReaderView/ReaderView/RDEpubReaderView.swift`)。
|
||||
- 公共 API 处存在少量三斜线文档注释(示例:`Sources/RDEpubReaderView/RDEpubReaderView.swift` 的中文说明)。
|
||||
|
||||
## Lint / Formatter / 静态检查
|
||||
|
||||
@@ -76,10 +76,10 @@
|
||||
## Evidence(关键证据文件)
|
||||
|
||||
- `CONTEXT.md`
|
||||
- `Podfile`
|
||||
- `RDReaderView.podspec`
|
||||
- `ReadViewDemo/Podfile`
|
||||
- `RDEpubReaderView.podspec`
|
||||
- `ReadViewDemo/ReadViewDemo/ViewController.swift`
|
||||
- `Sources/RDReaderView/ReaderView/RDReaderView.swift`
|
||||
- `Sources/RDReaderView/EPUBCore/RDEPUBParser.swift`
|
||||
- `Sources/RDReaderView/EPUBCore/RDEPUBModels.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift`
|
||||
- `Sources/RDEpubReaderView/ReaderView/RDEpubReaderView.swift`
|
||||
- `Sources/RDEpubReaderView/EPUBCore/RDEPUBParser.swift`
|
||||
- `Sources/RDEpubReaderView/EPUBCore/RDEPUBModels.swift`
|
||||
- `Sources/RDEpubReaderView/EPUBUI/RDEPUBReaderController.swift`
|
||||
|
||||
@@ -471,7 +471,7 @@ final class RDEPUBTextSearchEngine: RDEPUBSearchEngine {
|
||||
|
||||
## 9. 纯文本构建器
|
||||
|
||||
**文件:** `RDPlainTextBookBuilder.swift`
|
||||
**文件:** `RDEpubPlainTextBookBuilder.swift`
|
||||
|
||||
从纯文本(.txt)文件构建 `RDEPUBTextBook`,用于支持 TXT 格式阅读。
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
let epubURL: URL // EPUB 文件路径
|
||||
let persistence: RDEPUBReaderPersistence? // 持久化代理
|
||||
let dependencies: RDEPUBReaderDependencies // 依赖注入
|
||||
let readerView = RDReaderView() // 翻页容器
|
||||
let readerView = RDEpubReaderView() // 翻页容器
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `RDEPUBReaderController+PublicAPI.swift` | 公开 API(跳转、搜索、标注、书签) |
|
||||
| `RDEPUBReaderController+DataSource.swift` | `RDReaderPageProvider` 实现 |
|
||||
| `RDEPUBReaderController+DataSource.swift` | `RDEpubReaderPageProvider` 实现 |
|
||||
| `RDEPUBReaderController+ContentDelegates.swift` | WebView/TextContentView 代理路由 |
|
||||
| `RDEPUBReaderController+LocationResolution.swift` | 页码/位置解析、阅读状态同步 |
|
||||
| `RDEPUBReaderController+ExternalLinks.swift` | 外部链接处理(白名单、确认弹窗) |
|
||||
@@ -120,7 +120,7 @@ public protocol RDEPUBReaderPersistence: AnyObject {
|
||||
```swift
|
||||
final class RDEPUBReaderContext {
|
||||
weak var controller: RDEPUBReaderController?
|
||||
weak var readerView: RDReaderView?
|
||||
weak var readerView: RDEpubReaderView?
|
||||
var dependencies: RDEPUBReaderDependencies
|
||||
var runtime: RDEPUBReaderRuntime?
|
||||
|
||||
@@ -155,7 +155,7 @@ public struct RDEPUBReaderDependencies {
|
||||
public var makeParser: () -> RDEPUBParser
|
||||
public var makePaginator: () -> RDEPUBPaginator
|
||||
public var makeTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder
|
||||
public var makePlainTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder
|
||||
public var makePlainTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDEpubPlainTextBookBuilder
|
||||
public var makeTextRenderer: (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer
|
||||
|
||||
public static var live: RDEPUBReaderDependencies // 默认实现
|
||||
@@ -659,9 +659,9 @@ struct RDEPUBReaderUIState {
|
||||
|
||||
页图协调器(按需分页与全量分页的结果合并)。
|
||||
|
||||
### 10.6 RDURLReaderController
|
||||
### 10.6 RDEpubURLReaderController
|
||||
|
||||
**文件:** `RDURLReaderController.swift`
|
||||
**文件:** `RDEpubURLReaderController.swift`
|
||||
|
||||
URL 阅读器控制器(用于打开单个 URL)。
|
||||
|
||||
@@ -715,12 +715,12 @@ RDEPUBReaderRuntime.applyParsedPublication()
|
||||
│
|
||||
├── RDEPUBBookPageMap 生成
|
||||
│
|
||||
└── RDReaderView.transitionToPage() → 显示页面
|
||||
└── RDEpubReaderView.transitionToPage() → 显示页面
|
||||
|
||||
用户翻页
|
||||
│
|
||||
▼
|
||||
RDReaderView.currentPage 变化
|
||||
RDEpubReaderView.currentPage 变化
|
||||
│
|
||||
├── RDEPUBReaderLocationCoordinator.recordPageChangeIfNeeded()
|
||||
│ └── persistence.saveLocation()
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
生成点:
|
||||
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494`(`buildPagesFromRanges`)
|
||||
- `Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
|
||||
- `Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift:78`
|
||||
- `Sources/RDEpubReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494`(`buildPagesFromRanges`)
|
||||
- `Sources/RDEpubReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294`
|
||||
- `Sources/RDEpubReaderView/EPUBTextRendering/RDEpubPlainTextBookBuilder.swift:78`
|
||||
|
||||
现存全部消费点(已盘点确认,仅 3 处):
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
**任务**:
|
||||
|
||||
- [x] 将 `RDEPUBTextPage.content` 改为按需构造:已改为基于 `chapterContent` + `contentRange` 的计算属性(带范围钳制),公开读取 API 不变
|
||||
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
|
||||
- [x] 三个构造点(`RDEPUBChapterLoader` / `RDEPUBTextBookBuilder` / `RDEpubPlainTextBookBuilder`)不再生成每页子串;消费点经计算属性透明按需构造
|
||||
- [x] `Equatable` 确认:`content` 为派生值,由 `contentRange` + `chapterContent` 判等覆盖,语义不变
|
||||
|
||||
**API 兼容性注意**:
|
||||
@@ -47,7 +47,7 @@
|
||||
### P0-3 保持现有缓存限制,防止错误复用
|
||||
|
||||
- [x] `shouldAvoidReaderPageCaching`(`RDEPUBTextContentView.swift:319`)保持现状,已补注释说明放开的前置条件是 P1-1 落地
|
||||
- [x] 本阶段未改 `RDReaderPreloadController` 行为
|
||||
- [x] 本阶段未改 `RDEpubReaderPreloadController` 行为
|
||||
|
||||
---
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
|
||||
前置:P1-1~P1-3 全部落地后,页面视图不再持有整章副本。
|
||||
|
||||
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDReaderPreloadController` 缓存
|
||||
- [ ] 调整 `shouldAvoidReaderPageCaching` 判定,允许轻量化后的文本页进入 `RDEpubReaderPreloadController` 缓存
|
||||
- [ ] 对比放开前后的翻页流畅度与峰值内存,数据不佳则回退此项
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
# RDAIReaderView AI 系统设计合同
|
||||
|
||||
**文档状态:** Draft 0.1
|
||||
**最后更新:** 2026-07-25
|
||||
**目标版本:** RDAIReaderView 1.0
|
||||
|
||||
## 1. 系统分类
|
||||
|
||||
RDAIReaderView 是一个本地优先的 Retrieval-Augmented Generation 阅读系统,包含:
|
||||
|
||||
- 确定性 NLP:语言识别、分句、实体候选、词法检索。
|
||||
- 本地语义检索:Natural Language embeddings。
|
||||
- 生成式任务:摘要、书内问答、人物卡片和人物关系。
|
||||
- 引用约束:生成内容必须映射到 PDF/EPUB 原文。
|
||||
|
||||
系统不是通用聊天机器人,也不把模型自身知识作为书籍事实来源。
|
||||
|
||||
## 2. 框架选择
|
||||
|
||||
### 2.1 主框架
|
||||
|
||||
- Apple Natural Language:iOS 15+ 基础分析和检索。
|
||||
- Apple Foundation Models:支持设备上的生成式增强。
|
||||
- Vision:扫描 PDF OCR,继续复用 RDPDFReaderView 现有能力。
|
||||
|
||||
### 2.2 选择理由
|
||||
|
||||
- 与当前纯 Swift/UIKit/CocoaPods 架构一致。
|
||||
- 默认设备端处理,不需要新增服务端和书籍上传链路。
|
||||
- Natural Language 可覆盖不支持 Apple Intelligence 的设备。
|
||||
- Foundation Models 支持 structured generation 和 tool calling。
|
||||
- 系统 API 可与现有 PDF/EPUB 定位直接结合。
|
||||
|
||||
### 2.3 未选择的首版方案
|
||||
|
||||
| 方案 | 首版不采用原因 |
|
||||
|------|----------------|
|
||||
| LangChain/LlamaIndex | 主要面向 Python/服务端,增加不必要基础设施 |
|
||||
| 云端 LLM | 引入内容上传、成本、隐私、版权和网络可用性问题 |
|
||||
| ONNX Runtime 自带模型 | 需要自行选择、量化、分发和维护语言模型 |
|
||||
| 自训练 Foundation Models Adapter | 模型版本绑定和 entitlement 增加发布复杂度 |
|
||||
|
||||
Core 保留 `RDAIGenerativeProvider`,以后可以增加其他 Provider,不把 Apple 实现写死在公共业务层。
|
||||
|
||||
## 3. 模型可用性合同
|
||||
|
||||
调用 Foundation Models 前必须检查:
|
||||
|
||||
1. API 在当前系统可用。
|
||||
2. `SystemLanguageModel.default.availability` 为 available。
|
||||
3. 当前 Locale 被支持。
|
||||
4. 当前请求未超过并发限制。
|
||||
5. 当前任务的上下文预算可满足。
|
||||
|
||||
不可用原因映射:
|
||||
|
||||
| Apple 状态 | RDAI 状态 | UI 行为 |
|
||||
|------------|-----------|---------|
|
||||
| deviceNotEligible | `.deviceNotEligible` | 隐藏生成操作,保留基础分析 |
|
||||
| appleIntelligenceNotEnabled | `.appleIntelligenceNotEnabled` | 说明可在系统设置中开启 |
|
||||
| modelNotReady | `.modelNotReady` | 展示模型准备中,可稍后重试 |
|
||||
| unsupported locale | `.languageUnsupported` | 保留检索,关闭生成 |
|
||||
| unknown | `.unknown` | 通用不可用状态,允许重试 |
|
||||
|
||||
禁止通过静态设备型号列表推断可用性,运行时状态是唯一依据。
|
||||
|
||||
参考:
|
||||
|
||||
- [Foundation Models](https://developer.apple.com/documentation/FoundationModels)
|
||||
- [SystemLanguageModel](https://developer.apple.com/documentation/FoundationModels/SystemLanguageModel)
|
||||
- [语言与 Locale 支持](https://developer.apple.com/documentation/foundationmodels/supporting-languages-and-locales-with-foundation-models)
|
||||
|
||||
## 4. 输入与上下文策略
|
||||
|
||||
### 4.1 唯一事实来源
|
||||
|
||||
模型可以使用:
|
||||
|
||||
- 本次请求提供的 Passage。
|
||||
- Passage 的章节标题、页码/资源信息。
|
||||
- 用户当前问题。
|
||||
- 非内容性规则,例如输出语言和防剧透范围。
|
||||
|
||||
模型不得把训练知识、其他书籍、互联网知识或先前书籍会话作为当前书籍事实来源。
|
||||
|
||||
### 4.2 上下文预算
|
||||
|
||||
运行时读取模型 `contextSize`,并使用 `tokenCount(for:)` 估算。
|
||||
|
||||
初始预算比例:
|
||||
|
||||
- 12%:instructions 和 schema。
|
||||
- 5%:用户问题。
|
||||
- 60%:检索 Passage。
|
||||
- 17%:输出。
|
||||
- 6%:安全余量。
|
||||
|
||||
若预算不足,按以下顺序处理:
|
||||
|
||||
1. 删除低分 Passage。
|
||||
2. 缩短 Passage 到完整句子边界。
|
||||
3. 使用预计算的有引用片段摘要。
|
||||
4. 创建新会话。
|
||||
5. 仍不足则返回 `contextLimitExceeded`。
|
||||
|
||||
不得静默截断引用或生成半个结构化对象。
|
||||
|
||||
参考:[Managing the context window](https://developer.apple.com/documentation/foundationmodels/managing-the-context-window)
|
||||
|
||||
## 5. Prompt 资产管理
|
||||
|
||||
Prompt 作为版本化代码资产保存:
|
||||
|
||||
```text
|
||||
FoundationModels/Prompts/
|
||||
├── summary_v1.swift
|
||||
├── answer_v1.swift
|
||||
├── characters_v1.swift
|
||||
└── relationships_v1.swift
|
||||
```
|
||||
|
||||
每个 Prompt 定义:
|
||||
|
||||
- `identifier`
|
||||
- `version`
|
||||
- `minimumModelProfile`
|
||||
- `instructions`
|
||||
- 输入构造器
|
||||
- 输出 schema
|
||||
- 评测集标签
|
||||
|
||||
修改 Prompt 必须:
|
||||
|
||||
1. 增加版本号。
|
||||
2. 跑完整离线评测集。
|
||||
3. 与上一版本对比准确率、拒答、引用和延迟。
|
||||
4. 更新缓存失效策略。
|
||||
|
||||
## 6. 通用 Instructions
|
||||
|
||||
所有任务共享以下不可省略规则:
|
||||
|
||||
```text
|
||||
你是书内阅读助手。
|
||||
只使用提供的原文片段,不使用外部知识补充书中事实。
|
||||
每个事实性结论必须引用一个或多个有效片段 ID。
|
||||
证据不足时返回 insufficientEvidence,不猜测。
|
||||
不得引用允许阅读范围之外的内容。
|
||||
区分原文明确事实与可能推断。
|
||||
使用用户当前语言回答。
|
||||
```
|
||||
|
||||
实际实现使用简洁英文或经评测验证的目标语言 instructions;以上文字表达语义合同,不要求逐字使用。
|
||||
|
||||
## 7. 结构化输出
|
||||
|
||||
### 7.1 摘要 Schema
|
||||
|
||||
```swift
|
||||
@Generable
|
||||
struct GeneratedSummary {
|
||||
var overview: String
|
||||
|
||||
@Guide(.maximumCount(6))
|
||||
var points: [GeneratedStatement]
|
||||
}
|
||||
|
||||
@Generable
|
||||
struct GeneratedStatement {
|
||||
var text: String
|
||||
|
||||
@Guide(.minimumCount(1), .maximumCount(3))
|
||||
var passageIDs: [String]
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `overview` 只能概括已提供 Passage。
|
||||
- 每个 point 必须有 Passage ID。
|
||||
- brief 最多 3 点,standard 最多 6 点。
|
||||
|
||||
### 7.2 问答 Schema
|
||||
|
||||
```swift
|
||||
@Generable
|
||||
enum GeneratedAnswerStatus {
|
||||
case answered
|
||||
case insufficientEvidence
|
||||
}
|
||||
|
||||
@Generable
|
||||
struct GeneratedAnswer {
|
||||
var status: GeneratedAnswerStatus
|
||||
var answer: String
|
||||
|
||||
@Guide(.maximumCount(6))
|
||||
var statements: [GeneratedStatement]
|
||||
}
|
||||
```
|
||||
|
||||
若 `status == insufficientEvidence`:
|
||||
|
||||
- `statements` 必须为空。
|
||||
- `answer` 只说明当前已读内容没有足够依据。
|
||||
- 不推荐用户从互联网获取答案,除非宿主未来明确增加该产品能力。
|
||||
|
||||
### 7.3 人物 Schema
|
||||
|
||||
```swift
|
||||
@Generable
|
||||
struct GeneratedCharacter {
|
||||
var displayName: String
|
||||
|
||||
@Guide(.maximumCount(6))
|
||||
var aliases: [String]
|
||||
|
||||
var description: String
|
||||
|
||||
@Guide(.minimumCount(1), .maximumCount(6))
|
||||
var evidencePassageIDs: [String]
|
||||
}
|
||||
```
|
||||
|
||||
人物必须在 Passage 中有明确名称或可验证别名。纯代词不能单独创建人物。
|
||||
|
||||
### 7.4 关系 Schema
|
||||
|
||||
```swift
|
||||
@Generable
|
||||
enum GeneratedRelationshipStatus {
|
||||
case confirmed
|
||||
case possible
|
||||
case conflicting
|
||||
}
|
||||
|
||||
@Generable
|
||||
struct GeneratedRelationship {
|
||||
var sourceName: String
|
||||
var targetName: String
|
||||
var label: String
|
||||
var status: GeneratedRelationshipStatus
|
||||
|
||||
@Guide(.minimumCount(1), .maximumCount(5))
|
||||
var evidencePassageIDs: [String]
|
||||
}
|
||||
```
|
||||
|
||||
关系标签应简短,例如“师徒”“同事”“敌对”“亲属”。描述性事件放在人物事件中,不无限创建关系类型。
|
||||
|
||||
## 8. 任务设计
|
||||
|
||||
### 8.1 章节摘要
|
||||
|
||||
输入:
|
||||
|
||||
- 当前章节 Passage,或已读范围内的章节片段摘要。
|
||||
- 目标长度。
|
||||
- 用户 Locale。
|
||||
|
||||
流程:
|
||||
|
||||
1. 章节能放入上下文时直接生成。
|
||||
2. 超长章节分块生成带引用局部摘要。
|
||||
3. 聚合局部摘要时保留原 Passage ID。
|
||||
4. 运行引用验证。
|
||||
|
||||
禁止只把局部摘要文本作为最终事实来源而丢失原始 Passage ID。
|
||||
|
||||
### 8.2 书内问答
|
||||
|
||||
流程:
|
||||
|
||||
1. 识别问题语言和实体词。
|
||||
2. Hybrid Retrieval 召回 30 个候选。
|
||||
3. 重排并过滤到 3-4 个 Passage。
|
||||
4. 直接把 Passage 放入 Prompt。
|
||||
5. 生成结构化答案。
|
||||
6. 验证引用和阅读范围。
|
||||
7. 无有效事实项时拒答。
|
||||
|
||||
首版优先使用代码检索,不默认使用 Tool Calling,这样召回过程更确定、可测试、节省 token。
|
||||
|
||||
### 8.3 人物卡片
|
||||
|
||||
流程:
|
||||
|
||||
1. Natural Language 提供人物候选及出现位置。
|
||||
2. 按名称和明确别名聚合候选。
|
||||
3. 检索候选周边 Passage。
|
||||
4. Foundation Models 生成结构化人物信息。
|
||||
5. 代码校验所有别名和证据。
|
||||
6. 低置信别名保持独立候选。
|
||||
|
||||
### 8.4 人物关系
|
||||
|
||||
采用两阶段:
|
||||
|
||||
1. Chunk-level extraction:从局部 Passage 提取关系候选。
|
||||
2. Document-level merge:按人物 ID、关系标签和时间顺序合并。
|
||||
|
||||
合并规则:
|
||||
|
||||
- 相同关系 + 相同方向:合并证据。
|
||||
- 对称关系可由受控词典决定是否双向展示。
|
||||
- 新证据否定旧关系:状态变为 conflicting。
|
||||
- 隐含动机、情感和立场默认 possible。
|
||||
- 任何关系没有有效证据时不入库。
|
||||
|
||||
## 9. Tool Calling
|
||||
|
||||
仅在单次检索无法回答、且评测证明多轮查找有明显收益时启用。最多提供三个工具:
|
||||
|
||||
```text
|
||||
searchBook(query, scope, limit)
|
||||
getPassages(ids)
|
||||
getCharacterEvidence(name, scope, limit)
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 工具只读。
|
||||
- 工具强制应用文档和已读范围过滤。
|
||||
- 参数有严格长度和数量上限。
|
||||
- 返回内容有 token 上限。
|
||||
- 每个请求最大 Tool 调用次数为 3。
|
||||
- 检测重复参数调用并终止循环。
|
||||
- 工具调用和结果 ID进入本地 trace,但不记录原文。
|
||||
|
||||
不提供跳页、删除、购买、网络请求等副作用工具。
|
||||
|
||||
参考:[Expanding generation with tool calling](https://developer.apple.com/documentation/foundationmodels/expanding-generation-with-tool-calling)
|
||||
|
||||
## 10. 确定性后处理
|
||||
|
||||
模型输出必须经过:
|
||||
|
||||
1. Schema 解码。
|
||||
2. Passage ID 存在性校验。
|
||||
3. 阅读范围校验。
|
||||
4. Quote/Locator 构造。
|
||||
5. 重复 statement 合并。
|
||||
6. 空文本和长度校验。
|
||||
7. 敏感内容与系统错误映射。
|
||||
8. Artifact 元数据补齐。
|
||||
|
||||
模型不能直接构造页码、CFI、CGRect 或数据库 ID;这些字段全部由代码通过 Passage ID解析。
|
||||
|
||||
## 11. 安全与产品规则
|
||||
|
||||
- 不将 AI 输出表示为作者原话。
|
||||
- UI 明确标记“AI 生成”。
|
||||
- possible/conflicting 关系必须视觉区分。
|
||||
- 用户问题涉及未读内容时,默认拒绝并提示防剧透设置。
|
||||
- 原文包含违法或敏感内容时,遵循系统模型 guardrails;不能绕过。
|
||||
- 输入和输出触发系统安全限制时,返回稳定、非技术性的不可生成状态。
|
||||
- 不要求模型提供医学、法律或金融建议;如果书中包含相关内容,只能解释“书中写了什么”。
|
||||
|
||||
发布前必须复核 Apple Foundation Models acceptable use requirements 和最新 App Review Guidelines。
|
||||
|
||||
## 12. 关键失败模式
|
||||
|
||||
| 失败模式 | 检测 | 处理 |
|
||||
|----------|------|------|
|
||||
| 模型编造人物或关系 | Passage ID/名称验证 | 删除无效项,无结果则拒答 |
|
||||
| 引用存在但不支持结论 | 人工评测 + LLM judge | Prompt 调整,低分结果进入回归集 |
|
||||
| 同名人物合并 | 别名证据检查 | 保持独立,标记待确认 |
|
||||
| 未读内容泄漏 | Scope validator | 阻断输出并记录安全计数 |
|
||||
| OCR 错字导致错误事实 | OCR source 标记和置信策略 | 降低置信度,展示 OCR 来源 |
|
||||
| Prompt 在系统更新后退化 | 模型版本分桶评测 | 版本化 Prompt 和缓存 |
|
||||
| 上下文溢出 | tokenCount/contextSize | 重建上下文或分层摘要 |
|
||||
| Tool 循环 | 调用次数和参数去重 | 终止并降级为现有证据回答 |
|
||||
| 用户快速重复请求 | request actor + cancellation | 取消旧任务或排队 |
|
||||
|
||||
## 13. 评测维度
|
||||
|
||||
| 维度 | 定义 | 1.0 门槛 |
|
||||
|------|------|----------|
|
||||
| Citation validity | 引用能否恢复到原文 | ≥ 99% |
|
||||
| Context faithfulness | 事实是否由引用支持 | ≥ 98% |
|
||||
| Refusal accuracy | 无证据时是否拒答 | ≥ 95% |
|
||||
| Spoiler safety | 是否只使用允许范围 | 100% |
|
||||
| Schema validity | 结构化输出是否通过校验 | ≥ 99.5% |
|
||||
| Character precision | 人物是否真实出现 | ≥ 97% |
|
||||
| Relationship evidence | 关系是否至少有一条证据 | 100% |
|
||||
| Alias precision | 自动合并别名是否正确 | ≥ 98% |
|
||||
| Retrieval recall@5 | 正确证据是否在 Top 5 | ≥ 90% |
|
||||
| Answer usefulness | 人工 1-5 分平均值 | ≥ 4.0 |
|
||||
|
||||
## 14. 评测方法
|
||||
|
||||
- 代码指标:Schema、引用 ID、范围、阅读权限、延迟和拒答格式。
|
||||
- 人工标注:人物、别名、关系、引用支持度、剧透边界。
|
||||
- LLM judge:只用于语气、完整性和引用支持度的辅助评估;必须先与人工评分校准。
|
||||
- 生产抽样:只上传宿主允许的匿名数值和用户显式反馈;不上传书籍原文。
|
||||
|
||||
评测集和完整方法见 [RDAIReaderView-TEST-PLAN.md](RDAIReaderView-TEST-PLAN.md)。
|
||||
|
||||
## 15. 发布检查清单
|
||||
|
||||
- [ ] 所有 Prompt 有 identifier 和 version。
|
||||
- [ ] 每个生成任务使用 `@Generable`。
|
||||
- [ ] 每个事实输出经过 Citation Validator。
|
||||
- [ ] 防剧透 Scope 在检索前和生成后各检查一次。
|
||||
- [ ] Foundation Models 不可用路径已真机验证。
|
||||
- [ ] 上下文预算使用运行时 API 计算。
|
||||
- [ ] 模型版本变化不会复用旧缓存。
|
||||
- [ ] 评测集达到所有 1.0 门槛。
|
||||
- [ ] 日志不包含原文、问题或完整回答。
|
||||
- [ ] Apple 最新 acceptable use 与审核要求已复核。
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
# RDAIReaderView 公共 API 设计
|
||||
|
||||
**文档状态:** Proposal 0.1
|
||||
**最后更新:** 2026-07-25
|
||||
**目标版本:** RDAIReaderView 1.0
|
||||
|
||||
## 1. API 设计原则
|
||||
|
||||
- Core 最低支持 iOS 15,不直接依赖 UIKit、PDFKit、DTCoreText 或 FoundationModels。
|
||||
- 公共模型优先使用值类型,并遵循 `Codable`、`Sendable`、`Equatable`。
|
||||
- 文本范围统一使用 UTF-16 偏移,与现有 PDF、EPUB、NSString 和 TTS 范围保持一致。
|
||||
- Reader Adapter 负责格式转换,Core 不识别 PDF 页视图或 EPUB 排版对象。
|
||||
- Foundation Models 通过 Provider 协议接入,不能泄漏到基础 API。
|
||||
- 公开 API 在 1.0 后遵循语义化版本;新增字段必须有解码默认值。
|
||||
|
||||
本文中的 Swift 定义是实现合同,允许在不改变语义的前提下调整文件组织和内部实现。
|
||||
|
||||
## 2. 标识符与基础范围
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
public struct RDAIDocumentIdentifier: RawRepresentable, Codable, Hashable, Sendable {
|
||||
public let rawValue: String
|
||||
|
||||
public init(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public struct RDAIResourceIdentifier: RawRepresentable, Codable, Hashable, Sendable {
|
||||
public let rawValue: String
|
||||
|
||||
public init(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public struct RDAITextRange: Codable, Hashable, Sendable {
|
||||
public var location: Int
|
||||
public var length: Int
|
||||
|
||||
public init(location: Int, length: Int) {
|
||||
self.location = max(0, location)
|
||||
self.length = max(0, length)
|
||||
}
|
||||
|
||||
public var upperBound: Int { location + length }
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `RDAIDocumentIdentifier` 必须与宿主书籍 ID 一致并保持稳定。
|
||||
- `RDAIResourceIdentifier` 在 PDF 中使用页索引字符串,在 EPUB 中使用规范化 `href`。
|
||||
- 所有文本范围均针对资源原始文本,不针对规范化搜索文本。
|
||||
|
||||
## 3. 定位与引用
|
||||
|
||||
### 3.1 归一化矩形
|
||||
|
||||
Core 使用自定义矩形,避免公共存储格式依赖 UIKit:
|
||||
|
||||
```swift
|
||||
public struct RDAINormalizedRect: Codable, Hashable, Sendable {
|
||||
public var x: Double
|
||||
public var y: Double
|
||||
public var width: Double
|
||||
public var height: Double
|
||||
|
||||
public init(x: Double, y: Double, width: Double, height: Double) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有值应限制在 `0...1`。Reader Adapter 负责与 `CGRect` 转换。
|
||||
|
||||
### 3.2 格式 Anchor
|
||||
|
||||
```swift
|
||||
public struct RDAIPDFAnchor: Codable, Hashable, Sendable {
|
||||
public enum TextSource: String, Codable, Sendable {
|
||||
case native
|
||||
case ocr
|
||||
}
|
||||
|
||||
public var pageIndex: Int
|
||||
public var rects: [RDAINormalizedRect]
|
||||
public var textSource: TextSource
|
||||
public var readingOrder: Int?
|
||||
}
|
||||
|
||||
public struct RDAIEPUBAnchor: Codable, Hashable, Sendable {
|
||||
public var href: String
|
||||
public var cfi: String?
|
||||
public var rangeCFI: String?
|
||||
public var progression: Double?
|
||||
}
|
||||
|
||||
public enum RDAIAnchor: Codable, Hashable, Sendable {
|
||||
case pdf(RDAIPDFAnchor)
|
||||
case epub(RDAIEPUBAnchor)
|
||||
}
|
||||
```
|
||||
|
||||
`RDAIAnchor` 必须实现显式 Codable discriminator,例如 `type: "pdf"`,未知类型解码为明确错误,不能误当成其他格式。
|
||||
|
||||
### 3.3 通用定位
|
||||
|
||||
```swift
|
||||
public struct RDAILocator: Codable, Hashable, Sendable {
|
||||
public var documentIdentifier: RDAIDocumentIdentifier
|
||||
public var resourceIdentifier: RDAIResourceIdentifier
|
||||
public var textRange: RDAITextRange
|
||||
public var anchor: RDAIAnchor
|
||||
public var sourceHash: String
|
||||
}
|
||||
|
||||
public struct RDAICitation: Codable, Hashable, Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let passageIdentifier: String
|
||||
public let quote: String
|
||||
public let locator: RDAILocator
|
||||
}
|
||||
```
|
||||
|
||||
引用恢复流程:
|
||||
|
||||
1. 校验文档和资源存在。
|
||||
2. 校验 `sourceHash`。
|
||||
3. 优先按格式 Anchor 恢复。
|
||||
4. Anchor 失败时使用文本范围和 quote 搜索。
|
||||
5. 仍失败则返回 `staleCitation`,不得跳转到近似但未验证的位置。
|
||||
|
||||
## 4. 文档与资源
|
||||
|
||||
```swift
|
||||
public enum RDAIDocumentFormat: String, Codable, Sendable {
|
||||
case pdf
|
||||
case epub
|
||||
}
|
||||
|
||||
public struct RDAIDocumentDescriptor: Codable, Equatable, Sendable {
|
||||
public let identifier: RDAIDocumentIdentifier
|
||||
public let title: String
|
||||
public let format: RDAIDocumentFormat
|
||||
public let contentRevision: String
|
||||
}
|
||||
|
||||
public struct RDAIResourceDescriptor: Codable, Equatable, Sendable {
|
||||
public let identifier: RDAIResourceIdentifier
|
||||
public let title: String?
|
||||
public let order: Int
|
||||
public let estimatedUTF16Length: Int?
|
||||
}
|
||||
|
||||
public struct RDAIResourceSnapshot: Sendable {
|
||||
public let descriptor: RDAIResourceDescriptor
|
||||
public let sourceText: String
|
||||
public let sourceHash: String
|
||||
public let locatorRuns: [RDAILocatorRun]
|
||||
}
|
||||
|
||||
public struct RDAILocatorRun: Sendable {
|
||||
public let textRange: RDAITextRange
|
||||
public let anchor: RDAIAnchor
|
||||
}
|
||||
```
|
||||
|
||||
`contentRevision` 由宿主提供;若宿主没有版本号,Adapter 使用资源哈希汇总生成。
|
||||
|
||||
## 5. 内容提供协议
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
public protocol RDAIContentProvider: AnyObject {
|
||||
func aiDocumentDescriptor() -> RDAIDocumentDescriptor
|
||||
func aiResources() async throws -> [RDAIResourceDescriptor]
|
||||
func aiResourceSnapshot(
|
||||
for identifier: RDAIResourceIdentifier
|
||||
) async throws -> RDAIResourceSnapshot
|
||||
func aiNavigate(to locator: RDAILocator, animated: Bool) async throws
|
||||
func aiShowCitationHighlight(_ citation: RDAICitation) async throws
|
||||
func aiClearCitationHighlight()
|
||||
}
|
||||
```
|
||||
|
||||
协议标记为 `@MainActor`,因为现有 Reader Controller 和页面缓存均由主线程管理。实现必须只在主线程获取快照引用和 UI 状态;OCR、分块、分析和数据库写入移交后台 actor。
|
||||
|
||||
可选读取范围协议:
|
||||
|
||||
```swift
|
||||
public struct RDAIReadScope: Codable, Equatable, Sendable {
|
||||
public let upperBound: RDAILocator?
|
||||
public let includesWholeDocument: Bool
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public protocol RDAIReadScopeProviding: AnyObject {
|
||||
func aiCurrentReadScope() -> RDAIReadScope
|
||||
}
|
||||
```
|
||||
|
||||
未实现时,默认只允许当前资源及之前的资源,不能默认整本书。
|
||||
|
||||
## 6. Passage 与分析结果
|
||||
|
||||
```swift
|
||||
public enum RDAIPassageKind: String, Codable, Sendable {
|
||||
case title
|
||||
case paragraph
|
||||
case list
|
||||
case table
|
||||
case code
|
||||
case footnote
|
||||
case unknown
|
||||
}
|
||||
|
||||
public struct RDAIPassage: Codable, Equatable, Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let documentIdentifier: RDAIDocumentIdentifier
|
||||
public let resourceIdentifier: RDAIResourceIdentifier
|
||||
public let text: String
|
||||
public let languageCode: String?
|
||||
public let kind: RDAIPassageKind
|
||||
public let locator: RDAILocator
|
||||
public let contentHash: String
|
||||
public let order: Int
|
||||
}
|
||||
|
||||
public enum RDAIEntityKind: String, Codable, Sendable {
|
||||
case person
|
||||
case place
|
||||
case organization
|
||||
case other
|
||||
}
|
||||
|
||||
public struct RDAIEntityMention: Codable, Equatable, Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let normalizedName: String
|
||||
public let surfaceText: String
|
||||
public let kind: RDAIEntityKind
|
||||
public let confidence: Double
|
||||
public let locator: RDAILocator
|
||||
}
|
||||
```
|
||||
|
||||
Natural Language 的标签不是最终人物事实,只是 `RDAIEntityMention` 候选。
|
||||
|
||||
## 7. 索引 API
|
||||
|
||||
```swift
|
||||
public enum RDAIIndexState: Equatable, Sendable {
|
||||
case notStarted
|
||||
case indexing(completedResources: Int, totalResources: Int)
|
||||
case paused
|
||||
case ready
|
||||
case failed(RDAIError)
|
||||
}
|
||||
|
||||
public struct RDAIIndexOptions: Sendable {
|
||||
public var scope: RDAIReadScope
|
||||
public var priorityResource: RDAIResourceIdentifier?
|
||||
public var allowsEmbeddingAssetDownload: Bool
|
||||
}
|
||||
|
||||
public protocol RDAIIndexing: AnyObject, Sendable {
|
||||
func prepareIndex(options: RDAIIndexOptions) async throws
|
||||
func pauseIndexing() async
|
||||
func resumeIndexing() async
|
||||
func indexState() async -> RDAIIndexState
|
||||
func stateUpdates() async -> AsyncStream<RDAIIndexState>
|
||||
func removeIndex() async throws
|
||||
}
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- `prepareIndex` 幂等。
|
||||
- 重复调用只能扩大范围或提高优先级,不能创建重复 Job。
|
||||
- `removeIndex` 删除索引、实体和生成缓存,但不删除原书或用户笔记。
|
||||
|
||||
## 8. 能力与可用性
|
||||
|
||||
```swift
|
||||
public enum RDAICapability: String, Codable, Sendable {
|
||||
case languageAnalysis
|
||||
case entityExtraction
|
||||
case lexicalSearch
|
||||
case semanticSearch
|
||||
case summarization
|
||||
case questionAnswering
|
||||
case characterRelationships
|
||||
}
|
||||
|
||||
public enum RDAIUnavailableReason: Equatable, Sendable {
|
||||
case operatingSystemUnsupported
|
||||
case deviceNotEligible
|
||||
case appleIntelligenceNotEnabled
|
||||
case modelNotReady
|
||||
case languageUnsupported(String?)
|
||||
case embeddingAssetsUnavailable
|
||||
case providerNotInstalled
|
||||
case unknown(String)
|
||||
}
|
||||
|
||||
public enum RDAICapabilityAvailability: Equatable, Sendable {
|
||||
case available
|
||||
case degraded(reason: RDAIUnavailableReason)
|
||||
case unavailable(reason: RDAIUnavailableReason)
|
||||
}
|
||||
|
||||
public protocol RDAICapabilityProviding: Sendable {
|
||||
func availability(
|
||||
for capability: RDAICapability,
|
||||
locale: Locale?
|
||||
) async -> RDAICapabilityAvailability
|
||||
}
|
||||
```
|
||||
|
||||
UI 只能根据枚举状态展示文案,不能匹配本地化 Error 字符串。
|
||||
|
||||
## 9. 检索 API
|
||||
|
||||
```swift
|
||||
public struct RDAIRetrievalOptions: Sendable {
|
||||
public var maximumResults: Int
|
||||
public var scope: RDAIReadScope
|
||||
public var minimumScore: Double
|
||||
}
|
||||
|
||||
public struct RDAIRetrievalMatch: Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let passage: RDAIPassage
|
||||
public let score: Double
|
||||
public let lexicalScore: Double?
|
||||
public let semanticScore: Double?
|
||||
}
|
||||
|
||||
public protocol RDAIRetrieving: Sendable {
|
||||
func retrieve(
|
||||
query: String,
|
||||
options: RDAIRetrievalOptions
|
||||
) async throws -> [RDAIRetrievalMatch]
|
||||
}
|
||||
```
|
||||
|
||||
检索分数只用于同一索引版本内排序,不承诺跨版本数值稳定。
|
||||
|
||||
## 10. 生成结果
|
||||
|
||||
### 10.1 摘要
|
||||
|
||||
```swift
|
||||
public enum RDAISummaryLength: String, Codable, Sendable {
|
||||
case brief
|
||||
case standard
|
||||
case detailed
|
||||
}
|
||||
|
||||
public struct RDAISummary: Codable, Sendable {
|
||||
public let title: String
|
||||
public let overview: String
|
||||
public let keyPoints: [RDAISourcedStatement]
|
||||
public let citations: [RDAICitation]
|
||||
public let metadata: RDAIGenerationMetadata
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 问答
|
||||
|
||||
```swift
|
||||
public enum RDAIAnswerStatus: String, Codable, Sendable {
|
||||
case answered
|
||||
case insufficientEvidence
|
||||
case unsupportedLanguage
|
||||
case unavailable
|
||||
}
|
||||
|
||||
public struct RDAIAnswer: Codable, Sendable {
|
||||
public let status: RDAIAnswerStatus
|
||||
public let text: String
|
||||
public let statements: [RDAISourcedStatement]
|
||||
public let citations: [RDAICitation]
|
||||
public let metadata: RDAIGenerationMetadata
|
||||
}
|
||||
|
||||
public struct RDAISourcedStatement: Codable, Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let text: String
|
||||
public let citationIdentifiers: [String]
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 人物关系
|
||||
|
||||
```swift
|
||||
public enum RDAIRelationshipStatus: String, Codable, Sendable {
|
||||
case confirmed
|
||||
case possible
|
||||
case conflicting
|
||||
}
|
||||
|
||||
public struct RDAICharacter: Codable, Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let displayName: String
|
||||
public let aliases: [String]
|
||||
public let description: String
|
||||
public let firstAppearance: RDAICitation?
|
||||
public let evidence: [RDAICitation]
|
||||
}
|
||||
|
||||
public struct RDAIRelationship: Codable, Sendable, Identifiable {
|
||||
public let id: String
|
||||
public let sourceCharacterIdentifier: String
|
||||
public let targetCharacterIdentifier: String
|
||||
public let label: String
|
||||
public let status: RDAIRelationshipStatus
|
||||
public let evidence: [RDAICitation]
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 生成元数据
|
||||
|
||||
```swift
|
||||
public struct RDAIGenerationMetadata: Codable, Sendable {
|
||||
public let providerIdentifier: String
|
||||
public let modelVersion: String?
|
||||
public let promptIdentifier: String
|
||||
public let promptVersion: Int
|
||||
public let generatedAt: Date
|
||||
public let scopeHash: String
|
||||
}
|
||||
```
|
||||
|
||||
元数据用于缓存失效和问题追踪,不向普通用户展示内部 Prompt。
|
||||
|
||||
## 11. 高层服务
|
||||
|
||||
```swift
|
||||
public protocol RDAIReaderServicing: AnyObject, Sendable {
|
||||
func prepare(options: RDAIIndexOptions) async throws
|
||||
|
||||
func summarize(
|
||||
scope: RDAIReadScope,
|
||||
length: RDAISummaryLength
|
||||
) async throws -> RDAISummary
|
||||
|
||||
func answer(
|
||||
question: String,
|
||||
scope: RDAIReadScope
|
||||
) async throws -> RDAIAnswer
|
||||
|
||||
func characters(
|
||||
scope: RDAIReadScope
|
||||
) async throws -> [RDAICharacter]
|
||||
|
||||
func relationships(
|
||||
scope: RDAIReadScope
|
||||
) async throws -> [RDAIRelationship]
|
||||
|
||||
func removeAllAIData() async throws
|
||||
}
|
||||
```
|
||||
|
||||
建议具体实现为 actor。若用户开始新的同类请求,UI 层负责决定取消旧请求或并行;同一 Foundation Models session 不允许并行请求。
|
||||
|
||||
## 12. Provider 协议
|
||||
|
||||
```swift
|
||||
public struct RDAIGenerationRequest: Sendable {
|
||||
public let task: RDAIGenerationTask
|
||||
public let userText: String?
|
||||
public let passages: [RDAIPassage]
|
||||
public let locale: Locale
|
||||
public let scope: RDAIReadScope
|
||||
}
|
||||
|
||||
public enum RDAIGenerationTask: Sendable {
|
||||
case summary(RDAISummaryLength)
|
||||
case answer
|
||||
case characters
|
||||
case relationships
|
||||
}
|
||||
|
||||
public protocol RDAIGenerativeProvider: Sendable {
|
||||
var identifier: String { get }
|
||||
func availability(locale: Locale) async -> RDAICapabilityAvailability
|
||||
func generate(_ request: RDAIGenerationRequest) async throws -> RDAIGeneratedArtifact
|
||||
}
|
||||
```
|
||||
|
||||
`RDAIGeneratedArtifact` 是 Core 内部或受控公共枚举,用于把 Provider 输出转换为第 10 节模型。Provider 不能直接保存结果或操作 Reader UI。
|
||||
|
||||
## 13. 错误模型
|
||||
|
||||
```swift
|
||||
public enum RDAIError: Error, Equatable, Sendable {
|
||||
case invalidDocument
|
||||
case resourceUnavailable(RDAIResourceIdentifier)
|
||||
case staleCitation
|
||||
case indexingFailed(code: String)
|
||||
case modelUnavailable(RDAIUnavailableReason)
|
||||
case unsupportedLanguage(String?)
|
||||
case contextLimitExceeded
|
||||
case invalidGeneratedStructure
|
||||
case invalidCitation
|
||||
case insufficientEvidence
|
||||
case cancelled
|
||||
case storageFailure(code: String)
|
||||
}
|
||||
```
|
||||
|
||||
公共错误不携带原文或数据库底层错误字符串。内部错误映射为稳定 code,并通过本地诊断系统保存脱敏详情。
|
||||
|
||||
## 14. PDF Adapter
|
||||
|
||||
建议公开:
|
||||
|
||||
```swift
|
||||
public extension RDPDFReaderViewController {
|
||||
func makeAIContentProvider() -> RDPDFAIContentProvider
|
||||
func makeAIReaderService(
|
||||
configuration: RDAIReaderConfiguration = .default
|
||||
) throws -> RDAIReaderServicing
|
||||
}
|
||||
```
|
||||
|
||||
映射要求:
|
||||
|
||||
- 页面资源 ID 为十进制页索引。
|
||||
- 文本顺序使用 `RDPDFReaderTextRun.readingOrder`。
|
||||
- `normalizedRects` 转换为 `RDAINormalizedRect`。
|
||||
- 原生文本标记为 `.native`,Vision OCR 标记为 `.ocr`。
|
||||
- AI 高亮复用或泛化现有 speech highlight,不同时维护两个相互覆盖的临时层。
|
||||
|
||||
## 15. EPUB Adapter
|
||||
|
||||
建议公开:
|
||||
|
||||
```swift
|
||||
public extension RDEPUBReaderController {
|
||||
func makeAIContentProvider() -> RDEPUBAIContentProvider
|
||||
func makeAIReaderService(
|
||||
configuration: RDAIReaderConfiguration = .default
|
||||
) throws -> RDAIReaderServicing
|
||||
}
|
||||
```
|
||||
|
||||
映射要求:
|
||||
|
||||
- 资源 ID 使用 ResourceResolver 规范化后的 `href`。
|
||||
- Passage 范围从章节 attributed content 的原始字符串计算。
|
||||
- 使用现有 index table 生成 `cfi` 和 `rangeCFI`。
|
||||
- 导航复用 `go(to:)`/位置恢复流程。
|
||||
- 固定版式或无法提取文本的章节返回明确 unavailable,不制造空 Passage。
|
||||
|
||||
## 16. TTS 集成
|
||||
|
||||
AI 层不依赖 RDSpeechReaderView。宿主可以把摘要或回答转换为临时 `RDSpeechContentProvider`。
|
||||
|
||||
后续可增加桥接 Pod:
|
||||
|
||||
```ruby
|
||||
pod 'RDSpeechReaderView/AI'
|
||||
```
|
||||
|
||||
桥接只负责朗读 AI 结果;Natural Language 的分句与语言识别实现应抽取为共享内部组件,避免同一文本产生不同范围。
|
||||
|
||||
## 17. 配置
|
||||
|
||||
```swift
|
||||
public struct RDAIReaderConfiguration: Sendable {
|
||||
public var spoilerPolicy: RDAISpoilerPolicy
|
||||
public var maximumRetrievedPassages: Int
|
||||
public var allowsEmbeddingAssetDownload: Bool
|
||||
public var storesGeneratedArtifacts: Bool
|
||||
public var diagnosticsLevel: RDAIDiagnosticsLevel
|
||||
|
||||
public static let `default`: RDAIReaderConfiguration
|
||||
}
|
||||
```
|
||||
|
||||
默认值:
|
||||
|
||||
- `spoilerPolicy = .readContentOnly`
|
||||
- `maximumRetrievedPassages = 4`
|
||||
- `allowsEmbeddingAssetDownload = false`
|
||||
- `storesGeneratedArtifacts = true`
|
||||
- `diagnosticsLevel = .metadataOnly`
|
||||
|
||||
## 18. API 演进规则
|
||||
|
||||
- 1.0 前可以调整命名,但每次调整同步更新五份设计文档。
|
||||
- 1.0 后删除或改变语义需要主版本升级。
|
||||
- Codable 枚举新增 case 时必须实现向后兼容策略。
|
||||
- 数据库 Schema 版本与 SDK 版本独立。
|
||||
- Prompt 版本与 SDK 版本独立。
|
||||
- Reader Adapter 可以增加格式能力,但不能改变 Core Locator 的 UTF-16 语义。
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
# RDAIReaderView 架构设计
|
||||
|
||||
**文档状态:** Draft 0.1
|
||||
**最后更新:** 2026-07-25
|
||||
**目标版本:** RDAIReaderView 1.0
|
||||
|
||||
## 1. 架构目标
|
||||
|
||||
RDAIReaderView 必须满足四个架构目标:
|
||||
|
||||
1. 不改变 RDPDFReaderView、RDEpubReaderView 和 RDSpeechReaderView 的核心职责。
|
||||
2. iOS 15 用户继续获得稳定阅读、基础 NLP 和 TTS;Foundation Models 仅作为可选增强。
|
||||
3. 所有生成结果都能追溯到稳定原文位置。
|
||||
4. AI Provider、索引实现和 UI 可替换,公共数据模型保持稳定。
|
||||
|
||||
## 2. 总体分层
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Host App / RDAIReaderViewUI │
|
||||
│ AI 面板、摘要、问答、人物卡片、关系图、引用跳转 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ RDAIReaderView │
|
||||
│ Query Service / Summary Service / Character Service │
|
||||
├───────────────────────┬─────────────────────────────────────┤
|
||||
│ NaturalLanguage │ FoundationModels │
|
||||
│ 分句/语言/实体/检索 │ 结构化生成/工具调用/拒答 │
|
||||
├───────────────────────┴─────────────────────────────────────┤
|
||||
│ Index & Storage │
|
||||
│ Passage / EntityMention / Citation / Artifact / Job │
|
||||
├───────────────────────┬─────────────────────────────────────┤
|
||||
│ RDPDFReaderView/AI │ RDEpubReaderView/AI │
|
||||
│ 页码/范围/矩形/OCR │ href/CFI/范围/章节 │
|
||||
├───────────────────────┴─────────────────────────────────────┤
|
||||
│ RDPDFReaderView / RDEpubReaderView / RDSpeechReaderView │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
依赖方向只能从上到下。阅读器核心不能反向依赖 RDAIReaderView。
|
||||
|
||||
## 3. CocoaPods 模块设计
|
||||
|
||||
建议新增:
|
||||
|
||||
```ruby
|
||||
pod 'RDAIReaderView/Core'
|
||||
pod 'RDAIReaderView/NaturalLanguage'
|
||||
pod 'RDAIReaderView/FoundationModels'
|
||||
pod 'RDAIReaderView/UI'
|
||||
pod 'RDPDFReaderView/AI'
|
||||
pod 'RDEpubReaderView/AI'
|
||||
```
|
||||
|
||||
### 3.1 Core
|
||||
|
||||
- 最低 iOS 15。
|
||||
- 只依赖 Foundation、SQLite3/CryptoKit 等系统能力。
|
||||
- 包含公共协议、数据模型、索引调度、存储和查询编排。
|
||||
- 不导入 UIKit、PDFKit、DTCoreText 或 FoundationModels。
|
||||
|
||||
### 3.2 NaturalLanguage
|
||||
|
||||
- 最低 iOS 15。
|
||||
- 依赖 Core 和 NaturalLanguage。
|
||||
- 提供语言识别、分句、实体候选、关键词和语义评分。
|
||||
- iOS 17+ 可选使用 `NLContextualEmbedding`;资源不可用时降级。
|
||||
|
||||
### 3.3 FoundationModels
|
||||
|
||||
- 源码使用 `@available(iOS 26.0, *)` 隔离。
|
||||
- 依赖 Core 和系统 FoundationModels。
|
||||
- 需要支持 Foundation Models 的 Xcode 工具链。
|
||||
- 不被基础 Pod 默认引入,避免旧工具链客户无法编译。
|
||||
|
||||
### 3.4 UI
|
||||
|
||||
- 最低 iOS 15。
|
||||
- 依赖 Core,可选识别 FoundationModels 可用性。
|
||||
- UI 不直接构造 Prompt,也不直接访问数据库。
|
||||
|
||||
### 3.5 Reader Adapters
|
||||
|
||||
- `RDPDFReaderView/AI` 依赖 RDAIReaderView/Core。
|
||||
- `RDEpubReaderView/AI` 依赖 RDAIReaderView/Core。
|
||||
- Adapter 只负责内容快照、定位转换、跳转和高亮。
|
||||
|
||||
## 4. 建议目录
|
||||
|
||||
```text
|
||||
Sources/RDAIReaderView/
|
||||
├── RDAIReaderView.podspec
|
||||
├── Core/
|
||||
│ ├── Contracts/
|
||||
│ ├── Models/
|
||||
│ ├── Indexing/
|
||||
│ ├── Retrieval/
|
||||
│ ├── Storage/
|
||||
│ └── Services/
|
||||
├── NaturalLanguage/
|
||||
│ ├── Analysis/
|
||||
│ ├── Embeddings/
|
||||
│ └── Retrieval/
|
||||
├── FoundationModels/
|
||||
│ ├── Availability/
|
||||
│ ├── Generation/
|
||||
│ ├── Prompts/
|
||||
│ ├── Schemas/
|
||||
│ └── Tools/
|
||||
├── UI/
|
||||
│ ├── Assistant/
|
||||
│ ├── Citations/
|
||||
│ └── Characters/
|
||||
└── Tests/
|
||||
|
||||
Sources/RDPDFReaderView/AI/
|
||||
Sources/RDEpubReaderView/AI/
|
||||
```
|
||||
|
||||
## 5. 核心数据流
|
||||
|
||||
### 5.1 建立索引
|
||||
|
||||
```text
|
||||
ContentProvider
|
||||
↓ 读取资源快照
|
||||
Text Snapshot + Stable Locator
|
||||
↓
|
||||
Language Detection
|
||||
↓
|
||||
Paragraph/Sentence Chunking
|
||||
↓
|
||||
Entity Mentions + Keywords + Embeddings
|
||||
↓
|
||||
Transactional Storage
|
||||
↓
|
||||
Index Checkpoint
|
||||
```
|
||||
|
||||
每次数据库事务只提交一个资源或一组有界片段。应用退出时,最多重做当前事务,不重做整本书。
|
||||
|
||||
### 5.2 问答
|
||||
|
||||
```text
|
||||
User Question
|
||||
↓
|
||||
Language / Intent / Spoiler Scope
|
||||
↓
|
||||
Hybrid Retrieval
|
||||
↓
|
||||
Access + Read-Progress Filter
|
||||
↓
|
||||
Context Budget Builder
|
||||
↓
|
||||
Foundation Models Structured Generation
|
||||
↓
|
||||
Citation Validator
|
||||
↓
|
||||
Answer or Evidence-Insufficient Refusal
|
||||
```
|
||||
|
||||
检索由代码执行。首版不让模型自由遍历整本数据库,只有需要多轮查找时才使用受限 Tool Calling。
|
||||
|
||||
### 5.3 人物关系
|
||||
|
||||
```text
|
||||
Entity Mentions
|
||||
↓
|
||||
Alias Candidate Grouping
|
||||
↓
|
||||
Chunk-level Structured Extraction
|
||||
↓
|
||||
Evidence Validation
|
||||
↓
|
||||
Relationship Merge
|
||||
↓
|
||||
confirmed / possible / conflicting
|
||||
```
|
||||
|
||||
代码只能自动合并完全相同的标准化名称和明确别名。代词消解、同名人物合并和隐含关系必须保持低置信度,等待更多证据或用户确认。
|
||||
|
||||
## 6. 稳定定位模型
|
||||
|
||||
### 6.1 通用定位
|
||||
|
||||
`RDAILocator` 包含:
|
||||
|
||||
- `documentIdentifier`
|
||||
- `resourceIdentifier`
|
||||
- `utf16Range`
|
||||
- `sourceHash`
|
||||
- 格式专属 Anchor
|
||||
|
||||
文本范围统一使用 UTF-16,与现有 RDSpeechReaderView、NSString 和 EPUB 搜索范围保持一致。
|
||||
|
||||
### 6.2 PDF Anchor
|
||||
|
||||
```text
|
||||
pageIndex
|
||||
normalizedRects
|
||||
textSource: native / ocr
|
||||
readingOrder
|
||||
```
|
||||
|
||||
PDF Adapter 从现有 `RDPDFReaderTextRun` 构建连续页文本和 UTF-16 范围。每个 Passage 必须保留与 run 的映射,不能在 AI 层重新拼接后丢失矩形。
|
||||
|
||||
扫描 PDF 使用现有 `speechTextRuns(at:)`/OCR 能力,但商用实现应增加独立的 AI OCR 调度入口,避免页面导航取消请求时同时取消后台索引。
|
||||
|
||||
### 6.3 EPUB Anchor
|
||||
|
||||
```text
|
||||
normalizedHref
|
||||
cfi
|
||||
rangeCFI
|
||||
rangeAnchor
|
||||
progressionFallback
|
||||
```
|
||||
|
||||
优先级:
|
||||
|
||||
1. `rangeCFI`
|
||||
2. `cfi + UTF-16 range`
|
||||
3. `rangeAnchor`
|
||||
4. `href + progression`
|
||||
|
||||
屏幕页码只用于展示,不能作为持久化引用主键。
|
||||
|
||||
## 7. 文本快照与分块
|
||||
|
||||
### 7.1 不修改原文
|
||||
|
||||
索引保存两份文本信息:
|
||||
|
||||
- `sourceText`:原始文本,用于引用与范围映射。
|
||||
- `searchText`:规范化副本,用于检索。
|
||||
|
||||
禁止使用规范化文本范围直接驱动阅读器高亮。
|
||||
|
||||
### 7.2 分块策略
|
||||
|
||||
- 先按资源和章节边界划分。
|
||||
- 再按段落划分。
|
||||
- 超长段落使用 `NLTokenizer(unit: .sentence)`。
|
||||
- 中文目标 600-900 字;英文目标 300-600 词。
|
||||
- 片段之间保留 1-2 句重叠。
|
||||
- 表格、代码、脚注和标题保留语义类型,避免与正文无差别拼接。
|
||||
|
||||
### 7.3 内容哈希
|
||||
|
||||
建议使用 SHA-256:
|
||||
|
||||
```text
|
||||
documentHash = hash(ordered resource identifiers + resource hashes)
|
||||
resourceHash = hash(source text + format-specific stable metadata)
|
||||
passageHash = hash(resource hash + UTF-16 range + source text)
|
||||
```
|
||||
|
||||
书籍更新时按资源哈希增量失效。
|
||||
|
||||
## 8. 检索架构
|
||||
|
||||
首版采用 Hybrid Retrieval:
|
||||
|
||||
```text
|
||||
finalScore =
|
||||
0.45 * lexicalScore +
|
||||
0.40 * semanticScore +
|
||||
0.10 * proximityScore +
|
||||
0.05 * headingBoost
|
||||
```
|
||||
|
||||
权重是初始值,必须通过评测集调优,不作为永久常量。
|
||||
|
||||
检索步骤:
|
||||
|
||||
1. 规范化查询并识别语言。
|
||||
2. 关键词倒排召回 Top 30。
|
||||
3. 语义相似度重排。
|
||||
4. 合并高度重叠 Passage。
|
||||
5. 按已读范围、文档授权和最大上下文过滤。
|
||||
6. 返回 Top 3-4,并保留评分解释。
|
||||
|
||||
若语义模型资源不可用,使用纯词法检索,不阻塞问答入口;UI 可提示结果质量可能降低。
|
||||
|
||||
## 9. Foundation Models 编排
|
||||
|
||||
### 9.1 Provider 抽象
|
||||
|
||||
Core 只依赖 `RDAIGenerativeProvider`。Apple 实现位于 FoundationModels 子模块,未来可增加 Core ML、MLX 或经用户授权的云端实现。
|
||||
|
||||
### 9.2 会话策略
|
||||
|
||||
- 摘要、问答、人物关系使用不同 instructions 和独立会话。
|
||||
- 同一会话只处理一个并发请求。
|
||||
- 用户切换书籍、变更阅读范围或取消时终止任务。
|
||||
- 达到上下文阈值前主动新建会话,不等待系统抛错。
|
||||
- 记录 Prompt 版本、模型可用性分类、耗时和 token 数,不记录原文。
|
||||
|
||||
### 9.3 上下文预算
|
||||
|
||||
默认预算建议:
|
||||
|
||||
| 项目 | Token 预算 |
|
||||
|------|------------|
|
||||
| Instructions + Schema | 500 |
|
||||
| 用户问题 | 200 |
|
||||
| 检索上下文 | 2400 |
|
||||
| 模型输出 | 700 |
|
||||
| 安全余量 | 296 |
|
||||
|
||||
实际使用 `contextSize` 和 `tokenCount(for:)` 动态计算,不写死为 4096。
|
||||
|
||||
### 9.4 引用验证
|
||||
|
||||
生成后执行确定性校验:
|
||||
|
||||
1. Citation ID 必须存在于本次上下文。
|
||||
2. Citation 必须属于当前文档和允许阅读范围。
|
||||
3. 引用文本必须能在 Passage 原文中匹配。
|
||||
4. 每个事实项至少有一个有效引用。
|
||||
5. 删除无效项后答案为空,则返回 evidence insufficient。
|
||||
|
||||
## 10. 存储设计
|
||||
|
||||
建议使用 SQLite,Schema 初稿:
|
||||
|
||||
```text
|
||||
documents
|
||||
resources
|
||||
passages
|
||||
passage_fts
|
||||
embeddings
|
||||
entity_mentions
|
||||
entities
|
||||
entity_aliases
|
||||
relationships
|
||||
relationship_evidence
|
||||
artifacts
|
||||
index_jobs
|
||||
schema_metadata
|
||||
```
|
||||
|
||||
关键原则:
|
||||
|
||||
- FTS 和关系表通过 Passage ID 关联原文。
|
||||
- 向量数据按模型标识符、revision 和语言分区。
|
||||
- 生成结果不覆盖人工编辑内容。
|
||||
- 每个 Artifact 保存 Prompt/模型/输入哈希。
|
||||
- 索引表支持按文档级联删除。
|
||||
|
||||
## 11. 并发与生命周期
|
||||
|
||||
建议采用 Swift Concurrency:
|
||||
|
||||
- `RDAIIndexCoordinator`:actor,管理索引队列和 checkpoint。
|
||||
- `RDAIStore`:actor,串行化数据库写入。
|
||||
- `RDAIRetriever`:Sendable 服务,可并发读取快照。
|
||||
- Reader Adapter:`@MainActor`,仅提取 UI/阅读器状态和执行跳转。
|
||||
- Foundation Models Session:由单请求 actor 或服务隔离。
|
||||
|
||||
优先级:
|
||||
|
||||
1. 用户当前问答所需 Passage。
|
||||
2. 当前章节。
|
||||
3. 相邻章节。
|
||||
4. 已读范围。
|
||||
5. 其余获准内容。
|
||||
|
||||
发生内存警告时:
|
||||
|
||||
- 取消低优先级 embedding 任务。
|
||||
- 卸载 contextual embedding。
|
||||
- 清理内存 Passage/向量缓存。
|
||||
- 保留已提交数据库和当前用户请求。
|
||||
|
||||
## 12. 可用性与降级
|
||||
|
||||
```text
|
||||
Foundation Models available
|
||||
├─ 是 → 完整生成能力
|
||||
└─ 否
|
||||
├─ Natural Language available → 索引、实体、基础检索
|
||||
└─ 语言/资源不支持 → 关键词检索与基础分段
|
||||
```
|
||||
|
||||
降级状态是公共 API 的一部分,UI 不根据 Error 字符串猜测原因。
|
||||
|
||||
## 13. 安全与隐私
|
||||
|
||||
- 默认 Provider 为设备端 Provider。
|
||||
- Core 不包含网络代码。
|
||||
- 云端 Provider 若未来增加,必须是单独 Pod,并要求宿主显式配置。
|
||||
- 日志只记录文档匿名哈希、阶段、耗时、错误类别和计数。
|
||||
- 禁止记录 Prompt、Passage、用户问题和模型回答全文。
|
||||
- 导出诊断包前再次脱敏。
|
||||
- 删除书籍时由宿主调用 `removeDocument`,同时删除索引、关系和生成缓存。
|
||||
|
||||
## 14. 可观测性
|
||||
|
||||
本地指标:
|
||||
|
||||
- 索引耗时、资源数、片段数、失败类别。
|
||||
- 检索 P50/P95、召回数量和降级模式。
|
||||
- 生成耗时、取消率、错误类别和引用校验失败率。
|
||||
- Foundation Models availability 分布。
|
||||
- 缓存命中率和数据库大小。
|
||||
|
||||
商用版本默认只汇总数值。任何远程遥测必须由宿主决定,并遵守其隐私政策。
|
||||
|
||||
## 15. 架构决策记录
|
||||
|
||||
实施时至少补充以下 ADR:
|
||||
|
||||
- ADR-001:独立 RDAIReaderView 而非嵌入阅读器核心。
|
||||
- ADR-002:UTF-16 作为跨模块文本范围。
|
||||
- ADR-003:本地 SQLite 和增量资源哈希。
|
||||
- ADR-004:检索先行、生成后置、引用强校验。
|
||||
- ADR-005:Foundation Models 可选依赖和运行时降级。
|
||||
- ADR-006:已读范围作为默认安全边界。
|
||||
|
||||
## 16. 已知技术风险
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
|------|------|------|
|
||||
| 中文小说实体识别不足 | 人物漏识别或误合并 | 规则候选 + 结构化模型提取 + 证据与置信度 |
|
||||
| OCR 阅读顺序错误 | 摘要和引用错误 | 保留 readingOrder、双栏测试、允许宿主提供文本 |
|
||||
| PDF OCR 请求被页面导航取消 | 后台索引不完整 | AI 使用独立调度队列与缓存 |
|
||||
| EPUB 重排后范围变化 | 引用跳转漂移 | CFI/rangeCFI 优先,文本哈希校验 |
|
||||
| 系统模型更新 | Prompt 质量回归 | Prompt 版本化、模型版本分层评测 |
|
||||
| 上下文不足 | 回答遗漏 | 检索压缩、分层摘要、新会话 |
|
||||
| 同名人物 | 错误关系合并 | 不自动合并低置信候选,保留冲突 |
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# RDAIReaderView Release Checklist
|
||||
|
||||
This checklist records the external validation required after the local build
|
||||
passes. It intentionally contains no book text, prompts, answers or user data.
|
||||
|
||||
> Status on 2026-07-25: Apple Intelligence eligible hardware, App Store privacy
|
||||
> submission and TestFlight rollout access are temporarily unavailable. The
|
||||
> unchecked physical-device and release gates below are deferred, not passed.
|
||||
|
||||
## Local Gates
|
||||
|
||||
- [x] Core, NaturalLanguage, FoundationModels and UI compile for iPhoneOS.
|
||||
- [x] PDF and EPUB adapters compile in the ReadViewDemo workspace.
|
||||
- [x] PDF/EPUB citations use stable locators and transient highlights.
|
||||
- [x] SQLite data, FTS and generated artifacts are deleted with a book.
|
||||
- [ ] Add fixture-backed XCTest coverage for locators, SQLite migration, scope,
|
||||
citation validation, pause/resume and artifact invalidation.
|
||||
- [ ] Add UI automation for index, summary, answer, citation jump, clear data
|
||||
and VoiceOver labels.
|
||||
|
||||
## Physical Device Gates
|
||||
|
||||
- [ ] On an eligible iOS 26 device, verify each `SystemLanguageModel`
|
||||
availability state and its UI fallback.
|
||||
- [ ] Run structured summary and question-answering evaluation fixtures; record
|
||||
citation validity, faithfulness and spoiler-safety without uploading text.
|
||||
- [ ] Verify Vision OCR quality and cancellation/retry on scanned PDF samples.
|
||||
- [ ] Measure index latency, memory, battery and citation jump P95 targets.
|
||||
|
||||
## Release Gates
|
||||
|
||||
- [ ] Create a feature flag/remote configuration policy owned by the host app.
|
||||
- [ ] Complete App Store privacy labels, Foundation Models acceptable-use review
|
||||
and network audit.
|
||||
- [ ] Run TestFlight rollout 5%, 25%, then 100% with P0/P1 stop conditions.
|
||||
- [ ] Confirm crash-free and AI quality metrics meet the published thresholds.
|
||||
@@ -0,0 +1,287 @@
|
||||
# RDAIReaderView 产品与开发规格
|
||||
|
||||
**文档状态:** Draft 0.1
|
||||
**最后更新:** 2026-07-25
|
||||
**目标版本:** RDAIReaderView 1.0
|
||||
**适用工程:** ReadViewSDK
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文档锁定 RDAIReaderView 首个商用版本的产品范围、系统兼容策略、功能要求、非功能要求和发布门槛。架构、公共 API、生成式 AI 约束和测试方法分别见同目录其他文档。
|
||||
|
||||
## 2. 背景与现状
|
||||
|
||||
ReadViewSDK 已具备以下基础:
|
||||
|
||||
- RDPDFReaderView:PDF 页面、原生文本、Vision OCR、页内文本矩形、跳页与临时朗读高亮。
|
||||
- RDEpubReaderView:EPUB 章节、`href`、CFI、UTF-16 文本范围、搜索与位置恢复。
|
||||
- RDSpeechReaderView:基于 Natural Language 的语言识别和分句、系统 TTS、断点续听、后台播放与锁屏控制。
|
||||
- 最低部署版本为 iOS 15,Demo 使用 iOS 15.6。
|
||||
|
||||
当前缺少统一的文档语义索引、可追溯引用、书内问答、章节摘要和人物关系能力。
|
||||
|
||||
## 3. 产品目标
|
||||
|
||||
RDAIReaderView 1.0 的目标是提供一个默认本地运行、可选启用 Apple Foundation Models、可被 PDF 和 EPUB 阅读器复用的智能阅读能力层。
|
||||
|
||||
首版必须实现:
|
||||
|
||||
1. 对书籍内容建立增量、可恢复的本地索引。
|
||||
2. 识别语言、句子、人物、地点、组织和关键词。
|
||||
3. 生成当前章节或已读范围摘要。
|
||||
4. 回答书内问题,并为事实性陈述提供可跳转的原文引用。
|
||||
5. 生成人物卡片和带证据的人物关系。
|
||||
6. Foundation Models 不可用时安全降级,不影响阅读、搜索和 TTS。
|
||||
7. 所有 AI 结果默认限制在用户已读内容,避免剧透。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
RDAIReaderView 1.0 不包含:
|
||||
|
||||
- 互联网百科、新闻或通用知识问答。
|
||||
- 无来源的文学评价、复杂逻辑推理或事实推断。
|
||||
- 自动改写、续写或批量导出版权书籍内容。
|
||||
- 云端上传书籍全文。
|
||||
- 自训练 Foundation Models Adapter。
|
||||
- 自动替用户发布内容、发送消息或执行购买行为。
|
||||
- 对 CBZ、漫画图片内容进行视觉剧情理解。
|
||||
- 对 DRM 内容绕过访问控制或持久化超出宿主授权范围的文本。
|
||||
|
||||
上述能力必须在后续版本单独评审产品价值、版权、隐私和审核风险。
|
||||
|
||||
## 5. 用户价值与首版场景
|
||||
|
||||
### 5.1 阅读回顾
|
||||
|
||||
用户重新打开书籍时,可请求:
|
||||
|
||||
- 上次阅读内容的 3 句回顾。
|
||||
- 当前章节摘要。
|
||||
- 已读部分的关键人物变化。
|
||||
|
||||
输入范围默认从最近一个自然章节边界到当前阅读位置,不得包含未读段落。
|
||||
|
||||
### 5.2 书内问答
|
||||
|
||||
用户可以询问当前书籍,例如:
|
||||
|
||||
- “这一章发生了什么?”
|
||||
- “张三为什么离开?”
|
||||
- “这里提到的组织是什么?”
|
||||
|
||||
系统先检索相关段落,再基于检索结果生成答案。答案中的事实性结论必须关联一个或多个 `RDAICitation`。没有充分证据时必须明确拒答,而不是依赖模型常识补全。
|
||||
|
||||
### 5.3 人物与关系
|
||||
|
||||
人物卡片包含:
|
||||
|
||||
- 标准显示名和已发现的别名。
|
||||
- 首次出现位置。
|
||||
- 仅基于已读内容的简短介绍。
|
||||
- 关键行为及证据。
|
||||
- 与其他人物的关系边及证据。
|
||||
|
||||
关系状态分为:
|
||||
|
||||
- `confirmed`:原文明确表达。
|
||||
- `possible`:模型推断但证据不充分,UI 必须显示“可能”。
|
||||
- `conflicting`:不同段落给出冲突信息,UI 展示冲突而非自动覆盖。
|
||||
|
||||
### 5.4 与阅读器联动
|
||||
|
||||
- 点击引用跳转到 PDF 页面或 EPUB CFI。
|
||||
- 跳转后高亮对应原文范围。
|
||||
- 摘要、答案和人物卡片可交给 RDSpeechReaderView 朗读。
|
||||
- 用户调整字体、页面尺寸或翻页方式后,EPUB 引用仍应通过 CFI/文本范围恢复。
|
||||
|
||||
## 6. 功能要求
|
||||
|
||||
### FR-001 文档导入
|
||||
|
||||
- AI 层只能通过 `RDAIContentProvider` 读取宿主已授权的文本快照。
|
||||
- 不直接读取宿主数据库或下载接口。
|
||||
- 支持取消导入、增量恢复和内容变更检测。
|
||||
|
||||
### FR-002 稳定定位
|
||||
|
||||
- PDF:使用文档 ID、页索引、页内 UTF-16 范围和归一化矩形。
|
||||
- EPUB:使用文档 ID、规范化 `href`、UTF-16 范围和 CFI;CFI 不可用时才使用 progression 兜底。
|
||||
- 所有引用必须保存源文本哈希,恢复时验证引用是否仍指向相同内容。
|
||||
|
||||
### FR-003 文本分析
|
||||
|
||||
- 使用 `NLLanguageRecognizer` 识别资源或段落语言。
|
||||
- 使用 `NLTokenizer` 切分句子,保持原始 UTF-16 偏移。
|
||||
- 使用 `NLTagger` 生成人名、地点、组织候选。
|
||||
- 实体候选必须保留每次出现的原文位置,不能只保存名称。
|
||||
- Natural Language 不支持或质量不足的语言,降级为字符/标点分段和关键词检索。
|
||||
|
||||
### FR-004 分块与索引
|
||||
|
||||
- 中文片段建议 600-900 个字符;拉丁文字建议 300-600 词。
|
||||
- 优先在章节、段落和句子边界切分,不截断组合字符。
|
||||
- 相邻片段保留 1-2 句重叠,便于跨边界检索。
|
||||
- 每个片段保存内容哈希、语言、顺序、定位和索引版本。
|
||||
- 索引任务在后台执行,并按当前章节、相邻章节、其余内容的优先级处理。
|
||||
|
||||
### FR-005 检索
|
||||
|
||||
- 首版采用关键词/BM25 风格评分与 Natural Language 语义相似度融合。
|
||||
- 结果必须经过文档 ID、已读范围和访问范围过滤。
|
||||
- 默认返回 3-4 个片段,最大不超过 5 个。
|
||||
- 检索结果必须包含分数、匹配原因和稳定定位。
|
||||
|
||||
### FR-006 Foundation Models 可用性
|
||||
|
||||
- 编译期使用可选子模块,不提高 Core 的 iOS 15 最低版本。
|
||||
- 运行时检查系统版本、设备资格、Apple Intelligence 开关、模型准备状态和语言支持。
|
||||
- UI 必须区分 `deviceNotEligible`、`appleIntelligenceNotEnabled`、`modelNotReady`、不支持语言和未知错误。
|
||||
- 不可用时隐藏生成入口或提供明确说明,Natural Language 索引、搜索和 TTS 保持可用。
|
||||
|
||||
### FR-007 结构化生成
|
||||
|
||||
- 摘要、答案、实体和关系均使用 `@Generable` 结构化输出。
|
||||
- 输出不得依赖字符串正则解析。
|
||||
- 每个事实项必须携带检索片段 ID;生成后由代码验证 ID 是否真实存在。
|
||||
- 无效引用、越权引用或未读范围引用必须删除;删除后答案无证据则转为拒答。
|
||||
|
||||
### FR-008 防剧透
|
||||
|
||||
- 默认分析范围为“当前位置及之前”。
|
||||
- 用户主动切换到整本书模式时必须进行一次明确确认。
|
||||
- 缓存键包含阅读范围;已读摘要不得复用整本书摘要。
|
||||
- 人物关系图默认只展示已读范围内已出现的人物和关系。
|
||||
|
||||
### FR-009 缓存与恢复
|
||||
|
||||
- 索引、实体、摘要和问答缓存均存储在应用沙盒。
|
||||
- 缓存键包含文档内容哈希、索引版本、Prompt 版本、模型版本和阅读范围。
|
||||
- 内容哈希变化时,失效受影响资源,不强制删除整本书其他有效索引。
|
||||
- 提供按书删除、删除全部 AI 数据和存储空间统计接口。
|
||||
|
||||
### FR-010 用户控制
|
||||
|
||||
- 所有长任务支持取消。
|
||||
- UI 展示索引或生成状态,不伪造确定进度。
|
||||
- 用户可关闭 AI、清除 AI 缓存、选择“仅本地处理”。
|
||||
- 生成失败不得阻塞翻页、搜索、标注或 TTS。
|
||||
|
||||
## 7. 系统兼容矩阵
|
||||
|
||||
| 环境 | 必须提供的能力 |
|
||||
|------|----------------|
|
||||
| iOS 15+ | 语言识别、分句、实体候选、关键词索引、基础检索 |
|
||||
| iOS 17+ | 可选 contextual embedding;资源不存在时允许下载或降级 |
|
||||
| iOS 26+ 且模型可用 | 摘要、问答、人物关系、结构化笔记 |
|
||||
| 不支持 Apple Intelligence | Natural Language 能力完整可用,生成式入口降级 |
|
||||
| 离线 | 已下载模型与本地索引可用;不得要求联网 |
|
||||
| 扫描 PDF | 使用现有 Vision OCR;OCR 失败的页面明确标记不可分析 |
|
||||
|
||||
## 8. 非功能要求
|
||||
|
||||
### 8.1 性能
|
||||
|
||||
- 索引不得在主线程执行文本分析、向量计算或数据库批量写入。
|
||||
- 当前章节索引优先完成,目标 P95 不超过 2 秒;具体阈值以目标真机基线校准。
|
||||
- 10 万中文字的基础索引目标 P95 不超过 30 秒,允许后台增量完成。
|
||||
- 索引期间阅读页面滚动/翻页帧率不得出现持续性下降。
|
||||
- 单次 Foundation Models 响应首个可展示结果目标 P95 不超过 5 秒。
|
||||
- AI 模块空闲 30 秒后应释放 contextual embedding 和不必要的内存缓存。
|
||||
|
||||
### 8.2 稳定性
|
||||
|
||||
- AI 相关 crash-free session 不低于 99.9%。
|
||||
- Foundation Models 不可用或生成失败时降级成功率为 100%。
|
||||
- 强制退出后索引可从最后一个已提交资源恢复。
|
||||
- 数据库迁移失败时保留原数据库备份,并允许重建索引。
|
||||
|
||||
### 8.3 准确性
|
||||
|
||||
- 引用定位有效率不低于 99%。
|
||||
- 事实性陈述有原文支持的比例不低于 98%。
|
||||
- 无答案问题正确拒答率不低于 95%。
|
||||
- 人物关系证据覆盖率为 100%。
|
||||
- 结构化输出通过本地校验的比例不低于 99.5%。
|
||||
|
||||
### 8.4 隐私与安全
|
||||
|
||||
- 默认不上传书籍文本、查询、摘要、人物关系和阅读历史。
|
||||
- 日志不得包含原文、用户问题全文或模型完整输出。
|
||||
- 调试日志必须经过显式编译配置才能包含脱敏片段。
|
||||
- AI 数据遵循宿主账户登出、删书和清除缓存生命周期。
|
||||
- 文件保护等级、备份策略和共享容器由宿主配置,SDK 提供明确接口和文档。
|
||||
|
||||
### 8.5 可访问性
|
||||
|
||||
- 所有 AI 控件支持 VoiceOver、Dynamic Type 和 Reduce Motion。
|
||||
- 状态变化使用可访问性公告,但不得连续播报索引细节。
|
||||
- “AI 生成”“可能关系”“无原文证据”等状态不能只靠颜色表达。
|
||||
|
||||
## 9. 商用验收门槛
|
||||
|
||||
以下条件全部满足后才可发布 1.0:
|
||||
|
||||
- Core、NaturalLanguage、FoundationModels、PDF Adapter、EPUB Adapter 均有单元测试。
|
||||
- 关键用户流有 UI 自动化测试。
|
||||
- 完成至少 100 条人工标注的产品评测集。
|
||||
- 所有准确性指标达到第 8.3 节门槛。
|
||||
- 在最低支持系统、主流支持设备和至少两代 Apple Intelligence 设备上完成真机验证。
|
||||
- 完成模型不可用、未下载、语言不支持、上下文溢出和生成取消测试。
|
||||
- 完成隐私清单、App Store 隐私申报和 AI 功能说明审核。
|
||||
- TestFlight 灰度无 P0/P1 缺陷,AI 相关 crash-free session 达标。
|
||||
- 现有 PDF、EPUB、搜索、标注和 TTS 回归全部通过。
|
||||
|
||||
## 10. 实施路线
|
||||
|
||||
以下排期按 2 名 iOS 工程师、1 名测试工程师、产品/内容评测兼职参与估算,总周期 12-14 周。单人开发建议按 18-22 周估算。
|
||||
|
||||
| 周期 | 阶段 | 主要交付 | 退出条件 |
|
||||
|------|------|----------|----------|
|
||||
| 第 1 周 | 合同与工程骨架 | 五份开发文档、Podspec、目录、CI Scheme、ADR | 文档评审通过,空库支持 iOS 15 编译 |
|
||||
| 第 2-3 周 | Core 与存储 | 公共模型、SQLite Schema、迁移、Job/Checkpoint、删除接口 | 崩溃恢复和增量失效单测通过 |
|
||||
| 第 4-5 周 | Natural Language | 分句、语言、实体候选、词法/语义检索 | 基础检索评测达标,TTS 范围无漂移 |
|
||||
| 第 6 周 | PDF Adapter | 原生文本/OCR 快照、Locator、引用跳转和高亮 | PDF 引用恢复率达到门槛 |
|
||||
| 第 7 周 | EPUB Adapter | href/CFI/rangeCFI 快照、Locator、跳转和高亮 | 重排后引用恢复率达到门槛 |
|
||||
| 第 8-9 周 | Foundation Models | 可用性、摘要、问答、结构化输出、引用校验 | 不可用降级与 AI 评测通过 |
|
||||
| 第 10 周 | 人物关系 | 人物、别名、关系、冲突和证据合并 | 人物/关系指标达到门槛 |
|
||||
| 第 11 周 | 商用 UI | AI 面板、状态、取消、引用、防剧透、无障碍 | 核心 UI 自动化通过 |
|
||||
| 第 12 周 | 性能与隐私 | 内存、耗电、数据库、日志、清除数据、隐私说明 | 无 P0/P1,性能与隐私门禁通过 |
|
||||
| 第 13-14 周 | TestFlight 灰度 | 5%→25%→100% 分阶段发布 | Crash-free 和质量反馈持续达标 |
|
||||
|
||||
每阶段要求:
|
||||
|
||||
- 功能代码、单元测试和文档同一阶段完成。
|
||||
- 公共 API 变更必须先更新 API 文档。
|
||||
- Prompt 变更必须增加版本并跑 AI 回归集。
|
||||
- 阶段退出条件未满足时不得把未验证能力带入下一阶段默认开启。
|
||||
|
||||
## 11. 版本范围
|
||||
|
||||
### 1.0
|
||||
|
||||
- 本地索引。
|
||||
- 章节摘要。
|
||||
- 带引用的书内问答。
|
||||
- 人物卡片和基础人物关系。
|
||||
- PDF/EPUB 引用跳转。
|
||||
- 防剧透和完整降级。
|
||||
|
||||
### 1.1 候选
|
||||
|
||||
- 关系时间线与冲突关系展示。
|
||||
- 用户划线/笔记参与问答。
|
||||
- 多本书对照,仅限用户主动选择的本地书籍。
|
||||
- 可选 Core ML/MLX 或云端 Provider。
|
||||
|
||||
### 2.0 候选
|
||||
|
||||
- 多模态图片理解。
|
||||
- 漫画/图文书内容理解。
|
||||
- 经过独立法律和产品评审的云端增强能力。
|
||||
|
||||
## 12. 依赖文档
|
||||
|
||||
- [RDAIReaderView-ARCHITECTURE.md](RDAIReaderView-ARCHITECTURE.md)
|
||||
- [RDAIReaderView-API.md](RDAIReaderView-API.md)
|
||||
- [RDAIReaderView-AI-SPEC.md](RDAIReaderView-AI-SPEC.md)
|
||||
- [RDAIReaderView-TEST-PLAN.md](RDAIReaderView-TEST-PLAN.md)
|
||||
@@ -0,0 +1,473 @@
|
||||
# RDAIReaderView 商用测试与发布计划
|
||||
|
||||
**文档状态:** Draft 0.1
|
||||
**最后更新:** 2026-07-25
|
||||
**目标版本:** RDAIReaderView 1.0
|
||||
|
||||
## 1. 目标
|
||||
|
||||
测试计划验证以下结论:
|
||||
|
||||
1. AI 模块不会破坏现有 PDF、EPUB、搜索、标注和 TTS。
|
||||
2. 索引和引用在书籍更新、分页变化、OCR 和应用中断后仍然可靠。
|
||||
3. Foundation Models 的输出有证据、可拒答、不剧透并可安全降级。
|
||||
4. 性能、内存、耗电、隐私和可访问性达到商用要求。
|
||||
5. Prompt 或系统模型更新后,可以通过可重复评测发现质量回归。
|
||||
|
||||
## 2. 测试层级
|
||||
|
||||
```text
|
||||
人工与 TestFlight 验收
|
||||
↑
|
||||
UI / 真机系统测试
|
||||
↑
|
||||
PDF / EPUB / TTS 集成测试
|
||||
↑
|
||||
AI 产品评测与 Prompt 回归
|
||||
↑
|
||||
Core / Storage / NLP 单元测试
|
||||
```
|
||||
|
||||
确定性校验优先放在单元测试;非确定性生成质量使用固定数据集、多次运行和人工评审。
|
||||
|
||||
## 3. 测试目标与 Target
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
RDAIReaderViewCoreTests
|
||||
RDAIReaderViewNaturalLanguageTests
|
||||
RDAIReaderViewFoundationModelsTests
|
||||
RDPDFReaderViewAITests
|
||||
RDEpubReaderViewAITests
|
||||
ReadViewDemoAIUITests
|
||||
```
|
||||
|
||||
Foundation Models 真机测试与普通 CI 分离,避免不支持模型的 Runner 造成假失败。
|
||||
|
||||
## 4. 测试数据
|
||||
|
||||
### 4.1 数据来源
|
||||
|
||||
只使用:
|
||||
|
||||
- 公版书籍。
|
||||
- 项目拥有测试授权的书籍。
|
||||
- 团队自行编写的合成文本。
|
||||
- 经过脱敏、明确允许进入测试仓库的样本。
|
||||
|
||||
不得把商业书籍全文或用户内容提交到测试仓库。
|
||||
|
||||
### 4.2 数据集组成
|
||||
|
||||
首版至少包含:
|
||||
|
||||
| 类型 | 最低数量 | 重点 |
|
||||
|------|----------|------|
|
||||
| 中文小说章节 | 20 | 多人物、别名、代词、倒叙、否定关系 |
|
||||
| 英文小说章节 | 10 | 名称大小写、代词、长句 |
|
||||
| 中文技术/非虚构 | 10 | 术语、组织、事实问答 |
|
||||
| 英文技术/非虚构 | 10 | 代码块、列表、表格 |
|
||||
| 原生文本 PDF | 5 本/50 页 | 单栏、双栏、页眉页脚 |
|
||||
| 扫描 PDF | 5 本/50 页 | OCR 错字、旋转、低清晰度 |
|
||||
| EPUB | 5 本/30 章 | CFI、脚注、长章节、重排 |
|
||||
| 无答案问题 | 20 | 拒答 |
|
||||
| 剧透边界问题 | 20 | 已读/未读范围隔离 |
|
||||
| 同名或别名关系 | 20 | 实体合并准确性 |
|
||||
|
||||
首版产品评测集不少于 100 个 Case。每个 Case 保存输入、允许范围、期望证据、可接受答案要点和禁止行为。
|
||||
|
||||
### 4.3 Case 格式
|
||||
|
||||
建议使用 JSONL:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "qa-zh-001",
|
||||
"task": "questionAnswering",
|
||||
"documentFixture": "novel-zh-01",
|
||||
"readScope": {
|
||||
"resourceOrderUpperBound": 3,
|
||||
"utf16UpperBound": 8200
|
||||
},
|
||||
"input": "林川为什么离开村庄?",
|
||||
"expectedPassageIDs": ["p-3-18", "p-3-19"],
|
||||
"requiredFacts": ["受到追捕"],
|
||||
"forbiddenFacts": ["第四章之后的身份揭示"],
|
||||
"expectedStatus": "answered"
|
||||
}
|
||||
```
|
||||
|
||||
测试结果不得把 fixture 原文写入可上传日志。
|
||||
|
||||
## 5. Core 单元测试
|
||||
|
||||
### 5.1 文本范围
|
||||
|
||||
- 空文本、空白文本和超长文本。
|
||||
- Emoji、组合字符、代理对、中文标点和换行。
|
||||
- UTF-16 范围与 Swift String Index 双向转换。
|
||||
- 搜索规范化不能改变 source range。
|
||||
- Passage 重叠不能产生错误 Locator。
|
||||
|
||||
### 5.2 哈希与失效
|
||||
|
||||
- 相同内容产生相同哈希。
|
||||
- 只改变一个章节时只失效该资源。
|
||||
- Prompt 版本变化只失效相关 Artifact。
|
||||
- embedding 模型 revision 变化只重建对应向量。
|
||||
- 阅读范围扩大不复用越界缓存。
|
||||
|
||||
### 5.3 存储与迁移
|
||||
|
||||
- 首次建库、重复打开和并发读取。
|
||||
- 每个 Schema 版本向下一版本迁移。
|
||||
- 迁移中断后恢复或回滚。
|
||||
- 数据库损坏时保留诊断并可安全重建。
|
||||
- 按书删除和删除全部数据。
|
||||
- 登出/删书回调后无孤立 Passage、向量或 Artifact。
|
||||
|
||||
### 5.4 索引调度
|
||||
|
||||
- 当前章节优先。
|
||||
- 暂停、恢复、取消和重复 prepare。
|
||||
- 应用强制退出后从 checkpoint 恢复。
|
||||
- 内存警告取消低优先级任务。
|
||||
- 内容 Provider 释放后任务安全失败,不野指针或永久等待。
|
||||
|
||||
## 6. Natural Language 测试
|
||||
|
||||
### 6.1 语言与分句
|
||||
|
||||
- 中文、英文、中英混排。
|
||||
- 无标点长段落。
|
||||
- 缩写、小数、网址和引号。
|
||||
- 句子范围与 RDSpeechReaderView 结果一致。
|
||||
- 不支持语言的降级分块。
|
||||
|
||||
### 6.2 实体
|
||||
|
||||
按任务统计 Precision、Recall、F1:
|
||||
|
||||
```text
|
||||
precision = 正确识别实体 / 所有识别实体
|
||||
recall = 正确识别实体 / 所有标注实体
|
||||
F1 = 2 * precision * recall / (precision + recall)
|
||||
```
|
||||
|
||||
首版门槛:
|
||||
|
||||
- 人物候选 Precision ≥ 0.95。
|
||||
- 人物候选 Recall ≥ 0.85。
|
||||
- 地点/组织作为辅助信息,F1 ≥ 0.80。
|
||||
|
||||
人物候选宁可少召回,也不能大量创建虚假人物。
|
||||
|
||||
### 6.3 检索
|
||||
|
||||
- 关键词命中、同义表达、别名和错别字。
|
||||
- Top 5 包含正确证据的 Recall@5 ≥ 0.90。
|
||||
- 无 embedding 资源时词法检索仍可返回结果。
|
||||
- 已读范围外 Passage 召回数必须为 0。
|
||||
- 相同输入和索引版本的词法结果顺序稳定。
|
||||
|
||||
## 7. PDF Adapter 测试
|
||||
|
||||
- 原生文本 run 按 `readingOrder` 正确拼接。
|
||||
- 双栏页面不会按几何坐标错误穿插。
|
||||
- run 间换行计入 UTF-16 范围。
|
||||
- Passage 范围映射回正确 normalized rects。
|
||||
- characterRects 存在/缺失均可高亮。
|
||||
- OCR 与原生文本来源正确标记。
|
||||
- OCR 缓存命中、失败、取消和重试。
|
||||
- 页面缓存裁剪后 Citation 仍可重新加载。
|
||||
- 跳转后目标页和高亮正确。
|
||||
- 旋转、横屏、竖滑、双页模式下高亮位置正确。
|
||||
- 页面导航取消 OCR 时,AI 索引任务不应永久丢失。
|
||||
|
||||
PDF 引用定位有效率必须 ≥ 99%。
|
||||
|
||||
## 8. EPUB Adapter 测试
|
||||
|
||||
- `href` 规范化一致。
|
||||
- UTF-16 range、rangeAnchor、CFI 和 rangeCFI 互相映射。
|
||||
- 改字号、行距、边距、字体和横竖屏后 Citation 可恢复。
|
||||
- 长章节按需加载时可提取目标资源。
|
||||
- 脚注、列表、图片替代文本和代码块类型正确。
|
||||
- EPUB 更新导致 source hash 变化时旧 Citation 标记 stale。
|
||||
- CFI 缺失时 progression 只作兜底。
|
||||
- 引用跳转复用标准位置恢复流程。
|
||||
- 固定版式无文本章节返回明确不可分析状态。
|
||||
|
||||
EPUB 重排后引用定位有效率必须 ≥ 99%。
|
||||
|
||||
## 9. Foundation Models 产品评测
|
||||
|
||||
### 9.1 运行方式
|
||||
|
||||
- 每个 Case 至少运行 3 次,避免偶然结果掩盖问题。
|
||||
- 按系统模型版本、系统语言和设备分桶。
|
||||
- Prompt 新版本同时运行旧版和新版,生成差异报告。
|
||||
- 代码校验先执行,再进入人工/模型评分。
|
||||
|
||||
### 9.2 摘要 Rubric
|
||||
|
||||
| 分数 | 标准 |
|
||||
|------|------|
|
||||
| 5 | 覆盖关键事件,全部有证据,无未读信息,表述简洁 |
|
||||
| 3 | 基本正确但遗漏一项重要内容,或引用不够精确 |
|
||||
| 1 | 包含无证据事实、重大误解或剧透 |
|
||||
|
||||
发布门槛:
|
||||
|
||||
- 平均分 ≥ 4.0。
|
||||
- 任一剧透 Case 失败即阻断发布。
|
||||
- 无证据事实比例 ≤ 2%。
|
||||
|
||||
### 9.3 问答 Rubric
|
||||
|
||||
检查:
|
||||
|
||||
- 是否回答用户问题。
|
||||
- 每个事实是否被引用支持。
|
||||
- 是否遗漏关键反证。
|
||||
- 证据不足时是否拒答。
|
||||
- 是否泄漏未读内容。
|
||||
|
||||
发布门槛:
|
||||
|
||||
- Context faithfulness ≥ 98%。
|
||||
- 无答案正确拒答率 ≥ 95%。
|
||||
- Citation validity ≥ 99%。
|
||||
- Spoiler safety = 100%。
|
||||
|
||||
### 9.4 人物与关系 Rubric
|
||||
|
||||
检查:
|
||||
|
||||
- 人物真实出现。
|
||||
- 别名有明确证据。
|
||||
- 同名人物未误合并。
|
||||
- 关系方向正确。
|
||||
- `confirmed` 与 `possible` 分类合理。
|
||||
- 冲突关系没有被静默覆盖。
|
||||
|
||||
发布门槛:
|
||||
|
||||
- Character precision ≥ 97%。
|
||||
- Alias merge precision ≥ 98%。
|
||||
- 关系证据覆盖率 = 100%。
|
||||
- 无证据关系数 = 0。
|
||||
|
||||
### 9.5 LLM Judge
|
||||
|
||||
LLM Judge 只作为辅助:
|
||||
|
||||
- 使用固定 Rubric,不使用泛化“是否有帮助”问题。
|
||||
- 先对至少 30 个 Case 与人工评分校准。
|
||||
- Spearman/Pearson 相关性低于 0.7 时不得作为发布门禁。
|
||||
- Judge 分歧、低分和边界 Case 必须人工复核。
|
||||
- 若使用云端 Judge,测试原文必须为可上传的公版或合成数据。
|
||||
|
||||
## 10. 可用性与错误测试
|
||||
|
||||
覆盖:
|
||||
|
||||
- iOS 15/17:Foundation Models 模块不参与运行。
|
||||
- iOS 26+ 不支持设备。
|
||||
- Apple Intelligence 未开启。
|
||||
- 模型正在下载或未准备。
|
||||
- 当前语言不支持。
|
||||
- context limit exceeded。
|
||||
- guardrail 拒绝输入或输出。
|
||||
- 生成中取消、切书、关闭页面、进入后台。
|
||||
- 同一 session 并发请求。
|
||||
- 低存储空间和数据库写入失败。
|
||||
|
||||
每种情况必须产生稳定枚举状态,并保持阅读器可操作。
|
||||
|
||||
## 11. UI 与可访问性
|
||||
|
||||
### 11.1 UI 自动化
|
||||
|
||||
- 打开/关闭 AI 面板。
|
||||
- 当前章节索引状态。
|
||||
- 发起问题、取消和重试。
|
||||
- 点击引用并跳转高亮。
|
||||
- 仅已读范围默认开启。
|
||||
- 整本书模式确认。
|
||||
- Foundation Models 不可用状态。
|
||||
- 清除本书和全部 AI 数据。
|
||||
- AI 结果交给 TTS 朗读。
|
||||
|
||||
### 11.2 可访问性
|
||||
|
||||
- VoiceOver 顺序和标签。
|
||||
- Dynamic Type 最大辅助字号。
|
||||
- Reduce Motion。
|
||||
- 深色模式和高对比度。
|
||||
- `possible`/`conflicting` 不只依赖颜色。
|
||||
- 加载、取消和失败状态有可访问性公告。
|
||||
|
||||
## 12. 性能与资源测试
|
||||
|
||||
目标设备至少覆盖:
|
||||
|
||||
- 最低性能 iOS 15 支持设备。
|
||||
- iOS 17 中档设备。
|
||||
- 一台首代支持 Apple Intelligence 的设备。
|
||||
- 一台当前系统的高性能设备。
|
||||
|
||||
基准:
|
||||
|
||||
| 指标 | 1.0 目标 |
|
||||
|------|----------|
|
||||
| 当前章节基础索引 P95 | ≤ 2 秒 |
|
||||
| 10 万中文字基础索引 P95 | ≤ 30 秒 |
|
||||
| 本地检索 P95 | ≤ 300 ms |
|
||||
| 生成首个可展示结果 P95 | ≤ 5 秒 |
|
||||
| 引用跳转 P95 | ≤ 500 ms,不含未缓存页面加载 |
|
||||
| 索引峰值额外内存 | 目标 ≤ 150 MB,按真机基线确认 |
|
||||
| 空闲内存释放 | 30 秒内释放 embedding 和大文本缓存 |
|
||||
| AI crash-free session | ≥ 99.9% |
|
||||
|
||||
测试同时记录:
|
||||
|
||||
- CPU time。
|
||||
- 主线程卡顿。
|
||||
- thermal state。
|
||||
- 电量变化。
|
||||
- 数据库大小/万字。
|
||||
- embedding 资源加载耗时。
|
||||
- 缓存命中率。
|
||||
|
||||
性能基线变化超过 15% 时 CI 或发布报告必须提示。
|
||||
|
||||
## 13. 隐私与安全测试
|
||||
|
||||
- 网络抓包确认默认实现不上传书籍或问题。
|
||||
- 搜索日志、控制台日志和 crash breadcrumbs 不含原文。
|
||||
- 清除 AI 数据后数据库、缓存和临时文件均删除。
|
||||
- 删除书籍和退出账户触发相同清除路径。
|
||||
- Scope 过滤在检索前和生成后均执行。
|
||||
- 构造伪造 Passage ID,确认 Citation Validator 拒绝。
|
||||
- 构造路径、超长查询和大量 Tool 参数,确认边界限制。
|
||||
- Tool Calling 最大次数和重复调用检测生效。
|
||||
- 数据库迁移和诊断包不泄漏原文。
|
||||
|
||||
## 14. 回归测试
|
||||
|
||||
每次合入必须运行:
|
||||
|
||||
- Core 单元测试。
|
||||
- Natural Language 确定性测试。
|
||||
- PDF/EPUB Adapter fixture 测试。
|
||||
- 现有 PDF、EPUB 和 TTS 编译。
|
||||
- `git diff --check` 和公共 API 兼容检查。
|
||||
|
||||
每日或候选发布运行:
|
||||
|
||||
- UI smoke。
|
||||
- 完整 AI 评测集。
|
||||
- 大书索引性能。
|
||||
- Foundation Models 真机矩阵。
|
||||
- 现有 `ReadViewDemoUITests` 回归。
|
||||
|
||||
## 15. CI 建议
|
||||
|
||||
```text
|
||||
PR:
|
||||
lint/diff-check
|
||||
Core unit tests
|
||||
NLP unit tests
|
||||
Adapter tests
|
||||
build iOS 15 target
|
||||
build iOS 26 FoundationModels target
|
||||
|
||||
Nightly:
|
||||
Full reader UI regression
|
||||
AI deterministic evals
|
||||
Performance fixtures
|
||||
|
||||
Release candidate:
|
||||
Foundation Models physical-device eval
|
||||
Human review sample
|
||||
Privacy/network audit
|
||||
Migration matrix
|
||||
```
|
||||
|
||||
设备模型评测结果应保存以下元数据:
|
||||
|
||||
- OS 版本。
|
||||
- 模型版本或可识别 profile。
|
||||
- Prompt identifier/version。
|
||||
- fixture version。
|
||||
- SDK commit。
|
||||
- 各指标与失败 Case ID。
|
||||
|
||||
不得保存生产用户原文。
|
||||
|
||||
## 16. 缺陷分级
|
||||
|
||||
### P0
|
||||
|
||||
- 泄漏书籍内容或阅读历史。
|
||||
- 绕过已读范围造成剧透。
|
||||
- 删除用户原书、标注或笔记。
|
||||
- 大面积崩溃或数据库不可恢复损坏。
|
||||
|
||||
### P1
|
||||
|
||||
- 无引用或错误引用的事实作为确定答案展示。
|
||||
- 人物关系大面积误合并。
|
||||
- Foundation Models 不可用导致阅读器不可用。
|
||||
- 引用跳转到错误章节/页面。
|
||||
|
||||
### P2
|
||||
|
||||
- 摘要遗漏、检索质量下降、局部 UI 或性能问题。
|
||||
- 可重试且不影响阅读主流程的生成失败。
|
||||
|
||||
发布时 P0/P1 必须为 0;P2 必须有明确接受记录和后续版本计划。
|
||||
|
||||
## 17. TestFlight 灰度
|
||||
|
||||
### 阶段 A:内部
|
||||
|
||||
- 团队和测试设备。
|
||||
- 至少 7 天。
|
||||
- 完整日志仅限脱敏元数据。
|
||||
|
||||
### 阶段 B:5%
|
||||
|
||||
- 只开启摘要和问答。
|
||||
- 观察 crash-free、取消率、拒答率、引用点击成功率。
|
||||
- 人物关系仍受远程/本地 feature flag 控制。
|
||||
|
||||
### 阶段 C:25%
|
||||
|
||||
- 开启人物卡片。
|
||||
- 关系图只对达到索引完整度的书籍开放。
|
||||
- 至少稳定 7 天。
|
||||
|
||||
### 阶段 D:100%
|
||||
|
||||
- 所有发布门禁持续达标。
|
||||
- 保留快速关闭 Foundation Models 功能的配置,但关闭后基础阅读和 NLP 正常。
|
||||
|
||||
## 18. 最终发布门禁
|
||||
|
||||
- [ ] 五份开发文档与实现一致。
|
||||
- [ ] 公共 API 兼容检查通过。
|
||||
- [ ] Core/NLP/Adapter 单元和集成测试通过。
|
||||
- [ ] 现有 Reader 与 TTS 回归通过。
|
||||
- [ ] 100+ AI Case 全量运行并达到指标。
|
||||
- [ ] Foundation Models 支持与不支持路径均完成真机测试。
|
||||
- [ ] PDF/EPUB Citation validity ≥ 99%。
|
||||
- [ ] Context faithfulness ≥ 98%。
|
||||
- [ ] Spoiler safety = 100%。
|
||||
- [ ] AI crash-free session ≥ 99.9%。
|
||||
- [ ] 隐私和网络审计通过。
|
||||
- [ ] 无 P0/P1 缺陷。
|
||||
- [ ] TestFlight 灰度指标稳定。
|
||||
|
||||
@@ -6,54 +6,54 @@
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDReaderPageProvider` 协议提供页面内容视图,ReaderView 负责容器管理和翻页调度。
|
||||
`ReaderView` 是 ReadViewSDK 的**通用翻页容器层**,位于 EPUBUI 层之下。它提供与 EPUB 内容无关的页面展示、翻页动画、手势识别、页面预加载和双页布局能力。上层通过 `RDEpubReaderPageProvider` 协议提供页面内容视图,ReaderView 负责容器管理和翻页调度。
|
||||
|
||||
**文件清单(14 个 Swift 文件):**
|
||||
|
||||
| 文件 | 核心类型 | 职责 |
|
||||
|------|----------|------|
|
||||
| `RDReaderView.swift` | `RDReaderView` | 主容器视图,协调所有子组件 |
|
||||
| `RDReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
|
||||
| `RDReaderFlowLayout.swift` | `RDReaderFlowLayout` | UICollectionView 自定义布局 |
|
||||
| `RDReaderGestureController.swift` | `RDReaderGestureController` | 手势控制器(预留) |
|
||||
| `RDReaderContentCell.swift` | `RDReaderContentCell` | CollectionView 内容 Cell |
|
||||
| `RDReaderPageChildViewController.swift` | `RDReaderPageChildViewController` | PageCurl 模式子 VC |
|
||||
| `RDReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
|
||||
| `RDReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
|
||||
| `RDReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
|
||||
| `RDReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
|
||||
| `Paging/RDReaderPagingController.swift` | `RDReaderPagingController` | 翻页状态机 |
|
||||
| `Paging/RDReaderPreloadController.swift` | `RDReaderPreloadController` | 页面预加载与缓存 |
|
||||
| `Paging/RDReaderSpreadResolver.swift` | `RDReaderSpreadResolver` | 双页展开计算 |
|
||||
| `Paging/RDReaderTapRegionHandler.swift` | `RDReaderTapRegionHandler` | 点击区域判定 |
|
||||
| `RDEpubReaderView.swift` | `RDEpubReaderView` | 主容器视图,协调所有子组件 |
|
||||
| `RDEpubReaderViewProtocols.swift` | 协议 + 枚举 | 数据源、代理、导航协议定义 |
|
||||
| `RDEpubReaderFlowLayout.swift` | `RDEpubReaderFlowLayout` | UICollectionView 自定义布局 |
|
||||
| `RDEpubReaderGestureController.swift` | `RDEpubReaderGestureController` | 手势控制器(预留) |
|
||||
| `RDEpubReaderContentCell.swift` | `RDEpubReaderContentCell` | CollectionView 内容 Cell |
|
||||
| `RDEpubReaderPageChildViewController.swift` | `RDEpubReaderPageChildViewController` | PageCurl 模式子 VC |
|
||||
| `RDEpubReaderView+PageCurl.swift` | Extension | UIPageViewController 数据源/代理 |
|
||||
| `RDEpubReaderView+CollectionView.swift` | Extension | UICollectionView 数据源/布局代理 |
|
||||
| `RDEpubReaderView+ContentAccess.swift` | Extension | 内容视图访问与复用 |
|
||||
| `RDEpubReaderView+ToolView.swift` | Extension | 工具栏安装与动画 |
|
||||
| `Paging/RDEpubReaderPagingController.swift` | `RDEpubReaderPagingController` | 翻页状态机 |
|
||||
| `Paging/RDEpubReaderPreloadController.swift` | `RDEpubReaderPreloadController` | 页面预加载与缓存 |
|
||||
| `Paging/RDEpubReaderSpreadResolver.swift` | `RDEpubReaderSpreadResolver` | 双页展开计算 |
|
||||
| `Paging/RDEpubReaderTapRegionHandler.swift` | `RDEpubReaderTapRegionHandler` | 点击区域判定 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 协议定义(RDReaderViewProtocols.swift)
|
||||
## 2. 协议定义(RDEpubReaderViewProtocols.swift)
|
||||
|
||||
### 2.1 RDReaderDataSource(旧版数据源,已废弃)
|
||||
### 2.1 RDEpubReaderDataSource(旧版数据源,已废弃)
|
||||
|
||||
```swift
|
||||
@objc public protocol RDReaderDataSource: NSObjectProtocol {
|
||||
func pageCountOfReaderView(readerView: RDReaderView) -> Int
|
||||
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
|
||||
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
|
||||
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
|
||||
@objc public protocol RDEpubReaderDataSource: NSObjectProtocol {
|
||||
func pageCountOfReaderView(readerView: RDEpubReaderView) -> Int
|
||||
func pageContentView(readerView: RDEpubReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||||
func pageIdentifier(readerView: RDEpubReaderView, pageNum: Int) -> String?
|
||||
@objc optional func topToolView(readerView: RDEpubReaderView) -> UIView?
|
||||
@objc optional func bottomToolView(readerView: RDEpubReaderView) -> UIView?
|
||||
}
|
||||
```
|
||||
|
||||
> 向后兼容保留,新代码应使用 `RDReaderPageProvider`。
|
||||
> 向后兼容保留,新代码应使用 `RDEpubReaderPageProvider`。
|
||||
|
||||
### 2.2 RDReaderPageProvider(推荐数据源)
|
||||
### 2.2 RDEpubReaderPageProvider(推荐数据源)
|
||||
|
||||
```swift
|
||||
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
|
||||
func numberOfPages(in readerView: RDReaderView) -> Int
|
||||
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
|
||||
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
|
||||
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
|
||||
@objc public protocol RDEpubReaderPageProvider: NSObjectProtocol {
|
||||
func numberOfPages(in readerView: RDEpubReaderView) -> Int
|
||||
func readerView(_ readerView: RDEpubReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
|
||||
@objc optional func pageIdentifier(in readerView: RDEpubReaderView, index: Int) -> String?
|
||||
@objc optional func readerViewTopChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||
@objc optional func readerViewBottomChrome(_ readerView: RDEpubReaderView) -> UIView?
|
||||
}
|
||||
```
|
||||
|
||||
@@ -65,31 +65,31 @@
|
||||
| `readerViewTopChrome(_:)` | 返回顶部工具栏视图 |
|
||||
| `readerViewBottomChrome(_:)` | 返回底部工具栏视图 |
|
||||
|
||||
### 2.3 RDReaderDelegate
|
||||
### 2.3 RDEpubReaderDelegate
|
||||
|
||||
```swift
|
||||
@objc public protocol RDReaderDelegate: NSObjectProtocol {
|
||||
func pageNum(readerView: RDReaderView, pageNum: Int)
|
||||
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
|
||||
@objc public protocol RDEpubReaderDelegate: NSObjectProtocol {
|
||||
func pageNum(readerView: RDEpubReaderView, pageNum: Int)
|
||||
@objc optional func readerViewOrientationWillChange(readerView: RDEpubReaderView, isLandscape: Bool)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 RDReaderPageNavigating
|
||||
### 2.4 RDEpubReaderPageNavigating
|
||||
|
||||
```swift
|
||||
public protocol RDReaderPageNavigating: AnyObject {
|
||||
public protocol RDEpubReaderPageNavigating: AnyObject {
|
||||
var currentPage: Int { get }
|
||||
func reloadPages()
|
||||
func transition(to page: Int, animated: Bool)
|
||||
}
|
||||
```
|
||||
|
||||
`RDReaderView` 遵循此协议,提供统一的页面导航接口。
|
||||
`RDEpubReaderView` 遵循此协议,提供统一的页面导航接口。
|
||||
|
||||
### 2.5 枚举类型
|
||||
|
||||
```swift
|
||||
extension RDReaderView {
|
||||
extension RDEpubReaderView {
|
||||
public enum DisplayType {
|
||||
case pageCurl // 仿真翻页(UIPageViewController)
|
||||
case horizontalScroll // 水平滑动(UICollectionView)
|
||||
@@ -105,13 +105,13 @@ extension RDReaderView {
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心类:RDReaderView
|
||||
## 3. 核心类:RDEpubReaderView
|
||||
|
||||
**文件:** `RDReaderView.swift`
|
||||
**文件:** `RDEpubReaderView.swift`
|
||||
|
||||
`RDReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
|
||||
`RDEpubReaderView` 是一个 `UIView` 子类,作为翻页容器的主入口。内部管理两种翻页引擎:
|
||||
- **PageCurl 模式**:使用 `UIPageViewController` 实现仿真翻页
|
||||
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDReaderFlowLayout` 实现滑动翻页
|
||||
- **Scroll 模式**:使用 `UICollectionView` + 自定义 `RDEpubReaderFlowLayout` 实现滑动翻页
|
||||
|
||||
### 3.1 关键属性
|
||||
|
||||
@@ -124,16 +124,16 @@ extension RDReaderView {
|
||||
| `coverPageIndex` | `Int?` | 封面页索引(独占一屏) |
|
||||
| `pagesPerScreen` | `Int` | 每屏页数(横屏双页时为 2) |
|
||||
| `preloadRadius` | `Int` | 预加载半径(默认 1) |
|
||||
| `dataSource` | `RDReaderDataSource?` | 旧版数据源 |
|
||||
| `pageProvider` | `RDReaderPageProvider?` | 推荐数据源 |
|
||||
| `delegate` | `RDReaderDelegate?` | 事件代理 |
|
||||
| `dataSource` | `RDEpubReaderDataSource?` | 旧版数据源 |
|
||||
| `pageProvider` | `RDEpubReaderPageProvider?` | 推荐数据源 |
|
||||
| `delegate` | `RDEpubReaderDelegate?` | 事件代理 |
|
||||
| `toolViewAnimationDuration` | `TimeInterval` | 工具栏动画时长(0.3s) |
|
||||
|
||||
### 3.2 关键方法
|
||||
|
||||
```swift
|
||||
/// 切换显示模式(pageCurl / horizontalScroll / verticalScroll)
|
||||
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType)
|
||||
public func switchReaderDisplayType(_ displayType: RDEpubReaderView.DisplayType)
|
||||
|
||||
/// 跳转到指定页
|
||||
public func transitionToPage(pageNum: Int, animated: Bool = false)
|
||||
@@ -154,22 +154,22 @@ public func isFullScreenPage(_ pageNum: Int) -> Bool
|
||||
|------|------|------|
|
||||
| `pageViewController` | `UIPageViewController` | PageCurl 翻页引擎 |
|
||||
| `collectionView` | `UICollectionView` | Scroll 翻页引擎 |
|
||||
| `layout` | `RDReaderFlowLayout` | CollectionView 自定义布局 |
|
||||
| `spreadResolver` | `RDReaderSpreadResolver` | 双页配对计算 |
|
||||
| `tapRegionHandler` | `RDReaderTapRegionHandler` | 点击区域判定 |
|
||||
| `preloadController` | `RDReaderPreloadController` | 页面预加载与缓存 |
|
||||
| `pagingController` | `RDReaderPagingController` | 翻页状态管理 |
|
||||
| `layout` | `RDEpubReaderFlowLayout` | CollectionView 自定义布局 |
|
||||
| `spreadResolver` | `RDEpubReaderSpreadResolver` | 双页配对计算 |
|
||||
| `tapRegionHandler` | `RDEpubReaderTapRegionHandler` | 点击区域判定 |
|
||||
| `preloadController` | `RDEpubReaderPreloadController` | 页面预加载与缓存 |
|
||||
| `pagingController` | `RDEpubReaderPagingController` | 翻页状态管理 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 翻页状态机:RDReaderPagingController
|
||||
## 4. 翻页状态机:RDEpubReaderPagingController
|
||||
|
||||
**文件:** `Paging/RDReaderPagingController.swift`
|
||||
**文件:** `Paging/RDEpubReaderPagingController.swift`
|
||||
|
||||
管理 PageCurl 模式下的翻页请求队列,防止动画冲突。
|
||||
|
||||
```swift
|
||||
struct RDReaderPagingController {
|
||||
struct RDEpubReaderPagingController {
|
||||
struct PageTransitionRequest: Equatable {
|
||||
let pageNum: Int
|
||||
let animated: Bool
|
||||
@@ -180,7 +180,7 @@ struct RDReaderPagingController {
|
||||
var didBuildUI: Bool // UI 是否已构建
|
||||
|
||||
/// 判断是否应排队请求(PageCurl 模式下动画中返回 true)
|
||||
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool
|
||||
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDEpubReaderView.DisplayType) -> Bool
|
||||
|
||||
/// 完成翻页动画,返回待处理的请求
|
||||
mutating func finishPageCurlTransition() -> PageTransitionRequest?
|
||||
@@ -192,9 +192,9 @@ struct RDReaderPagingController {
|
||||
|
||||
---
|
||||
|
||||
## 5. 页面预加载:RDReaderPreloadController
|
||||
## 5. 页面预加载:RDEpubReaderPreloadController
|
||||
|
||||
**文件:** `Paging/RDReaderPreloadController.swift`
|
||||
**文件:** `Paging/RDEpubReaderPreloadController.swift`
|
||||
|
||||
负责在当前页周围预渲染页面视图,减少翻页时的白屏时间。
|
||||
|
||||
@@ -224,27 +224,27 @@ func invalidate(environment: Environment)
|
||||
|
||||
```swift
|
||||
struct Environment {
|
||||
let displayType: RDReaderView.DisplayType
|
||||
let displayType: RDEpubReaderView.DisplayType
|
||||
let isLandscape: Bool
|
||||
let pagesPerScreen: Int
|
||||
let boundsSize: CGSize
|
||||
let landscapeDualPageEnabled: Bool
|
||||
let coverPageIndex: Int?
|
||||
let totalPages: Int
|
||||
let spreadResolver: RDReaderSpreadResolver
|
||||
let spreadResolver: RDEpubReaderSpreadResolver
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 双页展开计算:RDReaderSpreadResolver
|
||||
## 6. 双页展开计算:RDEpubReaderSpreadResolver
|
||||
|
||||
**文件:** `Paging/RDReaderSpreadResolver.swift`
|
||||
**文件:** `Paging/RDEpubReaderSpreadResolver.swift`
|
||||
|
||||
纯函数式结构体,负责双页模式下的页面配对和导航计算。
|
||||
|
||||
```swift
|
||||
struct RDReaderSpreadResolver {
|
||||
struct RDEpubReaderSpreadResolver {
|
||||
/// 判断是否为全屏页(封面页独占一屏)
|
||||
func isFullScreenPage(_ pageNum: Int, landscapeDualPageEnabled: Bool, isLandscape: Bool, coverPageIndex: Int?) -> Bool
|
||||
|
||||
@@ -265,15 +265,15 @@ struct RDReaderSpreadResolver {
|
||||
|
||||
---
|
||||
|
||||
## 7. 点击区域判定:RDReaderTapRegionHandler
|
||||
## 7. 点击区域判定:RDEpubReaderTapRegionHandler
|
||||
|
||||
**文件:** `Paging/RDReaderTapRegionHandler.swift`
|
||||
**文件:** `Paging/RDEpubReaderTapRegionHandler.swift`
|
||||
|
||||
将屏幕三等分,判定点击属于左/中/右区域。
|
||||
|
||||
```swift
|
||||
struct RDReaderTapRegionHandler {
|
||||
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDReaderView.TapEvent
|
||||
struct RDEpubReaderTapRegionHandler {
|
||||
func resolveTapEvent(point: CGPoint, viewFrame: CGRect, isToolViewVisible: Bool) -> RDEpubReaderView.TapEvent
|
||||
}
|
||||
```
|
||||
|
||||
@@ -284,9 +284,9 @@ struct RDReaderTapRegionHandler {
|
||||
|
||||
---
|
||||
|
||||
## 8. 流式布局:RDReaderFlowLayout
|
||||
## 8. 流式布局:RDEpubReaderFlowLayout
|
||||
|
||||
**文件:** `RDReaderFlowLayout.swift`
|
||||
**文件:** `RDEpubReaderFlowLayout.swift`
|
||||
|
||||
`UICollectionViewFlowLayout` 子类,支持水平滚动和垂直滚动两种模式。
|
||||
|
||||
@@ -294,7 +294,7 @@ struct RDReaderTapRegionHandler {
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `displayType` | `RDReaderView.DisplayType` | 布局模式 |
|
||||
| `displayType` | `RDEpubReaderView.DisplayType` | 布局模式 |
|
||||
| `isLandscapeDualPage` | `Bool` | 是否横屏双页 |
|
||||
| `coverPageIndex` | `Int?` | 封面页索引 |
|
||||
| `pagesPerScreen` | `Int` | 每屏页数 |
|
||||
@@ -302,12 +302,12 @@ struct RDReaderTapRegionHandler {
|
||||
### 8.2 协议
|
||||
|
||||
```swift
|
||||
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
|
||||
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
|
||||
public protocol RDEpubReaderFlowLayoutDataSoure: NSObjectProtocol {
|
||||
func heigtOfVerticalScrollPage(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int) -> CGFloat?
|
||||
}
|
||||
|
||||
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
|
||||
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
|
||||
@objc public protocol RDEpubReaderFlowLayoutDelegate: NSObjectProtocol {
|
||||
func pageNum(flowLayout: RDEpubReaderFlowLayout, pageIndex: Int)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -324,28 +324,28 @@ func currentContentOffset(count: Int) -> CGPoint
|
||||
|
||||
---
|
||||
|
||||
## 9. 内容 Cell:RDReaderContentCell
|
||||
## 9. 内容 Cell:RDEpubReaderContentCell
|
||||
|
||||
**文件:** `RDReaderContentCell.swift`
|
||||
**文件:** `RDEpubReaderContentCell.swift`
|
||||
|
||||
`UICollectionViewCell` 子类,用于 Scroll 模式下承载页面内容视图。
|
||||
|
||||
```swift
|
||||
class RDReaderContentCell: UICollectionViewCell {
|
||||
class RDEpubReaderContentCell: UICollectionViewCell {
|
||||
var containerView: UIView? // 设置时自动添加到 contentView,移除旧视图
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. PageCurl 子控制器:RDReaderPageChildViewController
|
||||
## 10. PageCurl 子控制器:RDEpubReaderPageChildViewController
|
||||
|
||||
**文件:** `RDReaderPageChildViewController.swift`
|
||||
**文件:** `RDEpubReaderPageChildViewController.swift`
|
||||
|
||||
`UIViewController` 子类,作为 `UIPageViewController` 的页面 VC。
|
||||
|
||||
```swift
|
||||
class RDReaderPageChildViewController: UIViewController {
|
||||
class RDEpubReaderPageChildViewController: UIViewController {
|
||||
var contentView: UIView? // 内容视图,设置时自动安装到容器
|
||||
var pageNum: Int // 对应页码
|
||||
|
||||
@@ -357,7 +357,7 @@ class RDReaderPageChildViewController: UIViewController {
|
||||
|
||||
## 11. Extension 汇总
|
||||
|
||||
### 11.1 RDReaderView+PageCurl
|
||||
### 11.1 RDEpubReaderView+PageCurl
|
||||
|
||||
实现 `UIPageViewControllerDataSource` 和 `UIPageViewControllerDelegate`:
|
||||
|
||||
@@ -367,18 +367,18 @@ class RDReaderPageChildViewController: UIViewController {
|
||||
- `pageViewController(_:willTransitionTo:)` — 即将翻页时预加载
|
||||
|
||||
**特殊页码:**
|
||||
- `RDReaderView.blankPageNum`(`Int.max`)— 双页模式下的空白页
|
||||
- `RDReaderView.blankEndPageNum`(`Int.max - 1`)— 末尾空白页
|
||||
- `RDEpubReaderView.blankPageNum`(`Int.max`)— 双页模式下的空白页
|
||||
- `RDEpubReaderView.blankEndPageNum`(`Int.max - 1`)— 末尾空白页
|
||||
|
||||
### 11.2 RDReaderView+CollectionView
|
||||
### 11.2 RDEpubReaderView+CollectionView
|
||||
|
||||
实现 `UICollectionViewDataSource`、`RDReaderFlowLayoutDelegate`、`RDReaderFlowLayoutDataSoure`:
|
||||
实现 `UICollectionViewDataSource`、`RDEpubReaderFlowLayoutDelegate`、`RDEpubReaderFlowLayoutDataSoure`:
|
||||
|
||||
- `collectionView(_:cellForItemAt:)` — 复用预加载视图或创建新 Cell
|
||||
- `collectionView(_:numberOfItemsInSection:)` — 返回总页数
|
||||
- `pageNum(flowLayout:pageIndex:)` — 滚动时更新当前页码
|
||||
|
||||
### 11.3 RDReaderView+ContentAccess
|
||||
### 11.3 RDEpubReaderView+ContentAccess
|
||||
|
||||
提供内容视图的注册、复用和查询:
|
||||
|
||||
@@ -396,7 +396,7 @@ public func pageContentView(pageNum: Int) -> UIView?
|
||||
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize
|
||||
```
|
||||
|
||||
### 11.4 RDReaderView+ToolView
|
||||
### 11.4 RDEpubReaderView+ToolView
|
||||
|
||||
管理顶部/底部工具栏的安装、显示/隐藏动画:
|
||||
|
||||
@@ -420,11 +420,11 @@ func updateToolViewHeightConstraintsIfNeeded()
|
||||
| 模式 | 应用 |
|
||||
|------|------|
|
||||
| **策略模式** | `DisplayType` 切换 PageCurl / Scroll 两种翻页策略 |
|
||||
| **适配器模式** | `RDReaderLegacyDataSourceAdapter` 将旧 `RDReaderDataSource` 适配为 `RDReaderPageProvider` |
|
||||
| **命令队列** | `RDReaderPagingController` 管理翻页请求队列 |
|
||||
| **缓存签名** | `RDReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
|
||||
| **适配器模式** | `RDEpubReaderLegacyDataSourceAdapter` 将旧 `RDEpubReaderDataSource` 适配为 `RDEpubReaderPageProvider` |
|
||||
| **命令队列** | `RDEpubReaderPagingController` 管理翻页请求队列 |
|
||||
| **缓存签名** | `RDEpubReaderPreloadController.CacheSignature` 检测环境变化自动失效 |
|
||||
| **关注点分离** | Extension 将不同功能拆分到独立文件 |
|
||||
| **纯函数** | `RDReaderSpreadResolver` 和 `RDReaderTapRegionHandler` 无状态计算 |
|
||||
| **纯函数** | `RDEpubReaderSpreadResolver` 和 `RDEpubReaderTapRegionHandler` 无状态计算 |
|
||||
|
||||
---
|
||||
|
||||
@@ -434,7 +434,7 @@ func updateToolViewHeightConstraintsIfNeeded()
|
||||
用户点击屏幕
|
||||
│
|
||||
▼
|
||||
RDReaderTapRegionHandler.resolveTapEvent()
|
||||
RDEpubReaderTapRegionHandler.resolveTapEvent()
|
||||
│
|
||||
├── .left → goPreviousPage() ──→ spreadResolver.nextPage(forward: false)
|
||||
├── .right → goNextPage() ──→ spreadResolver.nextPage(forward: true)
|
||||
|
||||
+11
-4
@@ -34,6 +34,7 @@
|
||||
| LocationPersistenceTests.swift | 阅读位置持久化 |
|
||||
| MetadataParseBenchmarkTests.swift | 元数据解析性能基准 |
|
||||
| PageNavigationTests.swift | 页面导航 |
|
||||
| PDFPageTurnBlankPageTests.swift | PDF 翻页后白页回归(截屏墨迹占比判定) |
|
||||
| ReaderAnnotationTests.swift | 标注基础功能 |
|
||||
| ReaderAnnotationExtendedTests.swift | 标注扩展功能 |
|
||||
| ReaderOpenCloseTests.swift | 阅读器打开/关闭 |
|
||||
@@ -64,13 +65,19 @@
|
||||
|
||||
### CocoaPods 依赖准备
|
||||
|
||||
> 仓库包含示例工程 `ReadViewDemo`,并已提交 `Pods/` 与 `Podfile.lock`。如本地环境未同步,可在仓库根或示例工程目录运行:
|
||||
> 仓库包含示例工程 `ReadViewDemo`,并已提交 `ReadViewDemo/Pods/` 与 `ReadViewDemo/Podfile.lock`。如本地环境未同步,请在示例工程目录运行:
|
||||
|
||||
```bash
|
||||
pod install
|
||||
cd ReadViewDemo && pod install
|
||||
```
|
||||
|
||||
(依赖入口:`Podfile`、`ReadViewDemo/Podfile`)
|
||||
(依赖入口:`ReadViewDemo/Podfile`)
|
||||
|
||||
### 模拟器要求
|
||||
|
||||
Demo 链接了 `FoundationModels.framework`(AI 助手示例入口)。该框架自 iOS 26 起提供,
|
||||
在 iOS 18 及更早的模拟器上 App 会在启动时因 `Library not loaded` 直接崩溃,测试表现为
|
||||
卡在 Launch 阶段。请选择 iOS 26 及以上的模拟器运行 UI 测试。
|
||||
|
||||
### 运行 UI 测试
|
||||
|
||||
@@ -116,7 +123,7 @@ xcodebuild test \
|
||||
建议同时记录以下观测项:
|
||||
|
||||
- `prepareOnDemandChapter` 主线程 wall clock
|
||||
- `RDReaderPreloadController` 预加载命中率
|
||||
- `RDEpubReaderPreloadController` 预加载命中率
|
||||
- `bookPageMap` partial extension / full replacement 次数
|
||||
- 页面静态底图缓存命中率
|
||||
- CFI 延迟构建完成次数与耗时
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
**输出**:`RDEPUBTypesettingOutput`(渲染请求、诊断信息、兼容性报告)
|
||||
|
||||
**关键文件**:`Sources/RDReaderView/EPUBTextRendering/Typesetter/`
|
||||
**关键文件**:`Sources/RDEpubReaderView/EPUBTextRendering/Typesetter/`
|
||||
|
||||
---
|
||||
|
||||
|
||||
+28
-28
@@ -31,12 +31,12 @@ graph TB
|
||||
IndexTable[RDEPUBTextIndexTable]
|
||||
end
|
||||
|
||||
subgraph RDReaderView
|
||||
ReaderView[RDReaderView]
|
||||
FlowLayout[RDReaderFlowLayout]
|
||||
Preload[RDReaderPreloadController]
|
||||
Spread[RDReaderSpreadResolver]
|
||||
TapRegion[RDReaderTapRegionHandler]
|
||||
subgraph RDEpubReaderView
|
||||
ReaderView[RDEpubReaderView]
|
||||
FlowLayout[RDEpubReaderFlowLayout]
|
||||
Preload[RDEpubReaderPreloadController]
|
||||
Spread[RDEpubReaderSpreadResolver]
|
||||
TapRegion[RDEpubReaderTapRegionHandler]
|
||||
end
|
||||
|
||||
subgraph EPUBUI
|
||||
@@ -297,11 +297,11 @@ classDiagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RDReaderView {
|
||||
class RDEpubReaderView {
|
||||
+currentPage: Int
|
||||
+currentDisplayType: DisplayType
|
||||
+pageProvider: RDReaderPageProvider?
|
||||
+delegate: RDReaderDelegate?
|
||||
+pageProvider: RDEpubReaderPageProvider?
|
||||
+delegate: RDEpubReaderDelegate?
|
||||
+landscapeDualPageEnabled: Bool
|
||||
+pageDirection: PageDirection
|
||||
+coverPageIndex: Int?
|
||||
@@ -318,14 +318,14 @@ classDiagram
|
||||
verticalScroll
|
||||
}
|
||||
|
||||
class RDReaderPageProvider {
|
||||
class RDEpubReaderPageProvider {
|
||||
<<protocol>>
|
||||
+numberOfPages(in:) Int
|
||||
+readerView(_:viewForPageAt:reusableView:) UIView
|
||||
+pageIdentifier(in:index:) String?
|
||||
}
|
||||
|
||||
class RDReaderFlowLayout {
|
||||
class RDEpubReaderFlowLayout {
|
||||
+displayType: DisplayType
|
||||
+isLandscapeDualPage: Bool
|
||||
+coverPageIndex: Int?
|
||||
@@ -333,7 +333,7 @@ classDiagram
|
||||
+currentPage: Int
|
||||
}
|
||||
|
||||
class RDReaderPreloadController {
|
||||
class RDEpubReaderPreloadController {
|
||||
+radius: Int
|
||||
+pageViewForDisplay(pageNum:environment:contentViewProvider:) UIView?
|
||||
+takePreloadedView(for:) UIView?
|
||||
@@ -341,13 +341,13 @@ classDiagram
|
||||
+invalidate(environment:)
|
||||
}
|
||||
|
||||
class RDReaderSpreadResolver {
|
||||
class RDEpubReaderSpreadResolver {
|
||||
+isFullScreenPage(...) Bool
|
||||
+dualPagePair(for:totalPages:coverPageIndex:) (Int, Int?)
|
||||
+nextPage(from:totalPages:pagesPerScreen:coverPageIndex:forward:) Int?
|
||||
}
|
||||
|
||||
class RDReaderTapRegionHandler {
|
||||
class RDEpubReaderTapRegionHandler {
|
||||
+resolveTapEvent(point:viewFrame:isToolViewVisible:) TapEvent
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ classDiagram
|
||||
right
|
||||
}
|
||||
|
||||
class RDReaderPagingController {
|
||||
class RDEpubReaderPagingController {
|
||||
+isTransitioning: Bool
|
||||
+didBuildUI: Bool
|
||||
+pendingTransitionRequest: PageTransitionRequest?
|
||||
@@ -367,25 +367,25 @@ classDiagram
|
||||
+finishPageCurlTransition() PageTransitionRequest?
|
||||
}
|
||||
|
||||
class RDReaderContentCell {
|
||||
class RDEpubReaderContentCell {
|
||||
+containerView: UIView?
|
||||
}
|
||||
|
||||
class RDReaderPageChildViewController {
|
||||
class RDEpubReaderPageChildViewController {
|
||||
+contentView: UIView?
|
||||
+pageNum: Int
|
||||
}
|
||||
|
||||
RDReaderView --> DisplayType : uses
|
||||
RDReaderView --> RDReaderPageProvider : delegates
|
||||
RDReaderView --> RDReaderFlowLayout : owns
|
||||
RDReaderView --> RDReaderPreloadController : owns
|
||||
RDReaderView --> RDReaderSpreadResolver : owns
|
||||
RDReaderView --> RDReaderTapRegionHandler : owns
|
||||
RDReaderView --> RDReaderPagingController : owns
|
||||
RDReaderView --> RDReaderContentCell : creates
|
||||
RDReaderView --> RDReaderPageChildViewController : creates
|
||||
RDReaderTapRegionHandler --> TapEvent : produces
|
||||
RDEpubReaderView --> DisplayType : uses
|
||||
RDEpubReaderView --> RDEpubReaderPageProvider : delegates
|
||||
RDEpubReaderView --> RDEpubReaderFlowLayout : owns
|
||||
RDEpubReaderView --> RDEpubReaderPreloadController : owns
|
||||
RDEpubReaderView --> RDEpubReaderSpreadResolver : owns
|
||||
RDEpubReaderView --> RDEpubReaderTapRegionHandler : owns
|
||||
RDEpubReaderView --> RDEpubReaderPagingController : owns
|
||||
RDEpubReaderView --> RDEpubReaderContentCell : creates
|
||||
RDEpubReaderView --> RDEpubReaderPageChildViewController : creates
|
||||
RDEpubReaderTapRegionHandler --> TapEvent : produces
|
||||
```
|
||||
|
||||
---
|
||||
@@ -403,7 +403,7 @@ classDiagram
|
||||
+highlights: [RDEPUBHighlight]
|
||||
+bookmarks: [RDEPUBBookmark]
|
||||
+tableOfContents: [EPUBTableOfContentsItem]
|
||||
+readerView: RDReaderView
|
||||
+readerView: RDEpubReaderView
|
||||
+readerContext: RDEPUBReaderContext
|
||||
+runtime: RDEPUBReaderRuntime?
|
||||
}
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
# ReadViewSDK 文档索引
|
||||
|
||||
> 最后更新:2026-06-18
|
||||
> 最后更新:2026-07-25
|
||||
|
||||
---
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [CONCERNS.md](CONCERNS.md) | 代码库风险与关注点:12 项安全/性能/可维护性风险 |
|
||||
| [CONCERNS.md](CONCERNS.md) | 代码库风险与关注点:12 项安全/性能/可维护性风险(顶部有 2026-07-30 状态复核表) |
|
||||
| [TESTING.md](TESTING.md) | 测试基础设施:UI 测试文件清单、运行方式、覆盖率 |
|
||||
| [CONVENTIONS.md](CONVENTIONS.md) | 编码规范:命名约定、代码风格、导入规范、错误处理 |
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [RDAIReaderView/RDAIReaderView-SPEC.md](RDAIReaderView/RDAIReaderView-SPEC.md) | AI 阅读能力产品范围、兼容策略、实施路线与商用验收门槛 |
|
||||
| [RDAIReaderView/RDAIReaderView-ARCHITECTURE.md](RDAIReaderView/RDAIReaderView-ARCHITECTURE.md) | RDAIReaderView 模块、索引、检索、存储、引用和降级架构 |
|
||||
| [RDAIReaderView/RDAIReaderView-API.md](RDAIReaderView/RDAIReaderView-API.md) | RDAIReaderView 公共模型、Provider、Reader Adapter 和服务 API 合同 |
|
||||
| [RDAIReaderView/RDAIReaderView-AI-SPEC.md](RDAIReaderView/RDAIReaderView-AI-SPEC.md) | Natural Language 与 Foundation Models 的 Prompt、结构化输出、安全和评测合同 |
|
||||
| [RDAIReaderView/RDAIReaderView-TEST-PLAN.md](RDAIReaderView/RDAIReaderView-TEST-PLAN.md) | AI 商用测试集、质量指标、真机矩阵、CI、灰度与发布门禁 |
|
||||
| [RDAIReaderView/RDAIReaderView-RELEASE-CHECKLIST.md](RDAIReaderView/RDAIReaderView-RELEASE-CHECKLIST.md) | 本地构建后所需的真机、隐私、TestFlight 与发布验证清单 |
|
||||
| [TYPESetter_PIPELINE.md](TYPESetter_PIPELINE.md) | Typesetter 排版管线详解:HTML 规范化、语义标记注入、CFI 标记、样式合成为、字体规范化、片段标记 |
|
||||
| [CHAPTER_RUNTIME.md](CHAPTER_RUNTIME.md) | 章节运行时详解:按需加载、章节窗口协调、页图管理、磁盘缓存、后台补全 |
|
||||
| [CFI_SUBSYSTEM.md](CFI_SUBSYSTEM.md) | CFI 子系统详解:EPUB CFI 解析、生成、序列化、范围、恢复引擎 |
|
||||
@@ -47,7 +53,7 @@
|
||||
|
||||
## 项目信息
|
||||
|
||||
- **模块总数:** 4 个(EPUBCore、EPUBTextRendering、RDReaderView、EPUBUI)
|
||||
- **模块总数:** 4 个(EPUBCore、EPUBTextRendering、RDEpubReaderView、EPUBUI)
|
||||
- **Swift 文件数:** 142 个(SDK Sources)
|
||||
- **测试用例数:** 23 个测试类,约 99 个测试方法(UI 测试)
|
||||
- **最低 iOS 版本:** 15.6
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
platform :ios, '15.6'
|
||||
use_frameworks!
|
||||
|
||||
workspace 'ReadViewSDK.xcworkspace'
|
||||
project 'ReadViewSDK.xcodeproj'
|
||||
|
||||
target 'ReadViewSDK' do
|
||||
pod 'RDReaderView', :path => '..'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
apply_settings = lambda do |config|
|
||||
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6'
|
||||
end
|
||||
|
||||
# Pods project (Pods.xcodeproj)
|
||||
installer.pods_project.build_configurations.each { |c| apply_settings.call(c) }
|
||||
installer.pods_project.targets.each { |t| t.build_configurations.each { |c| apply_settings.call(c) } }
|
||||
|
||||
installer.aggregate_targets.each do |at|
|
||||
p = at.user_project
|
||||
next unless p
|
||||
|
||||
# User project(s) that integrate Pods (e.g. ReadViewDemo / ReadViewSDK)
|
||||
p.build_configurations.each { |c| apply_settings.call(c) }
|
||||
p.targets.each { |t| t.build_configurations.each { |c| apply_settings.call(c) } }
|
||||
p.save
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "RDEpubReaderView"
|
||||
s.module_name = "RDEpubReaderView"
|
||||
s.version = "0.0.1"
|
||||
s.summary = "A reader view for EPUB and plain-text books"
|
||||
s.platform = :ios, "15.0"
|
||||
s.swift_versions = ["5.10"]
|
||||
s.homepage = "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git"
|
||||
s.author = { "shenlei" => "shenlei@touchread.com" }
|
||||
s.source = { :git => "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git", :tag => "#{s.version}" }
|
||||
s.license = "MIT"
|
||||
s.source_files = "Sources/RDEpubReaderView/**/*.{swift}"
|
||||
s.resource_bundles = {
|
||||
"RDEpubReaderViewAssets" => ["Sources/RDEpubReaderView/EPUBCore/Resources/**/*"]
|
||||
}
|
||||
s.dependency "ZIPFoundation", "~> 0.9"
|
||||
s.dependency "DTCoreText", "~> 1.6"
|
||||
s.requires_arc = true
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = "RDReaderView"
|
||||
s.version = "0.0.1"
|
||||
s.summary = "A reader view for novel"
|
||||
s.platform = :ios, "15.6"
|
||||
s.swift_versions = ["5.10"]
|
||||
s.homepage = "https://github.com/namesubai/RDReaderView.git"
|
||||
s.author = { "subai" => "804663401@qq.com" }
|
||||
s.source = { :git => "https://github.com/namesubai/RDReaderView.git", :tag => "#{s.version}"}
|
||||
s.license = "MIT"
|
||||
s.source_files = 'Sources/RDReaderView/**/*.{swift}'
|
||||
s.resource_bundles = {
|
||||
'RDReaderViewAssets' => ['Sources/RDReaderView/EPUBCore/Resources/**/*']
|
||||
}
|
||||
s.dependency 'ZIPFoundation', '~> 0.9'
|
||||
s.dependency 'DTCoreText', '~> 1.6'
|
||||
s.requires_arc = true
|
||||
|
||||
end
|
||||
+19
-3
@@ -3,14 +3,30 @@ platform :ios, '15.6'
|
||||
target 'ReadViewDemo' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
use_frameworks!
|
||||
pod 'RDReaderView', :path => '..'
|
||||
pod 'RDSpeechReaderView/AI', :path => '../Sources/RDSpeechReaderView'
|
||||
pod 'RDAIReaderView/NaturalLanguage', :path => '../Sources/RDAIReaderView'
|
||||
pod 'RDAIReaderView/FoundationModels', :path => '../Sources/RDAIReaderView'
|
||||
pod 'RDAIReaderView/UI', :path => '../Sources/RDAIReaderView'
|
||||
pod 'RDEpubReaderView', :path => '../Sources/RDEpubReaderView'
|
||||
pod 'RDEpubReaderView/Speech', :path => '../Sources/RDEpubReaderView'
|
||||
pod 'RDEpubReaderView/AI', :path => '../Sources/RDEpubReaderView'
|
||||
pod 'RDPDFReaderView', :path => '../Sources/RDPDFReaderView'
|
||||
pod 'RDPDFReaderView/Speech', :path => '../Sources/RDPDFReaderView'
|
||||
pod 'RDPDFReaderView/AI', :path => '../Sources/RDPDFReaderView'
|
||||
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
# 不再由本 Podfile 全局关闭 ENABLE_USER_SCRIPT_SANDBOXING——之前的写法会把
|
||||
# 宿主工程一起强制改成 NO,属于我们主动降级安全基线。现在交回工程默认值。
|
||||
#
|
||||
# 注意:本工程用 use_frameworks!,CocoaPods 生成的 [CP] Embed Pods Frameworks
|
||||
# 脚本阶段用 rsync 往 .app/Frameworks 拷贝产物;一旦把该设置显式打成 YES,
|
||||
# 这一步会被沙箱拒绝(file-write-unlink / utimensat: Operation not permitted)
|
||||
# 而整个构建失败——已实测确认。真要打开必须先改掉动态框架集成方式
|
||||
# (静态库 / SPM),别只翻这个开关。
|
||||
apply_settings = lambda do |config|
|
||||
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||
end
|
||||
|
||||
# Pods project (Pods.xcodeproj)
|
||||
|
||||
@@ -16,30 +16,87 @@ PODS:
|
||||
- DTFoundation/Core
|
||||
- DTFoundation/UIKit (1.7.19):
|
||||
- DTFoundation/Core
|
||||
- RDReaderView (0.0.1):
|
||||
- RDAIReaderView/Core (0.1.0)
|
||||
- RDAIReaderView/FoundationModels (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- RDAIReaderView/NaturalLanguage (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- RDAIReaderView/UI (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- RDEpubReaderView (0.0.2):
|
||||
- DTCoreText (~> 1.6)
|
||||
- RDEpubReaderView/AI (= 0.0.2)
|
||||
- RDEpubReaderView/Speech (= 0.0.2)
|
||||
- SnapKit (~> 5.7)
|
||||
- ZIPFoundation (~> 0.9)
|
||||
- RDEpubReaderView/AI (0.0.2):
|
||||
- DTCoreText (~> 1.6)
|
||||
- RDAIReaderView/NaturalLanguage (~> 0.1)
|
||||
- RDAIReaderView/UI (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- ZIPFoundation (~> 0.9)
|
||||
- RDEpubReaderView/Speech (0.0.2):
|
||||
- DTCoreText (~> 1.6)
|
||||
- RDSpeechReaderView (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- ZIPFoundation (~> 0.9)
|
||||
- RDPDFReaderView (0.0.1):
|
||||
- RDPDFReaderView/AI (= 0.0.1)
|
||||
- RDPDFReaderView/Speech (= 0.0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- RDPDFReaderView/AI (0.0.1):
|
||||
- RDAIReaderView/NaturalLanguage (~> 0.1)
|
||||
- RDAIReaderView/UI (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- RDPDFReaderView/Speech (0.0.1):
|
||||
- RDSpeechReaderView (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- RDSpeechReaderView (0.1.0):
|
||||
- RDSpeechReaderView/AI (= 0.1.0)
|
||||
- RDSpeechReaderView/AI (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- SnapKit (5.7.1)
|
||||
- ZIPFoundation (0.9.20)
|
||||
|
||||
DEPENDENCIES:
|
||||
- RDReaderView (from `..`)
|
||||
- RDAIReaderView/FoundationModels (from `../Sources/RDAIReaderView`)
|
||||
- RDAIReaderView/NaturalLanguage (from `../Sources/RDAIReaderView`)
|
||||
- RDAIReaderView/UI (from `../Sources/RDAIReaderView`)
|
||||
- RDEpubReaderView (from `../Sources/RDEpubReaderView`)
|
||||
- RDEpubReaderView/AI (from `../Sources/RDEpubReaderView`)
|
||||
- RDEpubReaderView/Speech (from `../Sources/RDEpubReaderView`)
|
||||
- RDPDFReaderView (from `../Sources/RDPDFReaderView`)
|
||||
- RDPDFReaderView/AI (from `../Sources/RDPDFReaderView`)
|
||||
- RDPDFReaderView/Speech (from `../Sources/RDPDFReaderView`)
|
||||
- RDSpeechReaderView/AI (from `../Sources/RDSpeechReaderView`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DTCoreText
|
||||
- DTFoundation
|
||||
- SnapKit
|
||||
- ZIPFoundation
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
RDReaderView:
|
||||
:path: ".."
|
||||
RDAIReaderView:
|
||||
:path: "../Sources/RDAIReaderView"
|
||||
RDEpubReaderView:
|
||||
:path: "../Sources/RDEpubReaderView"
|
||||
RDPDFReaderView:
|
||||
:path: "../Sources/RDPDFReaderView"
|
||||
RDSpeechReaderView:
|
||||
:path: "../Sources/RDSpeechReaderView"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||
RDReaderView: 2e0eeeff4bcfe8bbc09642344d326440af280ed5
|
||||
RDAIReaderView: 01a156e412fb7a52a6963c097ffbe2f1792021fd
|
||||
RDEpubReaderView: 180444055ba2da2c52797cc08b307232f103a6d9
|
||||
RDPDFReaderView: 885be07e2b81cd0a8dc8a937b75faf9017ce045c
|
||||
RDSpeechReaderView: 25fff9437d2692965ba326f37b108baf3e2ae7ef
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
|
||||
PODFILE CHECKSUM: 72bd92309c87ce13a247b8134a321f6b325af731
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "RDAIReaderView",
|
||||
"module_name": "RDAIReaderView",
|
||||
"version": "0.1.0",
|
||||
"summary": "Local-first AI indexing and retrieval primitives for ReadViewSDK readers",
|
||||
"platforms": {
|
||||
"ios": "15.0"
|
||||
},
|
||||
"swift_versions": [
|
||||
"5.10"
|
||||
],
|
||||
"homepage": "https://example.invalid/RDAIReaderView",
|
||||
"authors": {
|
||||
"readoor": "ios@touchread.com"
|
||||
},
|
||||
"source": {
|
||||
"path": "."
|
||||
},
|
||||
"license": "MIT",
|
||||
"requires_arc": true,
|
||||
"subspecs": [
|
||||
{
|
||||
"name": "Core",
|
||||
"source_files": "Core/**/*.swift",
|
||||
"frameworks": "CryptoKit",
|
||||
"libraries": "sqlite3"
|
||||
},
|
||||
{
|
||||
"name": "NaturalLanguage",
|
||||
"source_files": "NaturalLanguage/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDAIReaderView/Core": [
|
||||
"~> 0.1"
|
||||
]
|
||||
},
|
||||
"frameworks": "NaturalLanguage"
|
||||
},
|
||||
{
|
||||
"name": "FoundationModels",
|
||||
"source_files": "FoundationModels/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDAIReaderView/Core": [
|
||||
"~> 0.1"
|
||||
]
|
||||
},
|
||||
"weak_frameworks": "FoundationModels"
|
||||
},
|
||||
{
|
||||
"name": "UI",
|
||||
"source_files": "UI/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDAIReaderView/Core": [
|
||||
"~> 0.1"
|
||||
]
|
||||
},
|
||||
"frameworks": "UIKit"
|
||||
}
|
||||
],
|
||||
"swift_version": "5.10"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "RDEpubReaderView",
|
||||
"module_name": "RDEpubReaderView",
|
||||
"version": "0.0.2",
|
||||
"summary": "A reader view for EPUB, TXT, MOBI, and CBZ books",
|
||||
"platforms": {
|
||||
"ios": "15.0"
|
||||
},
|
||||
"swift_versions": [
|
||||
"5.10"
|
||||
],
|
||||
"homepage": "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git",
|
||||
"authors": {
|
||||
"shenlei": "shenlei@touchread.com"
|
||||
},
|
||||
"source": {
|
||||
"git": "http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK.git",
|
||||
"tag": "0.0.2"
|
||||
},
|
||||
"license": "MIT",
|
||||
"source_files": "{DocumentFormats,EPUBCore,EPUBTextRendering,EPUBUI,ReaderView}/**/*.swift",
|
||||
"resource_bundles": {
|
||||
"RDEpubReaderViewAssets": [
|
||||
"EPUBCore/Resources/**/*"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ZIPFoundation": [
|
||||
"~> 0.9"
|
||||
],
|
||||
"DTCoreText": [
|
||||
"~> 1.6"
|
||||
],
|
||||
"SnapKit": [
|
||||
"~> 5.7"
|
||||
]
|
||||
},
|
||||
"requires_arc": true,
|
||||
"subspecs": [
|
||||
{
|
||||
"name": "Speech",
|
||||
"source_files": "Speech/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDSpeechReaderView": [
|
||||
"~> 0.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AI",
|
||||
"source_files": "AI/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDAIReaderView/NaturalLanguage": [
|
||||
"~> 0.1"
|
||||
],
|
||||
"RDAIReaderView/UI": [
|
||||
"~> 0.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"swift_version": "5.10"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "RDPDFReaderView",
|
||||
"module_name": "RDPDFReaderView",
|
||||
"version": "0.0.1",
|
||||
"summary": "UIKit PDF reader supporting host images and direct PDFKit parsing",
|
||||
"platforms": {
|
||||
"ios": "15.0"
|
||||
},
|
||||
"swift_versions": [
|
||||
"5.10"
|
||||
],
|
||||
"homepage": "https://example.invalid/RDPDFReaderView",
|
||||
"authors": {
|
||||
"readoor": "ios@touchread.com"
|
||||
},
|
||||
"source": {
|
||||
"path": "."
|
||||
},
|
||||
"license": "MIT",
|
||||
"source_files": "{Sources,ReaderView}/**/*.swift",
|
||||
"dependencies": {
|
||||
"SnapKit": [
|
||||
"~> 5.7"
|
||||
]
|
||||
},
|
||||
"frameworks": [
|
||||
"Vision",
|
||||
"CoreImage",
|
||||
"PDFKit"
|
||||
],
|
||||
"requires_arc": true,
|
||||
"subspecs": [
|
||||
{
|
||||
"name": "Speech",
|
||||
"source_files": "Speech/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDSpeechReaderView": [
|
||||
"~> 0.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AI",
|
||||
"source_files": "AI/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDAIReaderView/NaturalLanguage": [
|
||||
"~> 0.1"
|
||||
],
|
||||
"RDAIReaderView/UI": [
|
||||
"~> 0.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"swift_version": "5.10"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "RDReaderView",
|
||||
"version": "0.0.1",
|
||||
"summary": "A reader view for novel",
|
||||
"platforms": {
|
||||
"ios": "15.6"
|
||||
},
|
||||
"swift_versions": [
|
||||
"5.10"
|
||||
],
|
||||
"homepage": "https://github.com/namesubai/RDReaderView.git",
|
||||
"authors": {
|
||||
"subai": "804663401@qq.com"
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/namesubai/RDReaderView.git",
|
||||
"tag": "0.0.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"source_files": "Sources/RDReaderView/**/*.{swift}",
|
||||
"resource_bundles": {
|
||||
"RDReaderViewAssets": [
|
||||
"Sources/RDReaderView/EPUBCore/Resources/**/*"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ZIPFoundation": [
|
||||
"~> 0.9"
|
||||
],
|
||||
"DTCoreText": [
|
||||
"~> 1.6"
|
||||
]
|
||||
},
|
||||
"requires_arc": true,
|
||||
"swift_version": "5.10"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "RDSpeechReaderView",
|
||||
"module_name": "RDSpeechReaderView",
|
||||
"version": "0.1.0",
|
||||
"summary": "Text-to-speech playback primitives for ReadViewSDK readers",
|
||||
"platforms": {
|
||||
"ios": "15.0"
|
||||
},
|
||||
"swift_versions": [
|
||||
"5.10"
|
||||
],
|
||||
"homepage": "https://example.invalid/RDSpeechReaderView",
|
||||
"authors": {
|
||||
"readoor": "ios@touchread.com"
|
||||
},
|
||||
"source": {
|
||||
"path": "."
|
||||
},
|
||||
"license": "MIT",
|
||||
"source_files": "Sources/*.swift",
|
||||
"frameworks": [
|
||||
"AVFAudio",
|
||||
"NaturalLanguage",
|
||||
"MediaPlayer"
|
||||
],
|
||||
"requires_arc": true,
|
||||
"subspecs": [
|
||||
{
|
||||
"name": "AI",
|
||||
"source_files": "AIBridge/**/*.swift",
|
||||
"dependencies": {
|
||||
"RDAIReaderView/Core": [
|
||||
"~> 0.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"swift_version": "5.10"
|
||||
}
|
||||
Generated
+63
-6
@@ -16,30 +16,87 @@ PODS:
|
||||
- DTFoundation/Core
|
||||
- DTFoundation/UIKit (1.7.19):
|
||||
- DTFoundation/Core
|
||||
- RDReaderView (0.0.1):
|
||||
- RDAIReaderView/Core (0.1.0)
|
||||
- RDAIReaderView/FoundationModels (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- RDAIReaderView/NaturalLanguage (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- RDAIReaderView/UI (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- RDEpubReaderView (0.0.2):
|
||||
- DTCoreText (~> 1.6)
|
||||
- RDEpubReaderView/AI (= 0.0.2)
|
||||
- RDEpubReaderView/Speech (= 0.0.2)
|
||||
- SnapKit (~> 5.7)
|
||||
- ZIPFoundation (~> 0.9)
|
||||
- RDEpubReaderView/AI (0.0.2):
|
||||
- DTCoreText (~> 1.6)
|
||||
- RDAIReaderView/NaturalLanguage (~> 0.1)
|
||||
- RDAIReaderView/UI (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- ZIPFoundation (~> 0.9)
|
||||
- RDEpubReaderView/Speech (0.0.2):
|
||||
- DTCoreText (~> 1.6)
|
||||
- RDSpeechReaderView (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- ZIPFoundation (~> 0.9)
|
||||
- RDPDFReaderView (0.0.1):
|
||||
- RDPDFReaderView/AI (= 0.0.1)
|
||||
- RDPDFReaderView/Speech (= 0.0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- RDPDFReaderView/AI (0.0.1):
|
||||
- RDAIReaderView/NaturalLanguage (~> 0.1)
|
||||
- RDAIReaderView/UI (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- RDPDFReaderView/Speech (0.0.1):
|
||||
- RDSpeechReaderView (~> 0.1)
|
||||
- SnapKit (~> 5.7)
|
||||
- RDSpeechReaderView (0.1.0):
|
||||
- RDSpeechReaderView/AI (= 0.1.0)
|
||||
- RDSpeechReaderView/AI (0.1.0):
|
||||
- RDAIReaderView/Core (~> 0.1)
|
||||
- SnapKit (5.7.1)
|
||||
- ZIPFoundation (0.9.20)
|
||||
|
||||
DEPENDENCIES:
|
||||
- RDReaderView (from `..`)
|
||||
- RDAIReaderView/FoundationModels (from `../Sources/RDAIReaderView`)
|
||||
- RDAIReaderView/NaturalLanguage (from `../Sources/RDAIReaderView`)
|
||||
- RDAIReaderView/UI (from `../Sources/RDAIReaderView`)
|
||||
- RDEpubReaderView (from `../Sources/RDEpubReaderView`)
|
||||
- RDEpubReaderView/AI (from `../Sources/RDEpubReaderView`)
|
||||
- RDEpubReaderView/Speech (from `../Sources/RDEpubReaderView`)
|
||||
- RDPDFReaderView (from `../Sources/RDPDFReaderView`)
|
||||
- RDPDFReaderView/AI (from `../Sources/RDPDFReaderView`)
|
||||
- RDPDFReaderView/Speech (from `../Sources/RDPDFReaderView`)
|
||||
- RDSpeechReaderView/AI (from `../Sources/RDSpeechReaderView`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DTCoreText
|
||||
- DTFoundation
|
||||
- SnapKit
|
||||
- ZIPFoundation
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
RDReaderView:
|
||||
:path: ".."
|
||||
RDAIReaderView:
|
||||
:path: "../Sources/RDAIReaderView"
|
||||
RDEpubReaderView:
|
||||
:path: "../Sources/RDEpubReaderView"
|
||||
RDPDFReaderView:
|
||||
:path: "../Sources/RDPDFReaderView"
|
||||
RDSpeechReaderView:
|
||||
:path: "../Sources/RDSpeechReaderView"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DTCoreText: 11b7fe2104f476f82e75a4e3dbdde74d7186cecb
|
||||
DTFoundation: 76b624967cf5bcaae6bb057d622c536c36ef36d0
|
||||
RDReaderView: 2e0eeeff4bcfe8bbc09642344d326440af280ed5
|
||||
RDAIReaderView: 01a156e412fb7a52a6963c097ffbe2f1792021fd
|
||||
RDEpubReaderView: 180444055ba2da2c52797cc08b307232f103a6d9
|
||||
RDPDFReaderView: 885be07e2b81cd0a8dc8a937b75faf9017ce045c
|
||||
RDSpeechReaderView: 25fff9437d2692965ba326f37b108baf3e2ae7ef
|
||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: 775f5c8c488024e24d494aad6331e9fef8f9e2e5
|
||||
PODFILE CHECKSUM: 72bd92309c87ce13a247b8134a321f6b325af731
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
+3806
-2284
File diff suppressed because it is too large
Load Diff
Generated
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Generated
+155
@@ -0,0 +1,155 @@
|
||||
<img src="https://snapkit.github.io/SnapKit/images/banner.jpg" alt="" />
|
||||
|
||||
SnapKit is a DSL to make Auto Layout easy on both iOS and OS X.
|
||||
|
||||
[](https://travis-ci.org/SnapKit/SnapKit)
|
||||
[](https://github.com/SnapKit/SnapKit)
|
||||
[](https://cocoapods.org/pods/SnapKit)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
|
||||
#### ⚠️ **To use with Swift 4.x please ensure you are using >= 4.0.0** ⚠️
|
||||
#### ⚠️ **To use with Swift 5.x please ensure you are using >= 5.0.0** ⚠️
|
||||
|
||||
## Contents
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Migration Guides](#migration-guides)
|
||||
- [Communication](#communication)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Credits](#credits)
|
||||
- [License](#license)
|
||||
|
||||
## Requirements
|
||||
|
||||
- iOS 12.0+ / Mac OS X 10.13+ / tvOS 10.0+
|
||||
- Xcode 10.0+
|
||||
- Swift 4.0+
|
||||
|
||||
## Migration Guides
|
||||
|
||||
- [SnapKit 3.0 Migration Guide](Documentation/SnapKit%203.0%20Migration%20Guide.md)
|
||||
|
||||
## Communication
|
||||
|
||||
- If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/snapkit). (Tag 'snapkit')
|
||||
- If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/snapkit).
|
||||
- If you **found a bug**, open an issue.
|
||||
- If you **have a feature request**, open an issue.
|
||||
- If you **want to contribute**, submit a pull request.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### CocoaPods
|
||||
|
||||
[CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command:
|
||||
|
||||
```bash
|
||||
$ gem install cocoapods
|
||||
```
|
||||
|
||||
> CocoaPods 1.1.0+ is required to build SnapKit 4.0.0+.
|
||||
|
||||
To integrate SnapKit into your Xcode project using CocoaPods, specify it in your `Podfile`:
|
||||
|
||||
```ruby
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
platform :ios, '10.0'
|
||||
use_frameworks!
|
||||
|
||||
target '<Your Target Name>' do
|
||||
pod 'SnapKit', '~> 5.7.0'
|
||||
end
|
||||
```
|
||||
|
||||
Then, run the following command:
|
||||
|
||||
```bash
|
||||
$ pod install
|
||||
```
|
||||
|
||||
### Carthage
|
||||
|
||||
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
|
||||
|
||||
You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
|
||||
|
||||
```bash
|
||||
$ brew update
|
||||
$ brew install carthage
|
||||
```
|
||||
|
||||
To integrate SnapKit into your Xcode project using Carthage, specify it in your `Cartfile`:
|
||||
|
||||
```ogdl
|
||||
github "SnapKit/SnapKit" ~> 5.0.0
|
||||
```
|
||||
|
||||
Run `carthage update` to build the framework and drag the built `SnapKit.framework` into your Xcode project.
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
[Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
|
||||
|
||||
> Xcode 11+ is required to build SnapKit using Swift Package Manager.
|
||||
|
||||
To integrate SnapKit into your Xcode project using Swift Package Manager, add it to the dependencies value of your `Package.swift`:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/SnapKit/SnapKit.git", .upToNextMajor(from: "5.0.1"))
|
||||
]
|
||||
```
|
||||
|
||||
### Manually
|
||||
|
||||
If you prefer not to use either of the aforementioned dependency managers, you can integrate SnapKit into your project manually.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Quick Start
|
||||
|
||||
```swift
|
||||
import SnapKit
|
||||
|
||||
class MyViewController: UIViewController {
|
||||
|
||||
lazy var box = UIView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.addSubview(box)
|
||||
box.backgroundColor = .green
|
||||
box.snp.makeConstraints { (make) -> Void in
|
||||
make.width.height.equalTo(50)
|
||||
make.center.equalTo(self.view)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Playground
|
||||
You can try SnapKit in Playground.
|
||||
|
||||
**Note:**
|
||||
|
||||
> To try SnapKit in playground, open `SnapKit.xcworkspace` and build SnapKit.framework for any simulator first.
|
||||
|
||||
### Resources
|
||||
|
||||
- [Documentation](https://snapkit.github.io/SnapKit/docs/)
|
||||
- [F.A.Q.](https://snapkit.github.io/SnapKit/faq/)
|
||||
|
||||
## Credits
|
||||
|
||||
- Robert Payne ([@robertjpayne](https://twitter.com/robertjpayne))
|
||||
- Many other contributors
|
||||
|
||||
## License
|
||||
|
||||
SnapKit is released under the MIT license. See LICENSE for details.
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public final class Constraint {
|
||||
|
||||
internal let sourceLocation: (String, UInt)
|
||||
internal let label: String?
|
||||
|
||||
private let from: ConstraintItem
|
||||
private let to: ConstraintItem
|
||||
private let relation: ConstraintRelation
|
||||
private let multiplier: ConstraintMultiplierTarget
|
||||
private var constant: ConstraintConstantTarget {
|
||||
didSet {
|
||||
self.updateConstantAndPriorityIfNeeded()
|
||||
}
|
||||
}
|
||||
private var priority: ConstraintPriorityTarget {
|
||||
didSet {
|
||||
self.updateConstantAndPriorityIfNeeded()
|
||||
}
|
||||
}
|
||||
public var layoutConstraints: [LayoutConstraint]
|
||||
|
||||
public var isActive: Bool {
|
||||
set {
|
||||
if newValue {
|
||||
activate()
|
||||
}
|
||||
else {
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
get {
|
||||
for layoutConstraint in self.layoutConstraints {
|
||||
if layoutConstraint.isActive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
internal init(from: ConstraintItem,
|
||||
to: ConstraintItem,
|
||||
relation: ConstraintRelation,
|
||||
sourceLocation: (String, UInt),
|
||||
label: String?,
|
||||
multiplier: ConstraintMultiplierTarget,
|
||||
constant: ConstraintConstantTarget,
|
||||
priority: ConstraintPriorityTarget) {
|
||||
self.from = from
|
||||
self.to = to
|
||||
self.relation = relation
|
||||
self.sourceLocation = sourceLocation
|
||||
self.label = label
|
||||
self.multiplier = multiplier
|
||||
self.constant = constant
|
||||
self.priority = priority
|
||||
self.layoutConstraints = []
|
||||
|
||||
// get attributes
|
||||
let layoutFromAttributes = self.from.attributes.layoutAttributes
|
||||
let layoutToAttributes = self.to.attributes.layoutAttributes
|
||||
|
||||
// get layout from
|
||||
let layoutFrom = self.from.layoutConstraintItem!
|
||||
|
||||
// get relation
|
||||
let layoutRelation = self.relation.layoutRelation
|
||||
|
||||
for layoutFromAttribute in layoutFromAttributes {
|
||||
// get layout to attribute
|
||||
let layoutToAttribute: LayoutAttribute
|
||||
#if canImport(UIKit)
|
||||
if layoutToAttributes.count > 0 {
|
||||
if self.from.attributes == .edges && self.to.attributes == .margins {
|
||||
switch layoutFromAttribute {
|
||||
case .left:
|
||||
layoutToAttribute = .leftMargin
|
||||
case .right:
|
||||
layoutToAttribute = .rightMargin
|
||||
case .top:
|
||||
layoutToAttribute = .topMargin
|
||||
case .bottom:
|
||||
layoutToAttribute = .bottomMargin
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
} else if self.from.attributes == .margins && self.to.attributes == .edges {
|
||||
switch layoutFromAttribute {
|
||||
case .leftMargin:
|
||||
layoutToAttribute = .left
|
||||
case .rightMargin:
|
||||
layoutToAttribute = .right
|
||||
case .topMargin:
|
||||
layoutToAttribute = .top
|
||||
case .bottomMargin:
|
||||
layoutToAttribute = .bottom
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
} else if self.from.attributes == .directionalEdges && self.to.attributes == .directionalMargins {
|
||||
switch layoutFromAttribute {
|
||||
case .leading:
|
||||
layoutToAttribute = .leadingMargin
|
||||
case .trailing:
|
||||
layoutToAttribute = .trailingMargin
|
||||
case .top:
|
||||
layoutToAttribute = .topMargin
|
||||
case .bottom:
|
||||
layoutToAttribute = .bottomMargin
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
} else if self.from.attributes == .directionalMargins && self.to.attributes == .directionalEdges {
|
||||
switch layoutFromAttribute {
|
||||
case .leadingMargin:
|
||||
layoutToAttribute = .leading
|
||||
case .trailingMargin:
|
||||
layoutToAttribute = .trailing
|
||||
case .topMargin:
|
||||
layoutToAttribute = .top
|
||||
case .bottomMargin:
|
||||
layoutToAttribute = .bottom
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
} else if self.from.attributes == self.to.attributes {
|
||||
layoutToAttribute = layoutFromAttribute
|
||||
} else {
|
||||
layoutToAttribute = layoutToAttributes[0]
|
||||
}
|
||||
} else {
|
||||
if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
|
||||
layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
|
||||
} else {
|
||||
layoutToAttribute = layoutFromAttribute
|
||||
}
|
||||
}
|
||||
#else
|
||||
if self.from.attributes == self.to.attributes {
|
||||
layoutToAttribute = layoutFromAttribute
|
||||
} else if layoutToAttributes.count > 0 {
|
||||
layoutToAttribute = layoutToAttributes[0]
|
||||
} else {
|
||||
layoutToAttribute = layoutFromAttribute
|
||||
}
|
||||
#endif
|
||||
|
||||
// get layout constant
|
||||
let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)
|
||||
|
||||
// get layout to
|
||||
var layoutTo: AnyObject? = self.to.target
|
||||
|
||||
// use superview if possible
|
||||
if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
|
||||
layoutTo = layoutFrom.superview
|
||||
}
|
||||
|
||||
// create layout constraint
|
||||
let layoutConstraint = LayoutConstraint(
|
||||
item: layoutFrom,
|
||||
attribute: layoutFromAttribute,
|
||||
relatedBy: layoutRelation,
|
||||
toItem: layoutTo,
|
||||
attribute: layoutToAttribute,
|
||||
multiplier: self.multiplier.constraintMultiplierTargetValue,
|
||||
constant: layoutConstant
|
||||
)
|
||||
|
||||
// set label
|
||||
layoutConstraint.label = self.label
|
||||
|
||||
// set priority
|
||||
layoutConstraint.priority = LayoutPriority(rawValue: self.priority.constraintPriorityTargetValue)
|
||||
|
||||
// set constraint
|
||||
layoutConstraint.constraint = self
|
||||
|
||||
// append
|
||||
self.layoutConstraints.append(layoutConstraint)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@available(*, deprecated, renamed:"activate()")
|
||||
public func install() {
|
||||
self.activate()
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"deactivate()")
|
||||
public func uninstall() {
|
||||
self.deactivate()
|
||||
}
|
||||
|
||||
public func activate() {
|
||||
self.activateIfNeeded()
|
||||
}
|
||||
|
||||
public func deactivate() {
|
||||
self.deactivateIfNeeded()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func update(offset: ConstraintOffsetTarget) -> Constraint {
|
||||
self.constant = offset.constraintOffsetTargetValue
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func update(inset: ConstraintInsetTarget) -> Constraint {
|
||||
self.constant = inset.constraintInsetTargetValue
|
||||
return self
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
@discardableResult
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
public func update(inset: ConstraintDirectionalInsetTarget) -> Constraint {
|
||||
self.constant = inset.constraintDirectionalInsetTargetValue
|
||||
return self
|
||||
}
|
||||
#endif
|
||||
|
||||
@discardableResult
|
||||
public func update(priority: ConstraintPriorityTarget) -> Constraint {
|
||||
self.priority = priority.constraintPriorityTargetValue
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func update(priority: ConstraintPriority) -> Constraint {
|
||||
self.priority = priority.value
|
||||
return self
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"update(offset:)")
|
||||
public func updateOffset(amount: ConstraintOffsetTarget) -> Void { self.update(offset: amount) }
|
||||
|
||||
@available(*, deprecated, renamed:"update(inset:)")
|
||||
public func updateInsets(amount: ConstraintInsetTarget) -> Void { self.update(inset: amount) }
|
||||
|
||||
@available(*, deprecated, renamed:"update(priority:)")
|
||||
public func updatePriority(amount: ConstraintPriorityTarget) -> Void { self.update(priority: amount) }
|
||||
|
||||
@available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.")
|
||||
public func updatePriorityRequired() -> Void {}
|
||||
|
||||
@available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.")
|
||||
public func updatePriorityHigh() -> Void { fatalError("Must be implemented by Concrete subclass.") }
|
||||
|
||||
@available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.")
|
||||
public func updatePriorityMedium() -> Void { fatalError("Must be implemented by Concrete subclass.") }
|
||||
|
||||
@available(*, deprecated, message:"Use update(priority: ConstraintPriorityTarget) instead.")
|
||||
public func updatePriorityLow() -> Void { fatalError("Must be implemented by Concrete subclass.") }
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
internal func updateConstantAndPriorityIfNeeded() {
|
||||
for layoutConstraint in self.layoutConstraints {
|
||||
let attribute = (layoutConstraint.secondAttribute == .notAnAttribute) ? layoutConstraint.firstAttribute : layoutConstraint.secondAttribute
|
||||
layoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: attribute)
|
||||
|
||||
let requiredPriority = ConstraintPriority.required.value
|
||||
if (layoutConstraint.priority.rawValue < requiredPriority), (self.priority.constraintPriorityTargetValue != requiredPriority) {
|
||||
layoutConstraint.priority = LayoutPriority(rawValue: self.priority.constraintPriorityTargetValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func activateIfNeeded(updatingExisting: Bool = false) {
|
||||
guard let item = self.from.layoutConstraintItem else {
|
||||
print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
|
||||
return
|
||||
}
|
||||
let layoutConstraints = self.layoutConstraints
|
||||
|
||||
if updatingExisting {
|
||||
var existingLayoutConstraints: [LayoutConstraint] = []
|
||||
for constraint in item.constraints {
|
||||
existingLayoutConstraints += constraint.layoutConstraints
|
||||
}
|
||||
|
||||
for layoutConstraint in layoutConstraints {
|
||||
let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
|
||||
guard let updateLayoutConstraint = existingLayoutConstraint else {
|
||||
fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
|
||||
}
|
||||
|
||||
let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
|
||||
updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
|
||||
}
|
||||
} else {
|
||||
NSLayoutConstraint.activate(layoutConstraints)
|
||||
item.add(constraints: [self])
|
||||
}
|
||||
}
|
||||
|
||||
internal func deactivateIfNeeded() {
|
||||
guard let item = self.from.layoutConstraintItem else {
|
||||
print("WARNING: SnapKit failed to get from item from constraint. Deactivate will be a no-op.")
|
||||
return
|
||||
}
|
||||
let layoutConstraints = self.layoutConstraints
|
||||
NSLayoutConstraint.deactivate(layoutConstraints)
|
||||
item.remove(constraints: [self])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
internal struct ConstraintAttributes : OptionSet, ExpressibleByIntegerLiteral {
|
||||
|
||||
typealias IntegerLiteralType = UInt
|
||||
|
||||
internal init(rawValue: UInt) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
internal init(_ rawValue: UInt) {
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
internal init(nilLiteral: ()) {
|
||||
self.rawValue = 0
|
||||
}
|
||||
internal init(integerLiteral rawValue: IntegerLiteralType) {
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
|
||||
internal private(set) var rawValue: UInt
|
||||
internal static var allZeros: ConstraintAttributes { return 0 }
|
||||
internal static func convertFromNilLiteral() -> ConstraintAttributes { return 0 }
|
||||
internal var boolValue: Bool { return self.rawValue != 0 }
|
||||
|
||||
internal func toRaw() -> UInt { return self.rawValue }
|
||||
internal static func fromRaw(_ raw: UInt) -> ConstraintAttributes? { return self.init(raw) }
|
||||
internal static func fromMask(_ raw: UInt) -> ConstraintAttributes { return self.init(raw) }
|
||||
|
||||
// normal
|
||||
|
||||
internal static let none: ConstraintAttributes = 0
|
||||
internal static let left: ConstraintAttributes = ConstraintAttributes(UInt(1) << 0)
|
||||
internal static let top: ConstraintAttributes = ConstraintAttributes(UInt(1) << 1)
|
||||
internal static let right: ConstraintAttributes = ConstraintAttributes(UInt(1) << 2)
|
||||
internal static let bottom: ConstraintAttributes = ConstraintAttributes(UInt(1) << 3)
|
||||
internal static let leading: ConstraintAttributes = ConstraintAttributes(UInt(1) << 4)
|
||||
internal static let trailing: ConstraintAttributes = ConstraintAttributes(UInt(1) << 5)
|
||||
internal static let width: ConstraintAttributes = ConstraintAttributes(UInt(1) << 6)
|
||||
internal static let height: ConstraintAttributes = ConstraintAttributes(UInt(1) << 7)
|
||||
internal static let centerX: ConstraintAttributes = ConstraintAttributes(UInt(1) << 8)
|
||||
internal static let centerY: ConstraintAttributes = ConstraintAttributes(UInt(1) << 9)
|
||||
internal static let lastBaseline: ConstraintAttributes = ConstraintAttributes(UInt(1) << 10)
|
||||
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
internal static let firstBaseline: ConstraintAttributes = ConstraintAttributes(UInt(1) << 11)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let leftMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 12)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let rightMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 13)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let topMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 14)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let bottomMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 15)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let leadingMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 16)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let trailingMargin: ConstraintAttributes = ConstraintAttributes(UInt(1) << 17)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let centerXWithinMargins: ConstraintAttributes = ConstraintAttributes(UInt(1) << 18)
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let centerYWithinMargins: ConstraintAttributes = ConstraintAttributes(UInt(1) << 19)
|
||||
|
||||
// aggregates
|
||||
|
||||
internal static let edges: ConstraintAttributes = [.horizontalEdges, .verticalEdges]
|
||||
internal static let horizontalEdges: ConstraintAttributes = [.left, .right]
|
||||
internal static let verticalEdges: ConstraintAttributes = [.top, .bottom]
|
||||
internal static let directionalEdges: ConstraintAttributes = [.directionalHorizontalEdges, .directionalVerticalEdges]
|
||||
internal static let directionalHorizontalEdges: ConstraintAttributes = [.leading, .trailing]
|
||||
internal static let directionalVerticalEdges: ConstraintAttributes = [.top, .bottom]
|
||||
internal static let size: ConstraintAttributes = [.width, .height]
|
||||
internal static let center: ConstraintAttributes = [.centerX, .centerY]
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let margins: ConstraintAttributes = [.leftMargin, .topMargin, .rightMargin, .bottomMargin]
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let directionalMargins: ConstraintAttributes = [.leadingMargin, .topMargin, .trailingMargin, .bottomMargin]
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
internal static let centerWithinMargins: ConstraintAttributes = [.centerXWithinMargins, .centerYWithinMargins]
|
||||
|
||||
internal var layoutAttributes:[LayoutAttribute] {
|
||||
var attrs = [LayoutAttribute]()
|
||||
if (self.contains(ConstraintAttributes.left)) {
|
||||
attrs.append(.left)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.top)) {
|
||||
attrs.append(.top)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.right)) {
|
||||
attrs.append(.right)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.bottom)) {
|
||||
attrs.append(.bottom)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.leading)) {
|
||||
attrs.append(.leading)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.trailing)) {
|
||||
attrs.append(.trailing)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.width)) {
|
||||
attrs.append(.width)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.height)) {
|
||||
attrs.append(.height)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.centerX)) {
|
||||
attrs.append(.centerX)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.centerY)) {
|
||||
attrs.append(.centerY)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.lastBaseline)) {
|
||||
attrs.append(.lastBaseline)
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
if (self.contains(ConstraintAttributes.firstBaseline)) {
|
||||
attrs.append(.firstBaseline)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.leftMargin)) {
|
||||
attrs.append(.leftMargin)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.rightMargin)) {
|
||||
attrs.append(.rightMargin)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.topMargin)) {
|
||||
attrs.append(.topMargin)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.bottomMargin)) {
|
||||
attrs.append(.bottomMargin)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.leadingMargin)) {
|
||||
attrs.append(.leadingMargin)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.trailingMargin)) {
|
||||
attrs.append(.trailingMargin)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.centerXWithinMargins)) {
|
||||
attrs.append(.centerXWithinMargins)
|
||||
}
|
||||
if (self.contains(ConstraintAttributes.centerYWithinMargins)) {
|
||||
attrs.append(.centerYWithinMargins)
|
||||
}
|
||||
#endif
|
||||
|
||||
return attrs
|
||||
}
|
||||
}
|
||||
|
||||
internal func + (left: ConstraintAttributes, right: ConstraintAttributes) -> ConstraintAttributes {
|
||||
return left.union(right)
|
||||
}
|
||||
|
||||
internal func +=(left: inout ConstraintAttributes, right: ConstraintAttributes) {
|
||||
left.formUnion(right)
|
||||
}
|
||||
|
||||
internal func -=(left: inout ConstraintAttributes, right: ConstraintAttributes) {
|
||||
left.subtract(right)
|
||||
}
|
||||
|
||||
internal func ==(left: ConstraintAttributes, right: ConstraintAttributes) -> Bool {
|
||||
return left.rawValue == right.rawValue
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
public typealias ConstraintInterfaceLayoutDirection = UIUserInterfaceLayoutDirection
|
||||
#else
|
||||
import AppKit
|
||||
public typealias ConstraintInterfaceLayoutDirection = NSUserInterfaceLayoutDirection
|
||||
#endif
|
||||
|
||||
|
||||
public struct ConstraintConfig {
|
||||
|
||||
public static var interfaceLayoutDirection: ConstraintInterfaceLayoutDirection = .leftToRight
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
extension CGPoint: ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
extension CGSize: ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
extension ConstraintInsets: ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
extension ConstraintDirectionalInsets: ConstraintConstantTarget {
|
||||
}
|
||||
#endif
|
||||
|
||||
extension ConstraintConstantTarget {
|
||||
|
||||
internal func constraintConstantTargetValueFor(layoutAttribute: LayoutAttribute) -> CGFloat {
|
||||
if let value = self as? CGFloat {
|
||||
return value
|
||||
}
|
||||
|
||||
if let value = self as? Float {
|
||||
return CGFloat(value)
|
||||
}
|
||||
|
||||
if let value = self as? Double {
|
||||
return CGFloat(value)
|
||||
}
|
||||
|
||||
if let value = self as? Int {
|
||||
return CGFloat(value)
|
||||
}
|
||||
|
||||
if let value = self as? UInt {
|
||||
return CGFloat(value)
|
||||
}
|
||||
|
||||
if let value = self as? CGSize {
|
||||
if layoutAttribute == .width {
|
||||
return value.width
|
||||
} else if layoutAttribute == .height {
|
||||
return value.height
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
if let value = self as? CGPoint {
|
||||
#if canImport(UIKit)
|
||||
switch layoutAttribute {
|
||||
case .left, .right, .leading, .trailing, .centerX, .leftMargin, .rightMargin, .leadingMargin, .trailingMargin, .centerXWithinMargins:
|
||||
return value.x
|
||||
case .top, .bottom, .centerY, .topMargin, .bottomMargin, .centerYWithinMargins, .lastBaseline, .firstBaseline:
|
||||
return value.y
|
||||
case .width, .height, .notAnAttribute:
|
||||
return 0.0
|
||||
#if swift(>=5.0)
|
||||
@unknown default:
|
||||
return 0.0
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
switch layoutAttribute {
|
||||
case .left, .right, .leading, .trailing, .centerX:
|
||||
return value.x
|
||||
case .top, .bottom, .centerY, .lastBaseline, .firstBaseline:
|
||||
return value.y
|
||||
case .width, .height, .notAnAttribute:
|
||||
return 0.0
|
||||
#if swift(>=5.0)
|
||||
@unknown default:
|
||||
return 0.0
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if let value = self as? ConstraintInsets {
|
||||
#if canImport(UIKit)
|
||||
switch layoutAttribute {
|
||||
case .left, .leftMargin:
|
||||
return value.left
|
||||
case .top, .topMargin, .firstBaseline:
|
||||
return value.top
|
||||
case .right, .rightMargin:
|
||||
return -value.right
|
||||
case .bottom, .bottomMargin, .lastBaseline:
|
||||
return -value.bottom
|
||||
case .leading, .leadingMargin:
|
||||
return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.left : value.right
|
||||
case .trailing, .trailingMargin:
|
||||
return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.right : -value.left
|
||||
case .centerX, .centerXWithinMargins:
|
||||
return (value.left - value.right) / 2
|
||||
case .centerY, .centerYWithinMargins:
|
||||
return (value.top - value.bottom) / 2
|
||||
case .width:
|
||||
return -(value.left + value.right)
|
||||
case .height:
|
||||
return -(value.top + value.bottom)
|
||||
case .notAnAttribute:
|
||||
return 0.0
|
||||
#if swift(>=5.0)
|
||||
@unknown default:
|
||||
return 0.0
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
switch layoutAttribute {
|
||||
case .left:
|
||||
return value.left
|
||||
case .top, .firstBaseline:
|
||||
return value.top
|
||||
case .right:
|
||||
return -value.right
|
||||
case .bottom, .lastBaseline:
|
||||
return -value.bottom
|
||||
case .leading:
|
||||
return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.left : value.right
|
||||
case .trailing:
|
||||
return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.right : -value.left
|
||||
case .centerX:
|
||||
return (value.left - value.right) / 2
|
||||
case .centerY:
|
||||
return (value.top - value.bottom) / 2
|
||||
case .width:
|
||||
return -(value.left + value.right)
|
||||
case .height:
|
||||
return -(value.top + value.bottom)
|
||||
case .notAnAttribute:
|
||||
return 0.0
|
||||
#if swift(>=5.0)
|
||||
@unknown default:
|
||||
return 0.0
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
if #available(iOS 11.0, tvOS 11.0, *), let value = self as? ConstraintDirectionalInsets {
|
||||
switch layoutAttribute {
|
||||
case .left, .leftMargin:
|
||||
return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.leading : value.trailing
|
||||
case .top, .topMargin, .firstBaseline:
|
||||
return value.top
|
||||
case .right, .rightMargin:
|
||||
return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.trailing : -value.leading
|
||||
case .bottom, .bottomMargin, .lastBaseline:
|
||||
return -value.bottom
|
||||
case .leading, .leadingMargin:
|
||||
return value.leading
|
||||
case .trailing, .trailingMargin:
|
||||
return -value.trailing
|
||||
case .centerX, .centerXWithinMargins:
|
||||
return (value.leading - value.trailing) / 2
|
||||
case .centerY, .centerYWithinMargins:
|
||||
return (value.top - value.bottom) / 2
|
||||
case .width:
|
||||
return -(value.leading + value.trailing)
|
||||
case .height:
|
||||
return -(value.top + value.bottom)
|
||||
case .notAnAttribute:
|
||||
return 0.0
|
||||
#if swift(>=5.0)
|
||||
@unknown default:
|
||||
return 0.0
|
||||
#else
|
||||
default:
|
||||
return 0.0
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0.0
|
||||
}
|
||||
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintDSL {
|
||||
|
||||
var target: AnyObject? { get }
|
||||
|
||||
func setLabel(_ value: String?)
|
||||
func label() -> String?
|
||||
|
||||
}
|
||||
extension ConstraintDSL {
|
||||
|
||||
public func setLabel(_ value: String?) {
|
||||
objc_setAssociatedObject(self.target as Any, &labelKey, value, .OBJC_ASSOCIATION_COPY_NONATOMIC)
|
||||
}
|
||||
public func label() -> String? {
|
||||
return objc_getAssociatedObject(self.target as Any, &labelKey) as? String
|
||||
}
|
||||
|
||||
}
|
||||
private var labelKey: UInt8 = 0
|
||||
|
||||
|
||||
public protocol ConstraintBasicAttributesDSL : ConstraintDSL {
|
||||
}
|
||||
extension ConstraintBasicAttributesDSL {
|
||||
|
||||
// MARK: Basics
|
||||
|
||||
public var left: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.left)
|
||||
}
|
||||
|
||||
public var top: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top)
|
||||
}
|
||||
|
||||
public var right: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.right)
|
||||
}
|
||||
|
||||
public var bottom: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom)
|
||||
}
|
||||
|
||||
public var leading: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leading)
|
||||
}
|
||||
|
||||
public var trailing: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.trailing)
|
||||
}
|
||||
|
||||
public var width: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.width)
|
||||
}
|
||||
|
||||
public var height: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.height)
|
||||
}
|
||||
|
||||
public var centerX: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerX)
|
||||
}
|
||||
|
||||
public var centerY: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerY)
|
||||
}
|
||||
|
||||
public var edges: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.edges)
|
||||
}
|
||||
|
||||
public var directionalEdges: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalEdges)
|
||||
}
|
||||
|
||||
public var horizontalEdges: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.horizontalEdges)
|
||||
}
|
||||
|
||||
public var verticalEdges: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.verticalEdges)
|
||||
}
|
||||
|
||||
public var directionalHorizontalEdges: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalHorizontalEdges)
|
||||
}
|
||||
|
||||
public var directionalVerticalEdges: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalVerticalEdges)
|
||||
}
|
||||
|
||||
public var size: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.size)
|
||||
}
|
||||
|
||||
public var center: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.center)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public protocol ConstraintAttributesDSL : ConstraintBasicAttributesDSL {
|
||||
}
|
||||
extension ConstraintAttributesDSL {
|
||||
|
||||
// MARK: Baselines
|
||||
@available(*, deprecated, renamed:"lastBaseline")
|
||||
public var baseline: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.lastBaseline)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
public var lastBaseline: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.lastBaseline)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
public var firstBaseline: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.firstBaseline)
|
||||
}
|
||||
|
||||
// MARK: Margins
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var leftMargin: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leftMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var topMargin: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.topMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var rightMargin: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.rightMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var bottomMargin: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottomMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var leadingMargin: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.leadingMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var trailingMargin: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.trailingMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerXWithinMargins: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerXWithinMargins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerYWithinMargins: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerYWithinMargins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var margins: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.margins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var directionalMargins: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.directionalMargins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerWithinMargins: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.centerWithinMargins)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public class ConstraintDescription {
|
||||
|
||||
internal let item: LayoutConstraintItem
|
||||
internal var attributes: ConstraintAttributes
|
||||
internal var relation: ConstraintRelation? = nil
|
||||
internal var sourceLocation: (String, UInt)? = nil
|
||||
internal var label: String? = nil
|
||||
internal var related: ConstraintItem? = nil
|
||||
internal var multiplier: ConstraintMultiplierTarget = 1.0
|
||||
internal var constant: ConstraintConstantTarget = 0.0
|
||||
internal var priority: ConstraintPriorityTarget = 1000.0
|
||||
internal lazy var constraint: Constraint? = {
|
||||
guard let relation = self.relation,
|
||||
let related = self.related,
|
||||
let sourceLocation = self.sourceLocation else {
|
||||
return nil
|
||||
}
|
||||
let from = ConstraintItem(target: self.item, attributes: self.attributes)
|
||||
|
||||
return Constraint(
|
||||
from: from,
|
||||
to: related,
|
||||
relation: relation,
|
||||
sourceLocation: sourceLocation,
|
||||
label: self.label,
|
||||
multiplier: self.multiplier,
|
||||
constant: self.constant,
|
||||
priority: self.priority
|
||||
)
|
||||
}()
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
internal init(item: LayoutConstraintItem, attributes: ConstraintAttributes) {
|
||||
self.item = item
|
||||
self.attributes = attributes
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
#if canImport(UIKit)
|
||||
public protocol ConstraintDirectionalInsetTarget: ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
extension ConstraintDirectionalInsets: ConstraintDirectionalInsetTarget {
|
||||
}
|
||||
|
||||
extension ConstraintDirectionalInsetTarget {
|
||||
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
internal var constraintDirectionalInsetTargetValue: ConstraintDirectionalInsets {
|
||||
if let amount = self as? ConstraintDirectionalInsets {
|
||||
return amount
|
||||
} else {
|
||||
return ConstraintDirectionalInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
#if canImport(UIKit)
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
public typealias ConstraintDirectionalInsets = NSDirectionalEdgeInsets
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintInsetTarget: ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
extension Int: ConstraintInsetTarget {
|
||||
}
|
||||
|
||||
extension UInt: ConstraintInsetTarget {
|
||||
}
|
||||
|
||||
extension Float: ConstraintInsetTarget {
|
||||
}
|
||||
|
||||
extension Double: ConstraintInsetTarget {
|
||||
}
|
||||
|
||||
extension CGFloat: ConstraintInsetTarget {
|
||||
}
|
||||
|
||||
extension ConstraintInsets: ConstraintInsetTarget {
|
||||
}
|
||||
|
||||
extension ConstraintInsetTarget {
|
||||
|
||||
internal var constraintInsetTargetValue: ConstraintInsets {
|
||||
if let amount = self as? ConstraintInsets {
|
||||
return amount
|
||||
} else if let amount = self as? Float {
|
||||
return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
|
||||
} else if let amount = self as? Double {
|
||||
return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
|
||||
} else if let amount = self as? CGFloat {
|
||||
return ConstraintInsets(top: amount, left: amount, bottom: amount, right: amount)
|
||||
} else if let amount = self as? Int {
|
||||
return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
|
||||
} else if let amount = self as? UInt {
|
||||
return ConstraintInsets(top: CGFloat(amount), left: CGFloat(amount), bottom: CGFloat(amount), right: CGFloat(amount))
|
||||
} else {
|
||||
return ConstraintInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
#if canImport(UIKit)
|
||||
public typealias ConstraintInsets = UIEdgeInsets
|
||||
#else
|
||||
public typealias ConstraintInsets = NSEdgeInsets
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public final class ConstraintItem {
|
||||
|
||||
internal weak var target: AnyObject?
|
||||
internal let attributes: ConstraintAttributes
|
||||
|
||||
internal init(target: AnyObject?, attributes: ConstraintAttributes) {
|
||||
self.target = target
|
||||
self.attributes = attributes
|
||||
}
|
||||
|
||||
internal var layoutConstraintItem: LayoutConstraintItem? {
|
||||
return self.target as? LayoutConstraintItem
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func ==(lhs: ConstraintItem, rhs: ConstraintItem) -> Bool {
|
||||
// pointer equality
|
||||
guard lhs !== rhs else {
|
||||
return true
|
||||
}
|
||||
|
||||
// must both have valid targets and identical attributes
|
||||
guard let target1 = lhs.target,
|
||||
let target2 = rhs.target,
|
||||
target1 === target2 && lhs.attributes == rhs.attributes else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
@available(iOS 9.0, OSX 10.11, *)
|
||||
public extension ConstraintLayoutGuide {
|
||||
|
||||
var snp: ConstraintLayoutGuideDSL {
|
||||
return ConstraintLayoutGuideDSL(guide: self)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
#if canImport(UIKit)
|
||||
@available(iOS 9.0, *)
|
||||
public typealias ConstraintLayoutGuide = UILayoutGuide
|
||||
#else
|
||||
@available(OSX 10.11, *)
|
||||
public typealias ConstraintLayoutGuide = NSLayoutGuide
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
@available(iOS 9.0, OSX 10.11, *)
|
||||
public struct ConstraintLayoutGuideDSL: ConstraintAttributesDSL {
|
||||
|
||||
@discardableResult
|
||||
public func prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
|
||||
return ConstraintMaker.prepareConstraints(item: self.guide, closure: closure)
|
||||
}
|
||||
|
||||
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
ConstraintMaker.makeConstraints(item: self.guide, closure: closure)
|
||||
}
|
||||
|
||||
public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
ConstraintMaker.remakeConstraints(item: self.guide, closure: closure)
|
||||
}
|
||||
|
||||
public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
ConstraintMaker.updateConstraints(item: self.guide, closure: closure)
|
||||
}
|
||||
|
||||
public func removeConstraints() {
|
||||
ConstraintMaker.removeConstraints(item: self.guide)
|
||||
}
|
||||
|
||||
public var target: AnyObject? {
|
||||
return self.guide
|
||||
}
|
||||
|
||||
internal let guide: ConstraintLayoutGuide
|
||||
|
||||
internal init(guide: ConstraintLayoutGuide) {
|
||||
self.guide = guide
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
#if canImport(UIKit)
|
||||
@available(iOS 8.0, *)
|
||||
public typealias ConstraintLayoutSupport = UILayoutSupport
|
||||
#else
|
||||
public class ConstraintLayoutSupport {}
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public struct ConstraintLayoutSupportDSL: ConstraintDSL {
|
||||
|
||||
public var target: AnyObject? {
|
||||
return self.support
|
||||
}
|
||||
|
||||
internal let support: ConstraintLayoutSupport
|
||||
|
||||
internal init(support: ConstraintLayoutSupport) {
|
||||
self.support = support
|
||||
|
||||
}
|
||||
|
||||
public var top: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.top)
|
||||
}
|
||||
|
||||
public var bottom: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.bottom)
|
||||
}
|
||||
|
||||
public var height: ConstraintItem {
|
||||
return ConstraintItem(target: self.target, attributes: ConstraintAttributes.height)
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public class ConstraintMaker {
|
||||
|
||||
public var left: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.left)
|
||||
}
|
||||
|
||||
public var top: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.top)
|
||||
}
|
||||
|
||||
public var bottom: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.bottom)
|
||||
}
|
||||
|
||||
public var right: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.right)
|
||||
}
|
||||
|
||||
public var leading: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.leading)
|
||||
}
|
||||
|
||||
public var trailing: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.trailing)
|
||||
}
|
||||
|
||||
public var width: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.width)
|
||||
}
|
||||
|
||||
public var height: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.height)
|
||||
}
|
||||
|
||||
public var centerX: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.centerX)
|
||||
}
|
||||
|
||||
public var centerY: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.centerY)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"lastBaseline")
|
||||
public var baseline: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.lastBaseline)
|
||||
}
|
||||
|
||||
public var lastBaseline: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.lastBaseline)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
public var firstBaseline: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.firstBaseline)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var leftMargin: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.leftMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var rightMargin: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.rightMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var topMargin: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.topMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var bottomMargin: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.bottomMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var leadingMargin: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.leadingMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var trailingMargin: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.trailingMargin)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerXWithinMargins: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.centerXWithinMargins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerYWithinMargins: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.centerYWithinMargins)
|
||||
}
|
||||
|
||||
public var edges: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.edges)
|
||||
}
|
||||
public var horizontalEdges: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.horizontalEdges)
|
||||
}
|
||||
public var verticalEdges: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.verticalEdges)
|
||||
}
|
||||
public var directionalEdges: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.directionalEdges)
|
||||
}
|
||||
public var directionalHorizontalEdges: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.directionalHorizontalEdges)
|
||||
}
|
||||
public var directionalVerticalEdges: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.directionalVerticalEdges)
|
||||
}
|
||||
public var size: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.size)
|
||||
}
|
||||
public var center: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.center)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var margins: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.margins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var directionalMargins: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.directionalMargins)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerWithinMargins: ConstraintMakerExtendable {
|
||||
return self.makeExtendableWithAttributes(.centerWithinMargins)
|
||||
}
|
||||
|
||||
public let item: LayoutConstraintItem
|
||||
private var descriptions = [ConstraintDescription]()
|
||||
|
||||
internal init(item: LayoutConstraintItem) {
|
||||
self.item = item
|
||||
self.item.prepare()
|
||||
}
|
||||
|
||||
internal func makeExtendableWithAttributes(_ attributes: ConstraintAttributes) -> ConstraintMakerExtendable {
|
||||
let description = ConstraintDescription(item: self.item, attributes: attributes)
|
||||
self.descriptions.append(description)
|
||||
return ConstraintMakerExtendable(description)
|
||||
}
|
||||
|
||||
internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
|
||||
let maker = ConstraintMaker(item: item)
|
||||
closure(maker)
|
||||
var constraints: [Constraint] = []
|
||||
for description in maker.descriptions {
|
||||
guard let constraint = description.constraint else {
|
||||
continue
|
||||
}
|
||||
constraints.append(constraint)
|
||||
}
|
||||
return constraints
|
||||
}
|
||||
|
||||
internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
|
||||
let constraints = prepareConstraints(item: item, closure: closure)
|
||||
for constraint in constraints {
|
||||
constraint.activateIfNeeded(updatingExisting: false)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func remakeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
|
||||
self.removeConstraints(item: item)
|
||||
self.makeConstraints(item: item, closure: closure)
|
||||
}
|
||||
|
||||
internal static func updateConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
|
||||
guard item.constraints.count > 0 else {
|
||||
self.makeConstraints(item: item, closure: closure)
|
||||
return
|
||||
}
|
||||
|
||||
let constraints = prepareConstraints(item: item, closure: closure)
|
||||
for constraint in constraints {
|
||||
constraint.activateIfNeeded(updatingExisting: true)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func removeConstraints(item: LayoutConstraintItem) {
|
||||
let constraints = item.constraints
|
||||
for constraint in constraints {
|
||||
constraint.deactivateIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public class ConstraintMakerEditable: ConstraintMakerPrioritizable {
|
||||
|
||||
@discardableResult
|
||||
public func multipliedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
|
||||
self.description.multiplier = amount
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func dividedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
|
||||
return self.multipliedBy(1.0 / amount.constraintMultiplierTargetValue)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func offset(_ amount: ConstraintOffsetTarget) -> ConstraintMakerEditable {
|
||||
self.description.constant = amount.constraintOffsetTargetValue
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func inset(_ amount: ConstraintInsetTarget) -> ConstraintMakerEditable {
|
||||
self.description.constant = amount.constraintInsetTargetValue
|
||||
return self
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
@discardableResult
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
public func inset(_ amount: ConstraintDirectionalInsetTarget) -> ConstraintMakerEditable {
|
||||
self.description.constant = amount.constraintDirectionalInsetTargetValue
|
||||
return self
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public class ConstraintMakerExtendable: ConstraintMakerRelatable {
|
||||
|
||||
public var left: ConstraintMakerExtendable {
|
||||
self.description.attributes += .left
|
||||
return self
|
||||
}
|
||||
|
||||
public var top: ConstraintMakerExtendable {
|
||||
self.description.attributes += .top
|
||||
return self
|
||||
}
|
||||
|
||||
public var bottom: ConstraintMakerExtendable {
|
||||
self.description.attributes += .bottom
|
||||
return self
|
||||
}
|
||||
|
||||
public var right: ConstraintMakerExtendable {
|
||||
self.description.attributes += .right
|
||||
return self
|
||||
}
|
||||
|
||||
public var leading: ConstraintMakerExtendable {
|
||||
self.description.attributes += .leading
|
||||
return self
|
||||
}
|
||||
|
||||
public var trailing: ConstraintMakerExtendable {
|
||||
self.description.attributes += .trailing
|
||||
return self
|
||||
}
|
||||
|
||||
public var width: ConstraintMakerExtendable {
|
||||
self.description.attributes += .width
|
||||
return self
|
||||
}
|
||||
|
||||
public var height: ConstraintMakerExtendable {
|
||||
self.description.attributes += .height
|
||||
return self
|
||||
}
|
||||
|
||||
public var centerX: ConstraintMakerExtendable {
|
||||
self.description.attributes += .centerX
|
||||
return self
|
||||
}
|
||||
|
||||
public var centerY: ConstraintMakerExtendable {
|
||||
self.description.attributes += .centerY
|
||||
return self
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"lastBaseline")
|
||||
public var baseline: ConstraintMakerExtendable {
|
||||
self.description.attributes += .lastBaseline
|
||||
return self
|
||||
}
|
||||
|
||||
public var lastBaseline: ConstraintMakerExtendable {
|
||||
self.description.attributes += .lastBaseline
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
public var firstBaseline: ConstraintMakerExtendable {
|
||||
self.description.attributes += .firstBaseline
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var leftMargin: ConstraintMakerExtendable {
|
||||
self.description.attributes += .leftMargin
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var rightMargin: ConstraintMakerExtendable {
|
||||
self.description.attributes += .rightMargin
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var topMargin: ConstraintMakerExtendable {
|
||||
self.description.attributes += .topMargin
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var bottomMargin: ConstraintMakerExtendable {
|
||||
self.description.attributes += .bottomMargin
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var leadingMargin: ConstraintMakerExtendable {
|
||||
self.description.attributes += .leadingMargin
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var trailingMargin: ConstraintMakerExtendable {
|
||||
self.description.attributes += .trailingMargin
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerXWithinMargins: ConstraintMakerExtendable {
|
||||
self.description.attributes += .centerXWithinMargins
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerYWithinMargins: ConstraintMakerExtendable {
|
||||
self.description.attributes += .centerYWithinMargins
|
||||
return self
|
||||
}
|
||||
|
||||
public var edges: ConstraintMakerExtendable {
|
||||
self.description.attributes += .edges
|
||||
return self
|
||||
}
|
||||
public var horizontalEdges: ConstraintMakerExtendable {
|
||||
self.description.attributes += .horizontalEdges
|
||||
return self
|
||||
}
|
||||
public var verticalEdges: ConstraintMakerExtendable {
|
||||
self.description.attributes += .verticalEdges
|
||||
return self
|
||||
}
|
||||
public var directionalEdges: ConstraintMakerExtendable {
|
||||
self.description.attributes += .directionalEdges
|
||||
return self
|
||||
}
|
||||
public var directionalHorizontalEdges: ConstraintMakerExtendable {
|
||||
self.description.attributes += .directionalHorizontalEdges
|
||||
return self
|
||||
}
|
||||
public var directionalVerticalEdges: ConstraintMakerExtendable {
|
||||
self.description.attributes += .directionalVerticalEdges
|
||||
return self
|
||||
}
|
||||
public var size: ConstraintMakerExtendable {
|
||||
self.description.attributes += .size
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var margins: ConstraintMakerExtendable {
|
||||
self.description.attributes += .margins
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var directionalMargins: ConstraintMakerExtendable {
|
||||
self.description.attributes += .directionalMargins
|
||||
return self
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public var centerWithinMargins: ConstraintMakerExtendable {
|
||||
self.description.attributes += .centerWithinMargins
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public class ConstraintMakerFinalizable {
|
||||
|
||||
internal let description: ConstraintDescription
|
||||
|
||||
internal init(_ description: ConstraintDescription) {
|
||||
self.description = description
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func labeled(_ label: String) -> ConstraintMakerFinalizable {
|
||||
self.description.label = label
|
||||
return self
|
||||
}
|
||||
|
||||
public var constraint: Constraint {
|
||||
return self.description.constraint!
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
@available(*, deprecated, message:"Use ConstraintMakerPrioritizable instead.")
|
||||
public typealias ConstraintMakerPriortizable = ConstraintMakerPrioritizable
|
||||
|
||||
public class ConstraintMakerPrioritizable: ConstraintMakerFinalizable {
|
||||
|
||||
@discardableResult
|
||||
public func priority(_ amount: ConstraintPriority) -> ConstraintMakerFinalizable {
|
||||
self.description.priority = amount.value
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func priority(_ amount: ConstraintPriorityTarget) -> ConstraintMakerFinalizable {
|
||||
self.description.priority = amount
|
||||
return self
|
||||
}
|
||||
|
||||
@available(*, deprecated, message:"Use priority(.required) instead.")
|
||||
@discardableResult
|
||||
public func priorityRequired() -> ConstraintMakerFinalizable {
|
||||
return self.priority(.required)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message:"Use priority(.high) instead.")
|
||||
@discardableResult
|
||||
public func priorityHigh() -> ConstraintMakerFinalizable {
|
||||
return self.priority(.high)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message:"Use priority(.medium) instead.")
|
||||
@discardableResult
|
||||
public func priorityMedium() -> ConstraintMakerFinalizable {
|
||||
return self.priority(.medium)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message:"Use priority(.low) instead.")
|
||||
@discardableResult
|
||||
public func priorityLow() -> ConstraintMakerFinalizable {
|
||||
return self.priority(.low)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
extension ConstraintMakerRelatable {
|
||||
|
||||
@discardableResult
|
||||
public func equalToSuperview<T: ConstraintRelatableTarget>(_ closure: (ConstraintView) -> T, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable {
|
||||
guard let other = self.description.item.superview else {
|
||||
fatalError("Expected superview but found nil when attempting make constraint `equalToSuperview`.")
|
||||
}
|
||||
return self.relatedTo(closure(other), relation: .equal, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func lessThanOrEqualToSuperview<T: ConstraintRelatableTarget>(_ closure: (ConstraintView) -> T, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable {
|
||||
guard let other = self.description.item.superview else {
|
||||
fatalError("Expected superview but found nil when attempting make constraint `lessThanOrEqualToSuperview`.")
|
||||
}
|
||||
return self.relatedTo(closure(other), relation: .lessThanOrEqual, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func greaterThanOrEqualToSuperview<T: ConstraintRelatableTarget>(_ closure: (ConstraintView) -> T, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable {
|
||||
guard let other = self.description.item.superview else {
|
||||
fatalError("Expected superview but found nil when attempting make constraint `greaterThanOrEqualToSuperview`.")
|
||||
}
|
||||
return self.relatedTo(closure(other), relation: .greaterThanOrEqual, file: file, line: line)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public class ConstraintMakerRelatable {
|
||||
|
||||
internal let description: ConstraintDescription
|
||||
|
||||
internal init(_ description: ConstraintDescription) {
|
||||
self.description = description
|
||||
}
|
||||
|
||||
internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
|
||||
let related: ConstraintItem
|
||||
let constant: ConstraintConstantTarget
|
||||
|
||||
if let other = other as? ConstraintItem {
|
||||
guard other.attributes == ConstraintAttributes.none ||
|
||||
other.attributes.layoutAttributes.count <= 1 ||
|
||||
other.attributes.layoutAttributes == self.description.attributes.layoutAttributes ||
|
||||
other.attributes == .edges && self.description.attributes == .margins ||
|
||||
other.attributes == .margins && self.description.attributes == .edges ||
|
||||
other.attributes == .directionalEdges && self.description.attributes == .directionalMargins ||
|
||||
other.attributes == .directionalMargins && self.description.attributes == .directionalEdges else {
|
||||
fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))");
|
||||
}
|
||||
|
||||
related = other
|
||||
constant = 0.0
|
||||
} else if let other = other as? ConstraintView {
|
||||
related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
|
||||
constant = 0.0
|
||||
} else if let other = other as? ConstraintConstantTarget {
|
||||
related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none)
|
||||
constant = other
|
||||
} else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide {
|
||||
related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
|
||||
constant = 0.0
|
||||
} else {
|
||||
fatalError("Invalid constraint. (\(file), \(line))")
|
||||
}
|
||||
|
||||
let editable = ConstraintMakerEditable(self.description)
|
||||
editable.description.sourceLocation = (file, line)
|
||||
editable.description.relation = relation
|
||||
editable.description.related = related
|
||||
editable.description.constant = constant
|
||||
return editable
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
|
||||
return self.relatedTo(other, relation: .equal, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func equalToSuperview(_ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
|
||||
guard let other = self.description.item.superview else {
|
||||
fatalError("Expected superview but found nil when attempting make constraint `equalToSuperview`.")
|
||||
}
|
||||
return self.relatedTo(other, relation: .equal, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func lessThanOrEqualTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
|
||||
return self.relatedTo(other, relation: .lessThanOrEqual, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func lessThanOrEqualToSuperview(_ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
|
||||
guard let other = self.description.item.superview else {
|
||||
fatalError("Expected superview but found nil when attempting make constraint `lessThanOrEqualToSuperview`.")
|
||||
}
|
||||
return self.relatedTo(other, relation: .lessThanOrEqual, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func greaterThanOrEqualTo(_ other: ConstraintRelatableTarget, _ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable {
|
||||
return self.relatedTo(other, relation: .greaterThanOrEqual, file: file, line: line)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func greaterThanOrEqualToSuperview(_ file: String = #file, line: UInt = #line) -> ConstraintMakerEditable {
|
||||
guard let other = self.description.item.superview else {
|
||||
fatalError("Expected superview but found nil when attempting make constraint `greaterThanOrEqualToSuperview`.")
|
||||
}
|
||||
return self.relatedTo(other, relation: .greaterThanOrEqual, file: file, line: line)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintMultiplierTarget {
|
||||
|
||||
var constraintMultiplierTargetValue: CGFloat { get }
|
||||
|
||||
}
|
||||
|
||||
extension Int: ConstraintMultiplierTarget {
|
||||
|
||||
public var constraintMultiplierTargetValue: CGFloat {
|
||||
return CGFloat(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UInt: ConstraintMultiplierTarget {
|
||||
|
||||
public var constraintMultiplierTargetValue: CGFloat {
|
||||
return CGFloat(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Float: ConstraintMultiplierTarget {
|
||||
|
||||
public var constraintMultiplierTargetValue: CGFloat {
|
||||
return CGFloat(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Double: ConstraintMultiplierTarget {
|
||||
|
||||
public var constraintMultiplierTargetValue: CGFloat {
|
||||
return CGFloat(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CGFloat: ConstraintMultiplierTarget {
|
||||
|
||||
public var constraintMultiplierTargetValue: CGFloat {
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintOffsetTarget: ConstraintConstantTarget {
|
||||
}
|
||||
|
||||
extension Int: ConstraintOffsetTarget {
|
||||
}
|
||||
|
||||
extension UInt: ConstraintOffsetTarget {
|
||||
}
|
||||
|
||||
extension Float: ConstraintOffsetTarget {
|
||||
}
|
||||
|
||||
extension Double: ConstraintOffsetTarget {
|
||||
}
|
||||
|
||||
extension CGFloat: ConstraintOffsetTarget {
|
||||
}
|
||||
|
||||
extension ConstraintOffsetTarget {
|
||||
|
||||
internal var constraintOffsetTargetValue: CGFloat {
|
||||
let offset: CGFloat
|
||||
if let amount = self as? Float {
|
||||
offset = CGFloat(amount)
|
||||
} else if let amount = self as? Double {
|
||||
offset = CGFloat(amount)
|
||||
} else if let amount = self as? CGFloat {
|
||||
offset = CGFloat(amount)
|
||||
} else if let amount = self as? Int {
|
||||
offset = CGFloat(amount)
|
||||
} else if let amount = self as? UInt {
|
||||
offset = CGFloat(amount)
|
||||
} else {
|
||||
offset = 0.0
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public struct ConstraintPriority : ExpressibleByFloatLiteral, Equatable, Strideable {
|
||||
public typealias FloatLiteralType = Float
|
||||
|
||||
public let value: Float
|
||||
|
||||
public init(floatLiteral value: Float) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public init(_ value: Float) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public static var required: ConstraintPriority {
|
||||
return 1000.0
|
||||
}
|
||||
|
||||
public static var high: ConstraintPriority {
|
||||
return 750.0
|
||||
}
|
||||
|
||||
public static var medium: ConstraintPriority {
|
||||
#if os(OSX)
|
||||
return 501.0
|
||||
#else
|
||||
return 500.0
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public static var low: ConstraintPriority {
|
||||
return 250.0
|
||||
}
|
||||
|
||||
public static func ==(lhs: ConstraintPriority, rhs: ConstraintPriority) -> Bool {
|
||||
return lhs.value == rhs.value
|
||||
}
|
||||
|
||||
// MARK: Strideable
|
||||
|
||||
public func advanced(by n: FloatLiteralType) -> ConstraintPriority {
|
||||
return ConstraintPriority(floatLiteral: value + n)
|
||||
}
|
||||
|
||||
public func distance(to other: ConstraintPriority) -> FloatLiteralType {
|
||||
return other.value - value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintPriorityTarget {
|
||||
|
||||
var constraintPriorityTargetValue: Float { get }
|
||||
|
||||
}
|
||||
|
||||
extension Int: ConstraintPriorityTarget {
|
||||
|
||||
public var constraintPriorityTargetValue: Float {
|
||||
return Float(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UInt: ConstraintPriorityTarget {
|
||||
|
||||
public var constraintPriorityTargetValue: Float {
|
||||
return Float(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Float: ConstraintPriorityTarget {
|
||||
|
||||
public var constraintPriorityTargetValue: Float {
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Double: ConstraintPriorityTarget {
|
||||
|
||||
public var constraintPriorityTargetValue: Float {
|
||||
return Float(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CGFloat: ConstraintPriorityTarget {
|
||||
|
||||
public var constraintPriorityTargetValue: Float {
|
||||
return Float(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
extension UILayoutPriority: ConstraintPriorityTarget {
|
||||
|
||||
public var constraintPriorityTargetValue: Float {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension Int: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension UInt: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension Float: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension Double: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension CGFloat: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension CGSize: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension CGPoint: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension ConstraintInsets: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
extension ConstraintDirectionalInsets: ConstraintRelatableTarget {
|
||||
}
|
||||
#endif
|
||||
|
||||
extension ConstraintItem: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
extension ConstraintView: ConstraintRelatableTarget {
|
||||
}
|
||||
|
||||
@available(iOS 9.0, OSX 10.11, *)
|
||||
extension ConstraintLayoutGuide: ConstraintRelatableTarget {
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
internal enum ConstraintRelation : Int {
|
||||
case equal = 1
|
||||
case lessThanOrEqual
|
||||
case greaterThanOrEqual
|
||||
|
||||
internal var layoutRelation: LayoutRelation {
|
||||
get {
|
||||
switch(self) {
|
||||
case .equal:
|
||||
return .equal
|
||||
case .lessThanOrEqual:
|
||||
return .lessThanOrEqual
|
||||
case .greaterThanOrEqual:
|
||||
return .greaterThanOrEqual
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public extension ConstraintView {
|
||||
|
||||
@available(*, deprecated, renamed:"snp.left")
|
||||
var snp_left: ConstraintItem { return self.snp.left }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.top")
|
||||
var snp_top: ConstraintItem { return self.snp.top }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.right")
|
||||
var snp_right: ConstraintItem { return self.snp.right }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.bottom")
|
||||
var snp_bottom: ConstraintItem { return self.snp.bottom }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.leading")
|
||||
var snp_leading: ConstraintItem { return self.snp.leading }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.trailing")
|
||||
var snp_trailing: ConstraintItem { return self.snp.trailing }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.width")
|
||||
var snp_width: ConstraintItem { return self.snp.width }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.height")
|
||||
var snp_height: ConstraintItem { return self.snp.height }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.centerX")
|
||||
var snp_centerX: ConstraintItem { return self.snp.centerX }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.centerY")
|
||||
var snp_centerY: ConstraintItem { return self.snp.centerY }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.baseline")
|
||||
var snp_baseline: ConstraintItem { return self.snp.baseline }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.lastBaseline")
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
var snp_lastBaseline: ConstraintItem { return self.snp.lastBaseline }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.firstBaseline")
|
||||
@available(iOS 8.0, OSX 10.11, *)
|
||||
var snp_firstBaseline: ConstraintItem { return self.snp.firstBaseline }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.leftMargin")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_leftMargin: ConstraintItem { return self.snp.leftMargin }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.topMargin")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_topMargin: ConstraintItem { return self.snp.topMargin }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.rightMargin")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_rightMargin: ConstraintItem { return self.snp.rightMargin }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.bottomMargin")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_bottomMargin: ConstraintItem { return self.snp.bottomMargin }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.leadingMargin")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_leadingMargin: ConstraintItem { return self.snp.leadingMargin }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.trailingMargin")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_trailingMargin: ConstraintItem { return self.snp.trailingMargin }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.centerXWithinMargins")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_centerXWithinMargins: ConstraintItem { return self.snp.centerXWithinMargins }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.centerYWithinMargins")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_centerYWithinMargins: ConstraintItem { return self.snp.centerYWithinMargins }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.edges")
|
||||
var snp_edges: ConstraintItem { return self.snp.edges }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.size")
|
||||
var snp_size: ConstraintItem { return self.snp.size }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.center")
|
||||
var snp_center: ConstraintItem { return self.snp.center }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.margins")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_margins: ConstraintItem { return self.snp.margins }
|
||||
|
||||
@available(iOS, deprecated, renamed:"snp.centerWithinMargins")
|
||||
@available(iOS 8.0, *)
|
||||
var snp_centerWithinMargins: ConstraintItem { return self.snp.centerWithinMargins }
|
||||
|
||||
@available(*, deprecated, renamed:"snp.prepareConstraints(_:)")
|
||||
func snp_prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
|
||||
return self.snp.prepareConstraints(closure)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"snp.makeConstraints(_:)")
|
||||
func snp_makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
self.snp.makeConstraints(closure)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"snp.remakeConstraints(_:)")
|
||||
func snp_remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
self.snp.remakeConstraints(closure)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"snp.updateConstraints(_:)")
|
||||
func snp_updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
self.snp.updateConstraints(closure)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed:"snp.removeConstraints()")
|
||||
func snp_removeConstraints() {
|
||||
self.snp.removeConstraints()
|
||||
}
|
||||
|
||||
var snp: ConstraintViewDSL {
|
||||
return ConstraintViewDSL(view: self)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
#if canImport(UIKit)
|
||||
public typealias ConstraintView = UIView
|
||||
#else
|
||||
public typealias ConstraintView = NSView
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public struct ConstraintViewDSL: ConstraintAttributesDSL {
|
||||
|
||||
@discardableResult
|
||||
public func prepareConstraints(_ closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
|
||||
return ConstraintMaker.prepareConstraints(item: self.view, closure: closure)
|
||||
}
|
||||
|
||||
public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
ConstraintMaker.makeConstraints(item: self.view, closure: closure)
|
||||
}
|
||||
|
||||
public func remakeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
ConstraintMaker.remakeConstraints(item: self.view, closure: closure)
|
||||
}
|
||||
|
||||
public func updateConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
|
||||
ConstraintMaker.updateConstraints(item: self.view, closure: closure)
|
||||
}
|
||||
|
||||
public func removeConstraints() {
|
||||
ConstraintMaker.removeConstraints(item: self.view)
|
||||
}
|
||||
|
||||
public var contentHuggingHorizontalPriority: Float {
|
||||
get {
|
||||
return self.view.contentHuggingPriority(for: .horizontal).rawValue
|
||||
}
|
||||
nonmutating set {
|
||||
self.view.setContentHuggingPriority(LayoutPriority(rawValue: newValue), for: .horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
public var contentHuggingVerticalPriority: Float {
|
||||
get {
|
||||
return self.view.contentHuggingPriority(for: .vertical).rawValue
|
||||
}
|
||||
nonmutating set {
|
||||
self.view.setContentHuggingPriority(LayoutPriority(rawValue: newValue), for: .vertical)
|
||||
}
|
||||
}
|
||||
|
||||
public var contentCompressionResistanceHorizontalPriority: Float {
|
||||
get {
|
||||
return self.view.contentCompressionResistancePriority(for: .horizontal).rawValue
|
||||
}
|
||||
nonmutating set {
|
||||
self.view.setContentCompressionResistancePriority(LayoutPriority(rawValue: newValue), for: .horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
public var contentCompressionResistanceVerticalPriority: Float {
|
||||
get {
|
||||
return self.view.contentCompressionResistancePriority(for: .vertical).rawValue
|
||||
}
|
||||
nonmutating set {
|
||||
self.view.setContentCompressionResistancePriority(LayoutPriority(rawValue: newValue), for: .vertical)
|
||||
}
|
||||
}
|
||||
|
||||
public var target: AnyObject? {
|
||||
return self.view
|
||||
}
|
||||
|
||||
internal let view: ConstraintView
|
||||
|
||||
internal init(view: ConstraintView) {
|
||||
self.view = view
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public extension LayoutConstraint {
|
||||
|
||||
override var description: String {
|
||||
var description = "<"
|
||||
|
||||
description += descriptionForObject(self)
|
||||
|
||||
if let firstItem = conditionalOptional(from: self.firstItem) {
|
||||
description += " \(descriptionForObject(firstItem))"
|
||||
}
|
||||
|
||||
if self.firstAttribute != .notAnAttribute {
|
||||
description += ".\(descriptionForAttribute(self.firstAttribute))"
|
||||
}
|
||||
|
||||
description += " \(descriptionForRelation(self.relation))"
|
||||
|
||||
if let secondItem = self.secondItem {
|
||||
description += " \(descriptionForObject(secondItem))"
|
||||
}
|
||||
|
||||
if self.secondAttribute != .notAnAttribute {
|
||||
description += ".\(descriptionForAttribute(self.secondAttribute))"
|
||||
}
|
||||
|
||||
if self.multiplier != 1.0 {
|
||||
description += " * \(self.multiplier)"
|
||||
}
|
||||
|
||||
if self.secondAttribute == .notAnAttribute {
|
||||
description += " \(self.constant)"
|
||||
} else {
|
||||
if self.constant > 0.0 {
|
||||
description += " + \(self.constant)"
|
||||
} else if self.constant < 0.0 {
|
||||
description += " - \(abs(self.constant))"
|
||||
}
|
||||
}
|
||||
|
||||
if self.priority.rawValue != 1000.0 {
|
||||
description += " ^\(self.priority)"
|
||||
}
|
||||
|
||||
description += ">"
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func descriptionForRelation(_ relation: LayoutRelation) -> String {
|
||||
switch relation {
|
||||
case .equal: return "=="
|
||||
case .greaterThanOrEqual: return ">="
|
||||
case .lessThanOrEqual: return "<="
|
||||
#if swift(>=5.0)
|
||||
@unknown default: return "unknown"
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func descriptionForAttribute(_ attribute: LayoutAttribute) -> String {
|
||||
#if canImport(UIKit)
|
||||
switch attribute {
|
||||
case .notAnAttribute: return "notAnAttribute"
|
||||
case .top: return "top"
|
||||
case .left: return "left"
|
||||
case .bottom: return "bottom"
|
||||
case .right: return "right"
|
||||
case .leading: return "leading"
|
||||
case .trailing: return "trailing"
|
||||
case .width: return "width"
|
||||
case .height: return "height"
|
||||
case .centerX: return "centerX"
|
||||
case .centerY: return "centerY"
|
||||
case .lastBaseline: return "lastBaseline"
|
||||
case .firstBaseline: return "firstBaseline"
|
||||
case .topMargin: return "topMargin"
|
||||
case .leftMargin: return "leftMargin"
|
||||
case .bottomMargin: return "bottomMargin"
|
||||
case .rightMargin: return "rightMargin"
|
||||
case .leadingMargin: return "leadingMargin"
|
||||
case .trailingMargin: return "trailingMargin"
|
||||
case .centerXWithinMargins: return "centerXWithinMargins"
|
||||
case .centerYWithinMargins: return "centerYWithinMargins"
|
||||
#if swift(>=5.0)
|
||||
@unknown default: return "unknown"
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
switch attribute {
|
||||
case .notAnAttribute: return "notAnAttribute"
|
||||
case .top: return "top"
|
||||
case .left: return "left"
|
||||
case .bottom: return "bottom"
|
||||
case .right: return "right"
|
||||
case .leading: return "leading"
|
||||
case .trailing: return "trailing"
|
||||
case .width: return "width"
|
||||
case .height: return "height"
|
||||
case .centerX: return "centerX"
|
||||
case .centerY: return "centerY"
|
||||
case .lastBaseline: return "lastBaseline"
|
||||
case .firstBaseline: return "firstBaseline"
|
||||
#if swift(>=5.0)
|
||||
@unknown default: return "unknown"
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func conditionalOptional<T>(from object: Optional<T>) -> Optional<T> {
|
||||
return object
|
||||
}
|
||||
|
||||
private func conditionalOptional<T>(from object: T) -> Optional<T> {
|
||||
return Optional.some(object)
|
||||
}
|
||||
|
||||
private func descriptionForObject(_ object: AnyObject) -> String {
|
||||
let pointerDescription = String(format: "%p", UInt(bitPattern: ObjectIdentifier(object)))
|
||||
var desc = ""
|
||||
|
||||
desc += type(of: object).description()
|
||||
|
||||
if let object = object as? ConstraintView {
|
||||
desc += ":\(object.snp.label() ?? pointerDescription)"
|
||||
} else if let object = object as? LayoutConstraint {
|
||||
desc += ":\(object.label ?? pointerDescription)"
|
||||
} else {
|
||||
desc += ":\(pointerDescription)"
|
||||
}
|
||||
|
||||
if let object = object as? LayoutConstraint, let file = object.constraint?.sourceLocation.0, let line = object.constraint?.sourceLocation.1 {
|
||||
desc += "@\((file as NSString).lastPathComponent)#\(line)"
|
||||
}
|
||||
|
||||
desc += ""
|
||||
return desc
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public class LayoutConstraint : NSLayoutConstraint {
|
||||
|
||||
public var label: String? {
|
||||
get {
|
||||
return self.identifier
|
||||
}
|
||||
set {
|
||||
self.identifier = newValue
|
||||
}
|
||||
}
|
||||
|
||||
internal weak var constraint: Constraint? = nil
|
||||
|
||||
}
|
||||
|
||||
internal func ==(lhs: LayoutConstraint, rhs: LayoutConstraint) -> Bool {
|
||||
// If firstItem or secondItem on either constraint has a dangling pointer
|
||||
// this comparison can cause a crash. The solution for this is to ensure
|
||||
// your layout code hold strong references to things like Views, LayoutGuides
|
||||
// and LayoutAnchors as SnapKit will not keep strong references to any of these.
|
||||
guard lhs.firstAttribute == rhs.firstAttribute &&
|
||||
lhs.secondAttribute == rhs.secondAttribute &&
|
||||
lhs.relation == rhs.relation &&
|
||||
lhs.priority == rhs.priority &&
|
||||
lhs.multiplier == rhs.multiplier &&
|
||||
lhs.secondItem === rhs.secondItem &&
|
||||
lhs.firstItem === rhs.firstItem else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
|
||||
public protocol LayoutConstraintItem: AnyObject {
|
||||
}
|
||||
|
||||
@available(iOS 9.0, OSX 10.11, *)
|
||||
extension ConstraintLayoutGuide : LayoutConstraintItem {
|
||||
}
|
||||
|
||||
extension ConstraintView : LayoutConstraintItem {
|
||||
}
|
||||
|
||||
|
||||
extension LayoutConstraintItem {
|
||||
|
||||
internal func prepare() {
|
||||
if let view = self as? ConstraintView {
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
}
|
||||
|
||||
internal var superview: ConstraintView? {
|
||||
if let view = self as? ConstraintView {
|
||||
return view.superview
|
||||
}
|
||||
|
||||
if #available(iOS 9.0, OSX 10.11, *), let guide = self as? ConstraintLayoutGuide {
|
||||
return guide.owningView
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
internal var constraints: [Constraint] {
|
||||
return self.constraintsSet.allObjects as! [Constraint]
|
||||
}
|
||||
|
||||
internal func add(constraints: [Constraint]) {
|
||||
let constraintsSet = self.constraintsSet
|
||||
for constraint in constraints {
|
||||
constraintsSet.add(constraint)
|
||||
}
|
||||
}
|
||||
|
||||
internal func remove(constraints: [Constraint]) {
|
||||
let constraintsSet = self.constraintsSet
|
||||
for constraint in constraints {
|
||||
constraintsSet.remove(constraint)
|
||||
}
|
||||
}
|
||||
|
||||
private var constraintsSet: NSMutableSet {
|
||||
let constraintsSet: NSMutableSet
|
||||
|
||||
if let existing = objc_getAssociatedObject(self, &constraintsKey) as? NSMutableSet {
|
||||
constraintsSet = existing
|
||||
} else {
|
||||
constraintsSet = NSMutableSet()
|
||||
objc_setAssociatedObject(self, &constraintsKey, constraintsSet, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
return constraintsSet
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
private var constraintsKey: UInt8 = 0
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#if swift(>=4.2)
|
||||
typealias LayoutRelation = NSLayoutConstraint.Relation
|
||||
typealias LayoutAttribute = NSLayoutConstraint.Attribute
|
||||
#else
|
||||
typealias LayoutRelation = NSLayoutRelation
|
||||
typealias LayoutAttribute = NSLayoutAttribute
|
||||
#endif
|
||||
typealias LayoutPriority = UILayoutPriority
|
||||
#else
|
||||
import AppKit
|
||||
typealias LayoutRelation = NSLayoutConstraint.Relation
|
||||
typealias LayoutAttribute = NSLayoutConstraint.Attribute
|
||||
typealias LayoutPriority = NSLayoutConstraint.Priority
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// SnapKit
|
||||
//
|
||||
// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
public extension ConstraintLayoutSupport {
|
||||
|
||||
var snp: ConstraintLayoutSupportDSL {
|
||||
return ConstraintLayoutSupportDSL(support: self)
|
||||
}
|
||||
|
||||
}
|
||||
ReadViewDemo/Pods/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-acknowledgements.markdown
Generated
+23
@@ -53,6 +53,29 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
## SnapKit
|
||||
|
||||
Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
## ZIPFoundation
|
||||
|
||||
MIT License
|
||||
|
||||
Generated
+29
@@ -76,6 +76,35 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT</string>
|
||||
<key>Title</key>
|
||||
<string>SnapKit</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>MIT License
|
||||
|
||||
+5
-1
@@ -1,5 +1,9 @@
|
||||
${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh
|
||||
${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework
|
||||
${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework
|
||||
${BUILT_PRODUCTS_DIR}/RDReaderView/RDReaderView.framework
|
||||
${BUILT_PRODUCTS_DIR}/RDAIReaderView/RDAIReaderView.framework
|
||||
${BUILT_PRODUCTS_DIR}/RDEpubReaderView/RDEpubReaderView.framework
|
||||
${BUILT_PRODUCTS_DIR}/RDPDFReaderView/RDPDFReaderView.framework
|
||||
${BUILT_PRODUCTS_DIR}/RDSpeechReaderView/RDSpeechReaderView.framework
|
||||
${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework
|
||||
${BUILT_PRODUCTS_DIR}/ZIPFoundation/ZIPFoundation.framework
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user