create-dwy 0.14.6 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/bin/index.js +4 -0
- package/package.json +1 -1
- package/src/skills-install.js +113 -0
- package/src/sync-all.js +47 -3
- package/templates/ai-tools/CLAUDE.md +18 -1
- package/templates/ai-tools/hooks/Git/pre-git-commit-sensitive-check.sh +10 -2
- package/templates/ai-tools/rules/Android/dwy-android-core.md +8 -0
- package/templates/ai-tools/rules/Flutter/dwy-flutter-core.md +8 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-strict.md +221 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +193 -0
- package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +159 -0
- package/templates/ai-tools/rules/iOS/dwy-swift-core.md +35 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +23 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +28 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +22 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-ideate/SKILL.md +21 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +23 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-poc/SKILL.md +24 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +22 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +68 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +103 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +23 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +26 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-design/SKILL.md +23 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +22 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-launch/SKILL.md +25 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-prd/SKILL.md +22 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-ship/SKILL.md +21 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +22 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +25 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +28 -0
- package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +23 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/SKILL.md +365 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/references/electron-release-workflow-template.yml +98 -0
- package/templates/ai-tools/skills//{350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel → 345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish}/references/release-workflow-template.yml +3 -3
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +31 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +31 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +23 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +37 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +23 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +27 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +25 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +32 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +36 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/publish-config.md +65 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/version-sync.md +30 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/scripts/release_config.sh +115 -0
- package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/SKILL.md +31 -21
- package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/python-commercial-guide.md +1 -1
- package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/sensitive-patterns.md +1 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/init_config.sh +91 -0
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-whiteboard-prototype/SKILL.md +70 -0
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-whiteboard-prototype/assets/template.html +353 -0
- package/templates/ai-tools/rules/Global/dwy-common-code-baseline.md +0 -27
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/init_config.sh +0 -97
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-publish/SKILL.md +0 -50
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-publish/references/application-release.md +0 -106
- package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-publish/references/sdk-release.md +0 -93
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel/SKILL.md +0 -356
- /package/templates/ai-tools/rules/{Global → Docker}/dwy-docker.md +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/files-whitelist-snippet.json" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/js-commercial.npmignore" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/python-commercial-MANIFEST.in" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/assets/python-commercial.gitignore" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/js-commercial-guide.md" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/references/publish-blacklist.md" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/check_comments.py" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/check_js_publish.sh" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/check_python_publish.sh" +0 -0
- /package/templates/ai-tools/skills//345/{256/211/345/205/250 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-sdk-spec/scripts/detect_sdk.sh" +0 -0
- /package/templates/ai-tools/skills//345/{274/200/345/217/221/346/265/201/347/250/213 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-semver/SKILL.md" +0 -0
- /package/templates/ai-tools/skills//345/{274/200/345/217/221/346/265/201/347/250/213 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-semver/references/semver-spec.md" +0 -0
- /package/templates/ai-tools/skills//345/{274/200/345/217/221/346/265/201/347/250/213 → 217/221/345/270/203/345/217/221/347/211/210}/dwy-semver/scripts/bump.py" +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: HarmonyOS ArkUI 组件层 / V2 状态管理 / 图标资源 / 工程构建 / 运行时容错避坑(与 dwy-arkts-strict.md 语言层配套)
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.ets"
|
|
5
|
+
- "**/build-profile.json5"
|
|
6
|
+
- "**/main_pages.json"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# ArkUI / 工程 / 运行时 避坑
|
|
10
|
+
|
|
11
|
+
`dwy-arkts-strict.md` 管语言语法层;本文件管 **ArkUI 组件、V2 状态管理、资源(图标)、工程构建、运行时容错**。
|
|
12
|
+
均为真机/DevEco 构建实测踩坑(编译期或运行期暴露,命令行 tsc 查不出)。
|
|
13
|
+
|
|
14
|
+
## 1. sys.symbol 图标名 ≠ SF Symbols 名,必须查鸿蒙清单逐个映射
|
|
15
|
+
|
|
16
|
+
iOS 用 SF Symbols(`tv.and.mediabox`),鸿蒙 `$r('sys.symbol.xxx')` 是**另一套命名体系**,
|
|
17
|
+
大量 SF 名在鸿蒙不存在,写错报 `Unknown resource name 'xxx'`。
|
|
18
|
+
|
|
19
|
+
- **权威清单**:`<DevEco-SDK>/openharmony/toolchains/id_defined.json`(约 4000+ 个,`type=symbol`),
|
|
20
|
+
python 提取后 grep 验证目标名是否存在。
|
|
21
|
+
- **`$r('sys.symbol.x')` 的 x 必须是字面量**,不能用变量动态拼 → 只能逐处改,不能写映射函数。
|
|
22
|
+
- **常见映射**(SF → 鸿蒙):
|
|
23
|
+
|
|
24
|
+
| SF Symbols | 鸿蒙 sys.symbol |
|
|
25
|
+
|---|---|
|
|
26
|
+
| tv / tv.and.mediabox | `TV_tv` |
|
|
27
|
+
| iphone / ipad | `phone` / `pad` |
|
|
28
|
+
| command | `keyboard` |
|
|
29
|
+
| cpu / memorychip | `memory_module` |
|
|
30
|
+
| terminal | `code` |
|
|
31
|
+
| pencil | `doc_plaintext_and_pencil` |
|
|
32
|
+
| globe | `globe_and_keyboard` |
|
|
33
|
+
| tag | `label` |
|
|
34
|
+
| square.and.arrow.up | `share` |
|
|
35
|
+
| checkmark.seal.fill | `checkmark_circle_fill` |
|
|
36
|
+
| stop.fill | `stop_circle_fill` |
|
|
37
|
+
| shippingbox | `card_package` |
|
|
38
|
+
| square.stack.3d.up | `AI_square_stack` |
|
|
39
|
+
| gearshape.fill / house / chevron.left | 同名(`gearshape_fill` / `house` / `chevron_left`,少数直通) |
|
|
40
|
+
|
|
41
|
+
写前先 `grep -ix '<候选名>' id_defined.txt` 确认存在,别想当然。
|
|
42
|
+
|
|
43
|
+
## 2. 自定义组件后不能直接链通用属性(margin/layoutWeight/width…)
|
|
44
|
+
|
|
45
|
+
自定义 `@Component`/`@ComponentV2`(尤其带尾随 `@BuilderParam` 闭包的)**后面直接链通用属性
|
|
46
|
+
会编译失败**:`Declaration or statement expected` + `Cannot find name 'margin'`。
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// 错:HoldRepeatButton 是自定义组件,后链 .margin 解析失败
|
|
50
|
+
HoldRepeatButton({ action: f }) { this.icon() }
|
|
51
|
+
.margin({ bottom: 184 })
|
|
52
|
+
|
|
53
|
+
// 对:外层包系统组件(Column/Row/Stack)再链
|
|
54
|
+
Column() {
|
|
55
|
+
HoldRepeatButton({ action: f }) { this.icon() }
|
|
56
|
+
}.margin({ bottom: 184 })
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
需要偏移/尺寸时,要么外层包系统容器,要么给自定义组件加 `@Param` 参数在其内部应用。
|
|
60
|
+
|
|
61
|
+
## 3. 支持本地初始化的组件属性必须有默认值(mandatory-default-value-for-local-initialization)
|
|
62
|
+
|
|
63
|
+
DevEco **预览器/linter(PreviewBuild)比 assembleHap 更严**,会查这条;assembleHap 不查。
|
|
64
|
+
|
|
65
|
+
- **`@BuilderParam`** 必须给默认值(默认值是一个全局 `@Builder` 函数):
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
@Builder function emptyContent(): void {}
|
|
69
|
+
@ComponentV2
|
|
70
|
+
struct Foo {
|
|
71
|
+
@BuilderParam content: () => void = emptyContent; // 不能写 `: () => void;`
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **`@CustomDialog` 的 `controller`** 必须可选(否则同报无默认值):
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
@CustomDialog
|
|
79
|
+
struct MyDialog {
|
|
80
|
+
controller?: CustomDialogController; // 不能写 `controller: CustomDialogController;`
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
改可选后调用处要用可选链:`this.controller?.close()`(否则 `Object is possibly undefined`)。
|
|
84
|
+
|
|
85
|
+
- **例外**:`@Require @Param x: T;` 不受此规则约束(`@Require` 表必传,无需默认值)。
|
|
86
|
+
|
|
87
|
+
## 4. @ComponentV2 外部传入的回调属性必须用 @Param
|
|
88
|
+
|
|
89
|
+
父组件给子组件传属性(含回调)时,子组件该属性必须 `@Param` 装饰,否则报
|
|
90
|
+
`The 'regular' property 'xxx' cannot be initialized here (forbidden to specify)`:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// 错:onDone 是普通属性,父层 Foo({ onDone: ... }) 传不进
|
|
94
|
+
struct Foo { onDone: () => void = () => {}; }
|
|
95
|
+
// 对
|
|
96
|
+
struct Foo { @Param onDone: () => void = () => {}; }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 5. async 状态供 UI 同步读:用 @Trace 快照,别在 build 里 await
|
|
100
|
+
|
|
101
|
+
`build()` 是同步的,UI 不能 `await` 一个 async 方法拿状态(`Property 'kind' does not exist on
|
|
102
|
+
type 'Promise<T>'`)。把异步数据在装配/事件时算好,存进 `@Trace` 快照,UI 同步读快照:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
@ObservedV2 class Store {
|
|
106
|
+
@Trace snapshot: State = defaultState; // UI 读这个
|
|
107
|
+
private cache = 0;
|
|
108
|
+
async start(ctx): Promise<void> {
|
|
109
|
+
this.cache = await this.loadFromAsset(ctx); // 异步读一次缓存
|
|
110
|
+
this.refresh(); // 用缓存同步算快照
|
|
111
|
+
}
|
|
112
|
+
private refresh(): void { this.snapshot = compute(this.cache, Date.now()); }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 6. 跨模块 import 必须从「声明源」取,不能经中转模块隐式重导出
|
|
117
|
+
|
|
118
|
+
ArkTS 不支持 TS 的「A 从 B import 某类型,C 再从 A import 同名」隐式重导出:
|
|
119
|
+
`Module '"../X"' declares 'Y' locally, but it is not exported`。**直接从声明 Y 的那个文件 import**。
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// 错:AdbTransport 只是 import 了 AdbLog(没 re-export),别处从 AdbTransport 取 AdbLog
|
|
123
|
+
import { AdbLog } from '../adb/AdbTransport';
|
|
124
|
+
// 对:从声明 AdbLog 的源文件取
|
|
125
|
+
import { AdbLog } from '../adb/AdbConnection';
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 7. 工程/资源杂项
|
|
129
|
+
|
|
130
|
+
- **`Stack` 居中用 `.alignContent(Alignment.X)`**,不是构造参数 `Stack({ alignment: X })`
|
|
131
|
+
(`'alignment' does not exist in type 'StackOptions'`)。
|
|
132
|
+
- **`SymbolGlyph` 没有 `.textAlign`**:居中靠外层容器的 `justifyContent`/`alignItems`,别链 textAlign。
|
|
133
|
+
- **`build-profile.json5`(entry 模块)顶层字段固定**:apiType/targets/buildOption/buildOptionSet/
|
|
134
|
+
buildModeBinder/entryModules/apiType 等。塞 `testOption` 之类非法字段报
|
|
135
|
+
`Schema validate failed`。hypium 测试靠 `src/ohosTest/` 目录约定,不需在 build-profile 配。
|
|
136
|
+
- **退役/不编译的代码移出 `entry/src/main/ets`**(如 spike):放在 ets 下仍会被编译。
|
|
137
|
+
移到工程根的 `_archive/` 之类,并从 `main_pages.json` 去掉。
|
|
138
|
+
|
|
139
|
+
## 8. 运行时:全局状态装配(bootstrap)必须分步容错,不能全有或全无
|
|
140
|
+
|
|
141
|
+
app 入口的「装配全局状态」若把多个步骤串成一个 `await` 链,**任一步抛异常(尤其模拟器/无账号
|
|
142
|
+
环境下的 IAP、首次无持久化)会让后面全不执行**,导致全局 router/store 为 null,UI 一进就崩
|
|
143
|
+
(`AppContext 未装配` / `Object is possibly undefined`)。
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// 错:IAP 在模拟器抛错(如华为 IAP 错误码 27656204)→ router 没装配 → 全崩
|
|
147
|
+
async bootstrap(ctx) {
|
|
148
|
+
await store.load(ctx);
|
|
149
|
+
await iap.start(ctx); // ← 模拟器抛错,下面不执行
|
|
150
|
+
this.router = new Router(...); // ← 永远到不了
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 对:每步独立容错,核心对象(router)一定装配成功
|
|
154
|
+
async bootstrap(ctx) {
|
|
155
|
+
await this.safe('存档', () => store.load(ctx));
|
|
156
|
+
await this.safe('内购', () => iap.start(ctx)); // 失败只 log,降级
|
|
157
|
+
this.router = new Router(...); // 一定执行
|
|
158
|
+
}
|
|
159
|
+
private async safe(name, step) {
|
|
160
|
+
try { await step(); } catch (e) { console.error(`[${name}]降级: ${(e as Error).message}`); }
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
模块内部同理:**核心数据(如试用锚点)与非关键外部依赖(如 IAP 查询)分开 try**,
|
|
165
|
+
非关键失败不连累核心,给 UI 一个合理降级默认值。
|
|
166
|
+
|
|
167
|
+
## 9. DevEco 命令行构建(绕开 IDE,拿带 File:行:列 的错误)
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
NODE=/Applications/DevEco-Studio.app/Contents/tools/node/bin/node
|
|
171
|
+
HVIGOR=/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js
|
|
172
|
+
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk # 停 daemon 后必设
|
|
173
|
+
|
|
174
|
+
# 配置校验
|
|
175
|
+
$NODE $HVIGOR --sync -p product=default --daemon
|
|
176
|
+
# 编译 + 打 HAP(验 ArkTS 编译;产物 entry/build/.../entry-default-unsigned.hap)
|
|
177
|
+
$NODE $HVIGOR assembleHap -p product=default --mode module -p module=entry@default --daemon \
|
|
178
|
+
2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "Error Message:"
|
|
179
|
+
# 预览器构建(linter 更严,查 @BuilderParam/controller 默认值等;用独立 .preview 缓存)
|
|
180
|
+
$NODE $HVIGOR --mode module -p module=entry@default -p product=default -p pageType=page \
|
|
181
|
+
-p previewMode=true -p buildRoot=.preview PreviewBuild --daemon
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **必须 `--daemon`**:DevEco daemon 自动注入 SDK 路径;`--no-daemon` 报 `Invalid DEVECO_SDK_HOME`。
|
|
185
|
+
- **`Error Message:` 才是阻断错误**;`WARN ... may throw exceptions` 是建议,不阻断。
|
|
186
|
+
- **PreviewBuild 用独立 `.preview` 缓存**:改了源码仍报已修错误 → `rm -rf entry/.preview` 重跑。
|
|
187
|
+
- 两种构建都要过:assembleHap(真机包)+ PreviewBuild(linter 严,预览器)。
|
|
188
|
+
|
|
189
|
+
## 10. 仍需真机确认的(编译/预览过 ≠ 运行对)
|
|
190
|
+
|
|
191
|
+
见 `dwy-arkts-strict.md` 第 9 条(cryptoFramework 签名规格 / 公钥 DER 格式 / TCPSocket 局域网)。
|
|
192
|
+
补充:手撸 HTTP/2 的 HPACK 全字面量是否被对端(如 grpc-netty)接受、华为 IAP 需 AGC 后台
|
|
193
|
+
配商品 + 真机华为账号、asset/Preferences 在不同设备的可用性,均真机才能定论。
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: iOS / Swift / SwiftUI 开发避坑清单(XcodeGen / 签名 / 真机部署 / SwiftUI 代码踩坑)
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.swift"
|
|
5
|
+
- "**/project.yml"
|
|
6
|
+
- "**/Package.swift"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# iOS 开发坑清单
|
|
10
|
+
|
|
11
|
+
> 动手前先对照本清单避开已知坑;踩到新坑随手按四栏格式(现象 / 根因 / 正确做法 / 适用)追加到对应分类,没有合适分类就新建一个。
|
|
12
|
+
|
|
13
|
+
## 分类索引
|
|
14
|
+
|
|
15
|
+
- 工程与构建(XcodeGen / SPM / Xcode 工程)
|
|
16
|
+
- 签名与真机部署
|
|
17
|
+
- 证书 / Keychain / mTLS
|
|
18
|
+
- 排查方法论
|
|
19
|
+
- Swift / SwiftUI 代码
|
|
20
|
+
- 其他分类遇到再加:StoreKit / 内购、Info.plist / 权限 / Capability、上架审核
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 工程与构建
|
|
25
|
+
|
|
26
|
+
### XcodeGen 工程要打开 .xcodeproj,不能直接打开项目文件夹
|
|
27
|
+
|
|
28
|
+
- **现象**:Xcode 里左侧根节点是文件夹名、右边只显示 `Package.swift`,点项目没有 General / Signing & Capabilities 标签页;选不到真机、装不上真机。
|
|
29
|
+
- **根因**:用 Xcode 直接打开了**项目文件夹**(或 `Package.swift`),Xcode 把它当 Swift Package(纯库)加载,没有 app target,自然没有签名页、也不能跑真机。
|
|
30
|
+
- **正确做法**:先 `xcodegen generate` 生成 `<项目>.xcodeproj`,再用 Xcode 打开这个 `.xcodeproj`(Finder 双击或 File → Open),**不要打开文件夹 / Package.swift**。打开对了,左侧根节点是蓝色 app 图标、底下有 TARGETS。
|
|
31
|
+
- **适用**:用 XcodeGen 且 `.xcodeproj` 不入库(被 .gitignore)的工程。SPM-only 的纯库项目本就没 app target,不适用。
|
|
32
|
+
|
|
33
|
+
### .xcodeproj 是生成物,Xcode 里手改设置会被 xcodegen 覆盖
|
|
34
|
+
|
|
35
|
+
- **现象**:在 Xcode 里改了签名 / Build Settings / bundle id,下次 `xcodegen generate` 之后改动全没了。
|
|
36
|
+
- **根因**:`.xcodeproj` 由 `project.yml` 生成、通常不入库;`xcodegen generate` 整体重建工程,覆盖一切手改。
|
|
37
|
+
- **正确做法**:需要持久化的工程设置(签名、bundle id、Build Settings、Info.plist 键)一律改 `project.yml` 再 `xcodegen generate`。Xcode 里手改只用于临时试验。
|
|
38
|
+
- **适用**:XcodeGen 工程。
|
|
39
|
+
|
|
40
|
+
### Package.swift 的 .macOS 平台声明,别为「只支持手机」删掉
|
|
41
|
+
|
|
42
|
+
- **现象**:想让 app 只支持 iPhone,看到 `Package.swift` 里 `platforms: [.iOS(.v17), .macOS(.v14)]` 就想删掉 `.macOS`。
|
|
43
|
+
- **根因**:这是**纯逻辑库**的平台声明,作用是让命令行 `swift test` 能在 mac(host)上跑核心逻辑测试,和 iOS app 支持哪些设备**无关**。app 的设备类型由 app target 的 `TARGETED_DEVICE_FAMILY` 控制。删掉 `.macOS` 会让 `swift test` 在 mac 上跑不起来。
|
|
44
|
+
- **正确做法**:保留 `.macOS(.v14)`。要 app 只支持手机,设 app target `TARGETED_DEVICE_FAMILY: "1"`(1=iPhone,2=iPad)。库的 platforms 是「最低支持平台」,不影响 app。
|
|
45
|
+
- **适用**:两层结构(SPM 纯逻辑库 + iOS app)、靠命令行 `swift test` 验证核心逻辑的工程。
|
|
46
|
+
|
|
47
|
+
### XcodeGen 加 String Catalog,要靠 CFBundleLocalizations 才生成多语言 .lproj
|
|
48
|
+
|
|
49
|
+
- **现象**:加了 `Localizable.xcstrings` 并翻译了多国语言,运行时却只显示开发语言(如中文),系统切语言 / app 内切语言都不生效;`xcodegen generate` 出来的 `.xcodeproj` 里 `knownRegions` 只有 `Base, en`。
|
|
50
|
+
- **根因**:XcodeGen 不会扫 `.xcstrings` 内容来推断本地化区域。pbxproj 的 `knownRegions` 决定 Xcode 编译哪些 `.lproj`;某语言不在其中,String Catalog 里该语言就不会编进 app bundle,自建的 `loc()` / 系统本地化都只能回退开发语言。纯 `.xcodeproj` 工程不暴露此坑,因为 `knownRegions` 是手维护的(参考项目 niannian 即如此)。
|
|
51
|
+
- **正确做法**:在 `project.yml` 的 app target `info.properties` 下显式声明 `CFBundleLocalizations` 数组,列出全部目标区域(如 `zh-Hans, zh-Hant, en, ja, ko, de, fr, es`)。XcodeGen 会据此把这些区域写进 pbxproj 的 `knownRegions`,Xcode 才会把 `.xcstrings` 编成各语言 `.lproj`。生成后用 `grep -A12 knownRegions TVRemote.xcodeproj/project.pbxproj` 核对区域齐全。
|
|
52
|
+
- **适用**:XcodeGen + String Catalog(`.xcstrings`)且 `.xcodeproj` 不入库的工程。
|
|
53
|
+
|
|
54
|
+
### Info.plist 文案本地化要用固定文件名 InfoPlist.xcstrings
|
|
55
|
+
|
|
56
|
+
- **现象**:`CFBundleDisplayName`(app 显示名)、各 `NS*UsageDescription`(权限弹窗文案)硬编码成一种语言,想随界面语言本地化,但放进 `Localizable.xcstrings` 不生效。
|
|
57
|
+
- **根因**:Info.plist 的面向用户字段不归 `Localizable.xcstrings` 管,Xcode 用一个专门的 String Catalog —— 文件名必须是 `InfoPlist.xcstrings`(大小写敏感),Xcode 自动识别它来本地化 Info.plist 各键。
|
|
58
|
+
- **正确做法**:在资源目录建 `InfoPlist.xcstrings`,key 用 Info.plist 的键名(`CFBundleDisplayName` / `NSLocalNetworkUsageDescription` / `NSPhotoLibraryAddUsageDescription`…),各 key 补齐目标语言。XcodeGen 工程把它放进 target 的 sources 目录即自动纳入 resources(`lastKnownFileType = text.json.xcstrings`);仍需配 `CFBundleLocalizations` 注册区域(见上一条)。`project.yml` 里 `info.properties` 的原值作为开发语言默认保留即可,xcstrings 提供各语言覆盖。
|
|
59
|
+
- **适用**:iOS app 本地化 Info.plist 文案,Xcode 15+ String Catalog。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 签名与真机部署
|
|
64
|
+
|
|
65
|
+
### 命令行/CI 用的 CODE_SIGNING_ALLOWED=NO + 空 DEVELOPMENT_TEAM,装真机前要改
|
|
66
|
+
|
|
67
|
+
- **现象**:工程命令行编译、模拟器运行都正常,但装不上真机 / 没有可用签名。
|
|
68
|
+
- **根因**:`project.yml` 为命令行、CI、模拟器验证配了 `CODE_SIGNING_ALLOWED: "NO"` 和 `DEVELOPMENT_TEAM: ""`;真机安装必须代码签名,这套配置把签名直接关了。
|
|
69
|
+
- **正确做法**:真机部署在 `project.yml` 的 app target settings 配 `DEVELOPMENT_TEAM: "<teamID>"` + `CODE_SIGN_STYLE: Automatic`,删掉 `CODE_SIGNING_ALLOWED: "NO"`,再 `xcodegen generate`。命令行 / CI 仍可在调用时用 `xcodebuild ... CODE_SIGNING_ALLOWED=NO` 覆盖,不必把它写死进工程。teamID 在 Xcode → Settings → Accounts 看。
|
|
70
|
+
- **适用**:为命令行 / 模拟器验证而关签名的工程,首次要装真机时。
|
|
71
|
+
|
|
72
|
+
### personal team(免费账号)签名,bundle id 不能用 com.example 等保留前缀
|
|
73
|
+
|
|
74
|
+
- **现象**:用免费 personal team 自动签名失败 / 注册 App ID 被拒。
|
|
75
|
+
- **根因**:`com.example.*`(及 `com.apple.*` 等)是保留 / 示例域名,注册 App ID 会被拒。
|
|
76
|
+
- **正确做法**:换成自有反向域名,如 `com.<你的标识>.<app>`;同时改 `project.yml` 的 `bundleIdPrefix` 和 `PRODUCT_BUNDLE_IDENTIFIER`,再 `xcodegen generate`。
|
|
77
|
+
- **适用**:免费 personal team 真机调试。付费账号用已注册的 App ID。
|
|
78
|
+
|
|
79
|
+
### personal team 签的 app 7 天过期、首次需在设备「信任开发者」
|
|
80
|
+
|
|
81
|
+
- **现象**:免费账号装的 app 几天后打不开;或刚装上点击提示「不受信任的开发者」。
|
|
82
|
+
- **根因**:personal team 签名证书有效期仅 7 天;且首次安装的开发者证书需手动信任。
|
|
83
|
+
- **正确做法**:过期重新 `Cmd+R` 安装即可;首次安装后到 iPhone → 设置 → 通用 → VPN与设备管理 → 信任你的开发者证书。
|
|
84
|
+
- **适用**:免费 personal team。付费账号无 7 天限制。
|
|
85
|
+
|
|
86
|
+
### devicectl 的设备 UUID 和 xcodebuild 的 -destination id 不是同一套
|
|
87
|
+
|
|
88
|
+
- **现象**:`xcrun devicectl list devices` 拿到设备 Identifier(如 `5C5E6FAB-D288-559D-9425-3C2754AA401C`),直接拿去 `xcodebuild -destination 'id=...'`,报 `Unable to find a device matching the provided destination specifier` / `no available devices matched`。
|
|
89
|
+
- **根因**:两套工具用两套设备标识。`devicectl` 列的是它自己的 CoreDevice UUID;`xcodebuild` 的 `-destination id=` 要的是硬件 ECID 形式的 id(如 `00008101-001645E21406001E`)。同一台真机两个 id 完全不同,不能混用。
|
|
90
|
+
- **正确做法**:编译阶段从 `xcodebuild` 报错末尾的「Available destinations for the scheme」里抄那台机器的 `id=`(ECID 形式)传给 `-destination`;安装阶段 `xcrun devicectl device install app --device <UUID>` 用 `devicectl list devices` 的 UUID。即:build 用 xcodebuild 的 id,install 用 devicectl 的 UUID,按名字对上同一台机器。
|
|
91
|
+
- **适用**:命令行 `xcodebuild` 编译 + `xcrun devicectl device install` 安装真机的流程(Xcode 15+ / iOS 17+ CoreDevice)。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 排查方法论
|
|
96
|
+
|
|
97
|
+
### 别把 deploymentTarget 下限误读成「不支持高版本」
|
|
98
|
+
|
|
99
|
+
- **现象**:看到工程部署目标 iOS 17.0,以为「不支持 iOS 26」,甚至想抬高部署目标来「支持新系统」。
|
|
100
|
+
- **根因**:deploymentTarget 是**最低**支持版本(下限),不是上限。iOS 17.0 表示 17 及以上(含 26)全部支持。真机装不上几乎从不是因为这个。
|
|
101
|
+
- **正确做法**:真机装不上,按「打开方式 → 签名配置 → Xcode 是否支持该 iOS 大版本」排查,别先动部署目标。另:iOS 新大版本真机需要配套新 Xcode(如 iOS 26 真机需 Xcode 26),但那报的是设备支持 / 架构类错误,也不是改部署目标能解决的。
|
|
102
|
+
- **适用**:通用。
|
|
103
|
+
|
|
104
|
+
### 真机装不上时,拿「同机能跑的另一个项目」做对照
|
|
105
|
+
|
|
106
|
+
- **现象**:一个项目装不上真机、报错含糊(如 unsupported architecture),方向难定。
|
|
107
|
+
- **根因**:同一台 Mac、同一个 Xcode 下,若另一个项目能装真机,说明 Xcode 版本、设备、账号都没问题,差异一定在**这个项目的工程配置**(打开方式 / 签名 / bundle id)。
|
|
108
|
+
- **正确做法**:找一个同机能装真机的项目当 working example,逐项对比 `project.yml`、打开方式、签名设置,差异处就是根因——比凭空猜快得多。
|
|
109
|
+
- **适用**:手头有可对照项目时的真机部署排查。
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 证书 / Keychain / mTLS
|
|
114
|
+
|
|
115
|
+
### SecIdentityCreateWithCertificate 在 iOS 不存在,组装 SecIdentity 要走 keychain 查询
|
|
116
|
+
|
|
117
|
+
- **现象**:用 swift-certificates 生成自签证书后想组装 `SecIdentity`,命令行 `swift test`(host 是 macOS)能过,但一编译 iOS(模拟器/真机)就报 `cannot find 'SecIdentityCreateWithCertificate' in scope`,阻断整个 app build。
|
|
118
|
+
- **根因**:`SecIdentityCreateWithCertificate` 是 **macOS-only** API,iOS SDK 里没有。只在 macOS 跑 `swift test` 不会暴露——因为测试 host 就是 macOS,能解析到该符号。
|
|
119
|
+
- **正确做法**:iOS 上把私钥(`SecItemAdd` + `kSecClassKey`)和证书(`SecItemAdd` + `kSecClassCertificate`)分别入 keychain,再用 `kSecClassIdentity` + `SecItemCopyMatching` 查询——系统会自动把同一公钥的 key+cert 组装成 `SecIdentity` 返回。此写法 iOS / macOS 通用。
|
|
120
|
+
- **适用**:iOS 客户端 mTLS / 自签证书 / SecIdentity 组装。两层工程(SPM 纯逻辑库 + iOS app)只靠 `swift test` 验证逻辑时,**务必再补一次 iOS 编译**,否则 macOS-only 的 Security API 会从 `swift test` 漏过去。
|
|
121
|
+
|
|
122
|
+
### swift-crypto 的 Crypto 产物在 Xcode 测试目标链接报「framework 缺二进制」
|
|
123
|
+
|
|
124
|
+
- **现象**:给 SPM Core 加了 swift-crypto / swift-certificates 后,app target 能 build,但单元测试目标 `build-for-testing` 报 `clang: error: no such file or directory: .../Crypto_<hash>_PackageProduct.framework/Crypto_<hash>_PackageProduct`。
|
|
125
|
+
- **根因**:swift-crypto 的 `Crypto` 产物在 Apple 平台会桥接系统 CryptoKit,其 `PackageProduct.framework` 没有可链接的二进制。测试目标若**同时**依赖 app target 和 `package: <Core>`,就会去直接链这个空 framework 而报缺文件;app target 自身的链接逻辑会跳过它,所以 app build 不受影响、只有测试目标炸。
|
|
126
|
+
- **正确做法**:单元测试目标**只依赖 app 宿主 target**(`- target: <App>`),删掉冗余的 `- package: <Core>`。Core 及其传递框架由 app 链接并嵌入,测试经宿主仍可 `import <Core>`(含 `@testable import <App>`)。改 `project.yml` 后 `xcodegen generate` 重生工程。
|
|
127
|
+
- **适用**:XcodeGen + SPM,且 unit-test 目标依赖了含 swift-crypto / swift-certificates 这类「Apple 平台会桥接系统库」的包时。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Swift / SwiftUI 代码
|
|
132
|
+
|
|
133
|
+
### @AppStorage 驱动 .preferredColorScheme 切换后不即时生效(须重启)
|
|
134
|
+
|
|
135
|
+
- **现象**:设置页切「浅色/深色/跟随系统」,`.preferredColorScheme` 由 `@AppStorage("themeMode")` 驱动,改后界面不变,杀进程重启才生效。换 sheet→push、枚举存值改存 `String` rawValue、调整视图层级都**无效**——全是猜,方向错。
|
|
136
|
+
- **根因**:SwiftUI 框架级已知问题——`.preferredColorScheme` 由 `@AppStorage` 驱动时不即时刷新,尤其 `.system` 档对应的 `nil` 不触发更新(iOS 17/18 多人复现)。与「枚举 vs String 的 KVO」无关,换存储类型治不了本。
|
|
137
|
+
- **正确做法**:别用 `@AppStorage` 驱动 `.preferredColorScheme`。改用单一 `@MainActor @Observable` 单例(如 `ThemeManager.shared`,持久化自己写 UserDefaults),`@State private var theme = ThemeManager.shared` 在根视图持有,`.preferredColorScheme(theme.mode.colorScheme)` 读它;`@Observable` 属性变化可靠驱动重算,绕开框架 bug。这正是同项目语言切换 `LocalizationManager.shared` 已验证的免重启 pattern——设置类全局开关一律照此办,别走 `@AppStorage` + `App` struct。范例:`Sources/Foundation/DesignKit/ThemeManager.swift` + `TVRemote/App/RootView.swift`。仍命中 nil 不刷新时,system 档读 `@Environment(\.colorScheme)` 当前值兜底代替 nil。
|
|
138
|
+
- **适用**:iOS 17+ SwiftUI,运行时切换 `.preferredColorScheme` 需即时生效。推广:任何「设置改后须即时全局生效」的状态,优先 `@Observable` 单例 + `@State` 持有,而非 `@AppStorage` 跨视图/跨 App-struct 同步。
|
|
139
|
+
|
|
140
|
+
### 隐藏系统导航栏后边缘滑动返回手势失灵
|
|
141
|
+
|
|
142
|
+
- **现象**:用 NavigationStack 做主路由,页面自绘深色 UI 故 `.toolbar(.hidden, for: .navigationBar)` 隐藏系统导航栏。隐藏后 iOS 左缘右滑返回手势(interactivePopGestureRecognizer)不响应,只能点自绘返回按钮。
|
|
143
|
+
- **根因**:`interactivePopGestureRecognizer` 的 `delegate` 默认指向系统导航栏。导航栏被隐藏后,delegate 据「栏不可见」拒绝识别手势,于是边缘返回整体失效。这是 SwiftUI/UIKit 十年老坑,不是 NavigationStack 的 bug。
|
|
144
|
+
- **正确做法**:用 `UIViewControllerRepresentable` 在 `didMove(toParent:)` 拿 `navigationController?.interactivePopGestureRecognizer`,置 `isEnabled = true` 并把 delegate 换成**自定义带守卫的 delegate**(`gestureRecognizerShouldBegin` 仅在 `viewControllers.count > 1` 时返回 true)。封装成 `.swipeBackEnabled()` View 扩展。范例见 `TVRemote/App/SwipeBackEnabler.swift`。**切忌把 delegate 置 nil**——见下条。
|
|
145
|
+
- **适用**:iOS 16+ NavigationStack(或 UIKit push 栈)+ 隐藏系统导航栏自绘顶栏的场景。手感须真机验证。
|
|
146
|
+
|
|
147
|
+
### interactivePopGestureRecognizer.delegate 置 nil 致根页崩溃/卡死
|
|
148
|
+
|
|
149
|
+
- **现象**:为恢复边缘手势把 `interactivePopGestureRecognizer.delegate = nil`。结果:在首页(栈根)边缘右滑后首页卡死、点击崩溃;甚至点返回按钮程序化 pop 也崩溃,app 退到桌面。
|
|
150
|
+
- **根因**:UIKit 默认的手势 delegate 含「栈深 ≤ 1 不识别」的守卫,防止 pop 根控制器。置 nil 移除这道守卫,根页边缘滑动会试图 pop 根控制器,UINavigationController 内部状态损坏 → 卡死/崩溃;损坏后任何后续 pop(含程序化 `path = []`)都可能崩。
|
|
151
|
+
- **正确做法**:不要 `delegate = nil`。装自定义 `UIGestureRecognizerDelegate`,`gestureRecognizerShouldBegin` 返回 `navigationController.viewControllers.count > 1`,根页(count==1)不放行。`shouldRecognizeSimultaneouslyWith` 返回 false 避免与滚动冲突。delegate 弱引用 navigationController 防循环。
|
|
152
|
+
- **适用**:iOS 16+ 自管 interactivePopGestureRecognizer 的场景。本坑由上一条「隐藏 navbar 恢复手势」的错误修法引出,二者要一起记。
|
|
153
|
+
|
|
154
|
+
### TabView 切页保活视图态,别用 switch 渲染 tab 内容
|
|
155
|
+
|
|
156
|
+
- **现象**:底部 tab 用 `@ViewBuilder { switch tab { case .a: AViewView() ... } }` 渲染,切 tab 再切回,页面的滚动位置、已加载数据、嵌套导航栈全丢——「重复刷新」。
|
|
157
|
+
- **根因**:`switch` 在视图树里每次只保留当前 case 的子树,切 tab 时 SwiftUI **销毁**旧 tab 视图、**重建**新 tab 视图,`@State` 随之重置。它不是缓存容器。
|
|
158
|
+
- **正确做法**:用系统 `TabView(selection:)` + 每页 `.tag(...)`。TabView 保活所有 tab 页,切换只是显隐,状态天然保留。隐藏系统 tab bar(`.toolbar(.hidden, for: .tabBar)`)后用自绘底栏,`selection` 与自绘栏共用同一 `@State`。注意别用 `.tabViewStyle(.page)`(会引入左右滑动切页,与边缘返回手势冲突)。
|
|
159
|
+
- **适用**:iOS 16+ SwiftUI 多 tab 且需保留各 tab 页内状态时。
|
|
@@ -42,6 +42,32 @@ final class TaskListViewModel {}
|
|
|
42
42
|
- 全屏背景、遮罩、弹层可用 `ignoresSafeArea()`。
|
|
43
43
|
- 禁止使用固定状态栏高度常量。
|
|
44
44
|
|
|
45
|
+
## 软键盘
|
|
46
|
+
|
|
47
|
+
- 任何调起系统软键盘的输入控件,必须给用户提供主动关闭键盘的途径,禁止只能靠输入框自身失焦。
|
|
48
|
+
- 至少同时满足以下两种退出方式:
|
|
49
|
+
- 点击键盘外空白区域收起:根容器加 `.onTapGesture` 把 `@FocusState` 置 `false`。
|
|
50
|
+
- 滚动视图内滚动时收起:`ScrollView` 加 `.scrollDismissesKeyboard(.interactively)`。
|
|
51
|
+
- 多行输入或无明显空白可点的页面,额外在键盘上方放「完成」按钮收起。
|
|
52
|
+
- 焦点统一用 `@FocusState` 管理,不散落手写 UIKit `resignFirstResponder`。
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
@FocusState private var focused: Bool
|
|
56
|
+
|
|
57
|
+
ScrollView {
|
|
58
|
+
TextField(loc("input_placeholder"), text: $text)
|
|
59
|
+
.focused($focused)
|
|
60
|
+
}
|
|
61
|
+
.scrollDismissesKeyboard(.interactively)
|
|
62
|
+
.onTapGesture { focused = false }
|
|
63
|
+
.toolbar {
|
|
64
|
+
ToolbarItemGroup(placement: .keyboard) {
|
|
65
|
+
Spacer()
|
|
66
|
+
Button(loc("done")) { focused = false }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
45
71
|
## 动效
|
|
46
72
|
|
|
47
73
|
- 默认优先 `spring`,用同一组参数避免风格撕裂。
|
|
@@ -54,6 +80,7 @@ private let motionSpring = Animation.spring(response: 0.5, dampingFraction: 0.75
|
|
|
54
80
|
|
|
55
81
|
- 图标优先 `Image(systemName:)`,无符号则用成熟 SVG。
|
|
56
82
|
- iOS 应用图标设计稿只提供一张 `1024x1024` 的 PNG 源图,由系统生成各尺寸适配版本。
|
|
83
|
+
- App Icon 源图必须是直角完整方形,背景填满整张画布,禁止预裁圆角、圆角背景、透明圆角或模拟系统圆角遮罩。
|
|
57
84
|
- 图标主体必须预留至少 `10%` 安全边距,避免系统缩放、圆角遮罩或视觉放大时被切边。
|
|
58
85
|
- 文案禁止 emoji,禁止硬编码,统一 `loc("key")`。
|
|
59
86
|
- 禁止使用 emoji 作为功能说明、状态、提示。
|
|
@@ -75,3 +102,11 @@ Text(loc("home_title"))
|
|
|
75
102
|
|
|
76
103
|
- 颜色、字号走 token,不用裸值。
|
|
77
104
|
- 时间来源通过注入参数或测试可控入口,不直接散落 `Date()`。
|
|
105
|
+
|
|
106
|
+
## 打包与版本号
|
|
107
|
+
|
|
108
|
+
- **build 号用时间戳**:`CFBundleVersion` 每次打包设为 `date +%Y%m%d%H%M`(如 `202606231415`)。时间戳天然唯一递增,满足 TestFlight / App Store「build 号唯一递增」要求,免去人工记号撞号。
|
|
109
|
+
- marketing version `CFBundleShortVersionString`(如 `1.0`)按发布节奏手动改,不随每次打包变。
|
|
110
|
+
- 用 XcodeGen 的工程,`xcodegen generate` 会按 `project.yml` 重写 `Info.plist` 把 build 号覆盖回默认值。顺序必须 **先 `xcodegen generate`、再 `PlistBuddy -c "Set CFBundleVersion <时间戳>"` 设号、最后 archive**,别反。
|
|
111
|
+
- 归档产物放工程内固定目录(如 `build/`)并加 gitignore,不用 `$TMPDIR` 临时目录。
|
|
112
|
+
- changelog 维护在 iOS 端目录下(如 `ios/CHANGELOG.md`),发版前按「新增 / 变更 / 修复」记本次相对上版的用户可感变化。
|
package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-acceptance
|
|
3
|
+
description: "【dwy·验收测试】产品 0 到 1 上架迭代阶段。触发场景:用户说『验收 / 验收测试 / 跑集成测试 / E2E 测试 / 上线前测试 / acceptance』,要在上架前做集成/E2E/兼容/合规验收并查文档与代码实际差距时。不重复单元测试。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
集成/E2E/兼容/合规/沙盒验收 + 文档 vs 代码实际差距分析。**不重复单元测试**(单测归 dwy-tdd-dev)。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- state.json 的 `confirmed.dev_progress`、`confirmed.prd`、`confirmed.architecture`
|
|
11
|
+
- 项目源码 + `.dwy/prod/[项目]/02-需求规划/PRD.md`
|
|
12
|
+
- `.dwy/prod/[项目]/03-设计与架构/接口契约.md`
|
|
13
|
+
|
|
14
|
+
## 实现
|
|
15
|
+
[包装型] 用全局本地 skill(位于 `~/.dwy/skills/<外部skill名>/`):
|
|
16
|
+
读其 `SKILL.md`(及同目录 scripts/、配套 .md),按它的方式与用户交互/提问,产出按下方「产出契约」落地。
|
|
17
|
+
- 本 skill 依赖的外部 skill:`test-scenarios`、`intended-vs-implemented`(各对应 `~/.dwy/skills/<名>/`)
|
|
18
|
+
- 若 `~/.dwy/skills/<名>/` 不存在:提示用户先跑 `dwy skills install`(或 `dwy claude sync` 会自动装),不要自己用内置能力顶替。
|
|
19
|
+
|
|
20
|
+
## 产出契约(硬约束)
|
|
21
|
+
- 落到:`.dwy/prod/[项目]/04-上线交付/验收报告.md`
|
|
22
|
+
- 固定章节:集成/E2E 场景结果 / 兼容性 / 合规与沙盒 / 文档vs代码差距 / 验收结论
|
|
23
|
+
- 回写 state.json:`confirmed.acceptance`(验收结论 / 是否达上线标准)
|
package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-architecture
|
|
3
|
+
description: "【dwy·技术架构】产品 0 到 1 设计与架构阶段。触发场景:用户说『设计架构 / 出技术方案 / 设计库表 / 定接口契约 / 技术架构 / architecture』,要在开发前定下技术架构、数据库设计和接口契约时。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
按项目 rules 产出技术架构 + 数据库库表设计 + 接口契约。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- state.json 的 `confirmed.prd`、`confirmed.tasks`、`confirmed.prototype`
|
|
11
|
+
- `.dwy/prod/[项目]/02-需求规划/PRD.md`、`开发任务拆解.md`
|
|
12
|
+
- 项目 `.claude/rules/` 下的技术栈与编码规则
|
|
13
|
+
|
|
14
|
+
## 实现
|
|
15
|
+
[自写型]
|
|
16
|
+
1. 读项目 `.claude/rules/`,按项目既定技术栈与规范产出架构
|
|
17
|
+
2. 技术架构:分层、模块边界、技术选型理由
|
|
18
|
+
3. 数据库设计:库表、字段、索引、关系
|
|
19
|
+
4. 接口契约:路由、请求/响应 schema(遵循项目响应格式约定)
|
|
20
|
+
5. **走 rules 土建**,不引入项目规范外的技术栈
|
|
21
|
+
|
|
22
|
+
## 产出契约(硬约束)
|
|
23
|
+
- 落到:`.dwy/prod/[项目]/03-设计与架构/技术架构.md`、`数据库设计.md`、`接口契约.md`
|
|
24
|
+
- 固定章节:
|
|
25
|
+
- 技术架构.md:分层架构 / 模块划分 / 技术选型
|
|
26
|
+
- 数据库设计.md:库表清单 / 字段定义 / 关系与索引
|
|
27
|
+
- 接口契约.md:接口清单 / 请求响应 schema / 错误码
|
|
28
|
+
- 回写 state.json:`confirmed.architecture`
|
package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-competitor
|
|
3
|
+
description: "【dwy·竞品分析】产品 0 到 1 立项阶段。触发场景:用户说『分析竞品 / 看看市面上有啥 / 竞品调研 / 对标分析 / 市场上谁在做 / competitor』,想知道已有玩家、市场规模和差异化切入点时。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
盘点已有竞品 + 估算市场规模,找出差异化切入点。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- state.json 的 `confirmed.idea`
|
|
11
|
+
- `.dwy/prod/[项目]/01-立项/想法收敛.md`
|
|
12
|
+
|
|
13
|
+
## 实现
|
|
14
|
+
[包装型] 用全局本地 skill(位于 `~/.dwy/skills/<外部skill名>/`):
|
|
15
|
+
读其 `SKILL.md`(及同目录 scripts/、配套 .md),按它的方式与用户交互/提问,产出按下方「产出契约」落地。
|
|
16
|
+
- 本 skill 依赖的外部 skill:`competitor-analysis`、`market-sizing`(各对应 `~/.dwy/skills/<名>/`)
|
|
17
|
+
- 若 `~/.dwy/skills/<名>/` 不存在:提示用户先跑 `dwy skills install`(或 `dwy claude sync` 会自动装),不要自己用内置能力顶替。
|
|
18
|
+
|
|
19
|
+
## 产出契约(硬约束)
|
|
20
|
+
- 落到:`.dwy/prod/[项目]/01-立项/竞品分析.md`
|
|
21
|
+
- 固定章节:竞品清单 / 各竞品优劣 / 市场规模(TAM/SAM/SOM)/ 差异化切入点
|
|
22
|
+
- 回写 state.json:`confirmed.competitors`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-ideate
|
|
3
|
+
description: "【dwy·想法收敛】产品 0 到 1 立项第一步。触发场景:用户说『我有个想法 / 帮我把这个点子理清楚 / 想做个 XX / 收敛想法 / 立项 / ideate』,要把模糊念头通过苏格拉底式反问发散+收敛成清晰可执行的产品想法时。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
把用户模糊的初始念头,通过苏格拉底式反问发散+收敛,产出一份清晰的产品想法定义。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- 用户口述的初始想法(首个原子,无上游产出)
|
|
11
|
+
|
|
12
|
+
## 实现
|
|
13
|
+
[包装型] 用全局本地 skill(位于 `~/.dwy/skills/<外部skill名>/`):
|
|
14
|
+
读其 `SKILL.md`(及同目录 scripts/、配套 .md),按它的方式与用户交互/提问,产出按下方「产出契约」落地。
|
|
15
|
+
- 本 skill 依赖的外部 skill:`brainstorming`(对应 `~/.dwy/skills/brainstorming/`)
|
|
16
|
+
- 若 `~/.dwy/skills/brainstorming/` 不存在:提示用户先跑 `dwy skills install`(或 `dwy claude sync` 会自动装),不要自己用内置能力顶替。
|
|
17
|
+
|
|
18
|
+
## 产出契约(硬约束)
|
|
19
|
+
- 落到:`.dwy/prod/[项目]/01-立项/想法收敛.md`
|
|
20
|
+
- 固定章节:核心问题 / 目标用户 / 价值主张 / 关键假设 / 边界与不做什么
|
|
21
|
+
- 回写 state.json:`confirmed.idea`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-mvp
|
|
3
|
+
description: "【dwy·MVP定义】产品 0 到 1 立项阶段(闸3)。触发场景:用户说『定义 MVP / 最小可行产品 / 先做哪些功能 / 砍功能 / 第一版做什么 / mvp』,要从想法收敛出第一版必须做的核心功能清单时。MVP 功能硬上限 ≤7 个。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
从想法+验证结果,收敛出第一版 MVP 功能清单。**功能硬上限 ≤7 个**,超出的进后续版本。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- state.json 的 `confirmed.idea`、`confirmed.validation`、`confirmed.poc`
|
|
11
|
+
- `.dwy/prod/[项目]/01-立项/想法收敛.md`、`需求市场验证.md`、`技术验证.md`
|
|
12
|
+
|
|
13
|
+
## 实现
|
|
14
|
+
[包装型] 用全局本地 skill(位于 `~/.dwy/skills/<外部skill名>/`):
|
|
15
|
+
读其 `SKILL.md`(及同目录 scripts/、配套 .md),按它的方式与用户交互/提问,产出按下方「产出契约」落地。
|
|
16
|
+
- 本 skill 依赖的外部 skill:`prioritize-features`、`opportunity-solution-tree`(各对应 `~/.dwy/skills/<名>/`)
|
|
17
|
+
- 若 `~/.dwy/skills/<名>/` 不存在:提示用户先跑 `dwy skills install`(或 `dwy claude sync` 会自动装),不要自己用内置能力顶替。
|
|
18
|
+
|
|
19
|
+
## 产出契约(硬约束)
|
|
20
|
+
- 落到:`.dwy/prod/[项目]/01-立项/MVP清单.md`
|
|
21
|
+
- 固定章节:MVP 功能清单(≤7 个)/ 优先级排序 / 砍到后续版本的功能
|
|
22
|
+
- **功能数超 7 个直接拒绝,强制收敛**
|
|
23
|
+
- 回写 state.json:`confirmed.mvp_features`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-poc
|
|
3
|
+
description: "【dwy·技术验证】产品 0 到 1 立项阶段(闸2)。触发场景:用户说『技术验证 / 这技术能不能成 / 跑个 POC / 验证可行性 / 核心技术死穴 / poc』,要在写 PRD 前用最小可跑代码实测核心技术假设时。仅在需求市场验证通过后执行。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
识别 1 个核心技术死穴假设,写最小可跑 POC 实测能不能成。失败→建议砍方向/换技术栈,不进 PRD。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- state.json 的 `confirmed.validation`(仅当 `pass=true` 才执行)
|
|
11
|
+
- `.dwy/prod/[项目]/01-立项/需求市场验证.md`、`想法收敛.md`
|
|
12
|
+
|
|
13
|
+
## 实现
|
|
14
|
+
[自写型]
|
|
15
|
+
1. 校验 `confirmed.validation.pass=true`,否则直接退出
|
|
16
|
+
2. 从想法中识别**唯一一个**最致命的技术假设(最可能让方案不成立的那个)
|
|
17
|
+
3. 写最小可跑 POC 代码实测,代码放 `.dwy/prod/[项目]/01-立项/poc/`
|
|
18
|
+
4. 实测结果定性:成立 / 不成立
|
|
19
|
+
5. 不成立 → 给出砍方向或换技术栈建议,**不进 PRD**
|
|
20
|
+
|
|
21
|
+
## 产出契约(硬约束)
|
|
22
|
+
- 落到:`.dwy/prod/[项目]/01-立项/技术验证.md`,POC 代码放 `01-立项/poc/`
|
|
23
|
+
- 固定章节:核心技术假设 / POC 方案 / 实测结果 / 结论与建议
|
|
24
|
+
- 回写 state.json:`confirmed.poc`(含 `pass` 布尔字段)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-prd
|
|
3
|
+
description: "【dwy·PRD生成】产品 0 到 1 需求规划阶段。触发场景:用户说『写 PRD / 出需求文档 / 产品需求文档 / prd』,要把 MVP 清单细化成可开发的产品需求文档时。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 职责(单一)
|
|
7
|
+
把 MVP 功能清单细化成完整可开发的 PRD。
|
|
8
|
+
|
|
9
|
+
## 输入(从哪读)
|
|
10
|
+
- state.json 的 `confirmed.mvp_features`、`confirmed.idea`
|
|
11
|
+
- `.dwy/prod/[项目]/01-立项/MVP清单.md`
|
|
12
|
+
|
|
13
|
+
## 实现
|
|
14
|
+
[包装型] 用全局本地 skill(位于 `~/.dwy/skills/<外部skill名>/`):
|
|
15
|
+
读其 `SKILL.md`(及同目录 scripts/、配套 .md),按它的方式与用户交互/提问,产出按下方「产出契约」落地。
|
|
16
|
+
- 本 skill 依赖的外部 skill:`create-prd`(对应 `~/.dwy/skills/create-prd/`)
|
|
17
|
+
- 若 `~/.dwy/skills/create-prd/` 不存在:提示用户先跑 `dwy skills install`(或 `dwy claude sync` 会自动装),不要自己用内置能力顶替。
|
|
18
|
+
|
|
19
|
+
## 产出契约(硬约束)
|
|
20
|
+
- 落到:`.dwy/prod/[项目]/02-需求规划/PRD.md`
|
|
21
|
+
- 固定章节:背景与目标 / 用户场景 / 功能需求(逐条)/ 非功能需求 / 验收标准
|
|
22
|
+
- 回写 state.json:`confirmed.prd`
|