@mmerterden/multi-agent-pipeline 20.17.2 → 20.19.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 +19 -0
- package/docs/facts.json +2 -2
- package/install/_mcp-register.mjs +1 -1
- package/install/_xcode-skills.mjs +263 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +23 -0
- package/install/codex.mjs +11 -0
- package/install/copilot.mjs +9 -0
- package/manifest.json +25 -22
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/stack/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +45 -18
- package/pipeline/lib/xcode-plugin.mjs +131 -0
- package/pipeline/multi-agent-refs/features/doctor.md +10 -0
- package/pipeline/scripts/README.md +1 -0
- package/pipeline/scripts/doctor.mjs +38 -0
- package/pipeline/scripts/release-notes.mjs +167 -0
- package/pipeline/scripts/uninstall.mjs +57 -1
- package/pipeline/skills/.skill-manifest.json +2 -2
- package/pipeline/skills/shared/external/ios-iphone-duo/SKILL.md +9 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/evals/evals.json +15 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +59 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* release-notes.mjs - what an update brought, grouped so the user can be told.
|
|
4
|
+
*
|
|
5
|
+
* `/multi-agent:update` runs this from the release it just unpacked, against that
|
|
6
|
+
* release's CHANGELOG: every version after the one that was installed, up to the
|
|
7
|
+
* one now installed, with its bullets grouped by kind (added, changed, fixed,
|
|
8
|
+
* removed, ...). A jump across many releases is reported whole, not cut at a line
|
|
9
|
+
* count. The same is done for each stack plugin the plugin refresh moved, read
|
|
10
|
+
* from that plugin's CHANGELOG in the marketplace checkout.
|
|
11
|
+
*
|
|
12
|
+
* The output is data for the update step, which summarizes it for the user in
|
|
13
|
+
* their language. It never fails an update: a missing or unreadable CHANGELOG is
|
|
14
|
+
* empty notes and exit 0.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* release-notes.mjs --changelog <CHANGELOG.md> --from <old> --to <new>
|
|
18
|
+
* [--plugins-dir <marketplace>/plugins --plugin-refresh-log <file>]
|
|
19
|
+
*
|
|
20
|
+
* stdout: JSON `{ pipeline: Notes, plugins: [{ name, from, to, notes: Notes }] }`
|
|
21
|
+
* where Notes is `{ from, to, versions: [{ version, date, sections }], counts }`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
27
|
+
|
|
28
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
29
|
+
|
|
30
|
+
const RELEASE_RE = /^## \[(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\](?:\s*-\s*(.+))?\s*$/;
|
|
31
|
+
const SECTION_RE = /^### (.+?)\s*$/;
|
|
32
|
+
|
|
33
|
+
function cmp(a, b) {
|
|
34
|
+
const [pa, qa = ""] = a.split("-");
|
|
35
|
+
const [pb, qb = ""] = b.split("-");
|
|
36
|
+
const x = pa.split(".").map(Number);
|
|
37
|
+
const y = pb.split(".").map(Number);
|
|
38
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
|
|
39
|
+
if (qa === qb) return 0;
|
|
40
|
+
if (!qa) return 1;
|
|
41
|
+
if (!qb) return -1;
|
|
42
|
+
return qa < qb ? -1 : 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Released versions in a Keep a Changelog file, newest first as written.
|
|
47
|
+
* `## [Unreleased]` is skipped; a bullet's wrapped lines are joined.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @returns {{version: string, date: string|null, sections: Record<string, string[]>}[]}
|
|
51
|
+
*/
|
|
52
|
+
export function parseChangelog(text) {
|
|
53
|
+
const releases = [];
|
|
54
|
+
let release = null;
|
|
55
|
+
let section = null;
|
|
56
|
+
let bullet = null;
|
|
57
|
+
const flush = () => {
|
|
58
|
+
if (bullet !== null && release && section) release.sections[section].push(bullet.trim());
|
|
59
|
+
bullet = null;
|
|
60
|
+
};
|
|
61
|
+
for (const line of String(text || "").split("\n")) {
|
|
62
|
+
if (line.startsWith("## ")) {
|
|
63
|
+
flush();
|
|
64
|
+
const m = RELEASE_RE.exec(line);
|
|
65
|
+
release = m ? { version: m[1], date: m[2]?.trim() || null, sections: {} } : null;
|
|
66
|
+
if (release) releases.push(release);
|
|
67
|
+
section = null;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!release) continue;
|
|
71
|
+
const s = SECTION_RE.exec(line);
|
|
72
|
+
if (s) {
|
|
73
|
+
flush();
|
|
74
|
+
section = s[1].toLowerCase();
|
|
75
|
+
release.sections[section] ||= [];
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!section) continue;
|
|
79
|
+
if (/^[-*] /.test(line)) {
|
|
80
|
+
flush();
|
|
81
|
+
bullet = line.slice(2);
|
|
82
|
+
} else if (bullet !== null && /^\s+\S/.test(line)) {
|
|
83
|
+
bullet += " " + line.trim();
|
|
84
|
+
} else if (!line.trim()) {
|
|
85
|
+
flush();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
flush();
|
|
89
|
+
return releases;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Releases after `from` up to and including `to`, newest first, with a count of
|
|
94
|
+
* bullets per section across all of them.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} text
|
|
97
|
+
* @param {{from: string, to: string}} range
|
|
98
|
+
*/
|
|
99
|
+
export function releaseNotes(text, { from, to }) {
|
|
100
|
+
const versions = parseChangelog(text)
|
|
101
|
+
.filter((r) => cmp(r.version, from) > 0 && cmp(r.version, to) <= 0)
|
|
102
|
+
.sort((a, b) => cmp(b.version, a.version));
|
|
103
|
+
const counts = {};
|
|
104
|
+
for (const v of versions) {
|
|
105
|
+
for (const [k, items] of Object.entries(v.sections))
|
|
106
|
+
counts[k] = (counts[k] || 0) + items.length;
|
|
107
|
+
}
|
|
108
|
+
return { from, to, versions, counts };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Plugins the refresh moved, from the CLI's own lines
|
|
113
|
+
* (`Plugin "<name>" updated from <a> to <b> for scope ...`). A plugin updated in
|
|
114
|
+
* several scopes is reported once, from the oldest version any of them had.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} output
|
|
117
|
+
* @returns {{name: string, from: string, to: string}[]}
|
|
118
|
+
*/
|
|
119
|
+
export function pluginUpdates(output) {
|
|
120
|
+
const byName = new Map();
|
|
121
|
+
const re = /Plugin "([^"]+)" updated from (\S+) to (\S+)/g;
|
|
122
|
+
for (const m of String(output || "").matchAll(re)) {
|
|
123
|
+
const [, name, from, to] = m;
|
|
124
|
+
const seen = byName.get(name);
|
|
125
|
+
if (!seen) byName.set(name, { name, from, to });
|
|
126
|
+
else {
|
|
127
|
+
if (cmp(from, seen.from) < 0) seen.from = from;
|
|
128
|
+
if (cmp(to, seen.to) > 0) seen.to = to;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...byName.values()];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function read(path) {
|
|
135
|
+
try {
|
|
136
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
137
|
+
} catch {
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function arg(argv, name) {
|
|
143
|
+
const i = argv.indexOf(name);
|
|
144
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function main(argv = process.argv.slice(2)) {
|
|
148
|
+
const from = arg(argv, "--from") || "0.0.0";
|
|
149
|
+
const to = arg(argv, "--to") || from;
|
|
150
|
+
const pipeline = releaseNotes(read(arg(argv, "--changelog") || ""), { from, to });
|
|
151
|
+
|
|
152
|
+
const pluginsDir = arg(argv, "--plugins-dir");
|
|
153
|
+
const refreshLog = arg(argv, "--plugin-refresh-log");
|
|
154
|
+
const plugins = [];
|
|
155
|
+
if (pluginsDir && refreshLog) {
|
|
156
|
+
for (const u of pluginUpdates(read(refreshLog))) {
|
|
157
|
+
const notes = releaseNotes(read(join(pluginsDir, u.name, "CHANGELOG.md")), u);
|
|
158
|
+
plugins.push({ ...u, notes });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
process.stdout.write(JSON.stringify({ pipeline, plugins }, null, 2) + "\n");
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
|
166
|
+
runMain("release-notes", () => main());
|
|
167
|
+
}
|
|
@@ -53,6 +53,12 @@ import { createHash } from "crypto";
|
|
|
53
53
|
import { join, dirname } from "path";
|
|
54
54
|
import { homedir } from "os";
|
|
55
55
|
import { pathToFileURL } from "url";
|
|
56
|
+
import {
|
|
57
|
+
CLAUDE_MARKETPLACE_DIR,
|
|
58
|
+
XCODE_MARKETPLACE,
|
|
59
|
+
XCODE_PLUGIN_ID,
|
|
60
|
+
XCODE_SKILLS_MANIFEST,
|
|
61
|
+
} from "../lib/xcode-plugin.mjs";
|
|
56
62
|
import { createInterface } from "readline";
|
|
57
63
|
|
|
58
64
|
const HOME = process.env.HOME;
|
|
@@ -425,6 +431,48 @@ function deregisterClaudeMcpServer() {
|
|
|
425
431
|
deregisterMcpServer("claude", existsSync(localBin) ? localBin : undefined, ["--scope", "user"]);
|
|
426
432
|
}
|
|
427
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Apple's Xcode plugin is linked into Claude Code from a local directory
|
|
436
|
+
* marketplace the installer keeps at the installed Xcode build. Without the
|
|
437
|
+
* pipeline nothing refreshes that copy, so it goes with the pipeline; Xcode
|
|
438
|
+
* itself still offers the same skills.
|
|
439
|
+
*/
|
|
440
|
+
function unlinkClaudeXcodePlugin(home) {
|
|
441
|
+
const mpDir = join(home, CLAUDE_MARKETPLACE_DIR);
|
|
442
|
+
if (!existsSync(mpDir)) return;
|
|
443
|
+
const localBin = join(homedir(), ".local", "bin", "claude");
|
|
444
|
+
const cli = existsSync(localBin) ? localBin : "claude";
|
|
445
|
+
const steps = [
|
|
446
|
+
["plugin", "uninstall", XCODE_PLUGIN_ID, "--scope", "user"],
|
|
447
|
+
["plugin", "marketplace", "remove", XCODE_MARKETPLACE],
|
|
448
|
+
];
|
|
449
|
+
if (dryRun) {
|
|
450
|
+
for (const args of steps) report("would run", `claude ${args.join(" ")}`);
|
|
451
|
+
report("would remove", mpDir);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const failed = [];
|
|
455
|
+
for (const args of steps) {
|
|
456
|
+
try {
|
|
457
|
+
execFileSync(cli, args, { stdio: "pipe", timeout: 20_000 });
|
|
458
|
+
} catch (e) {
|
|
459
|
+
const out = `${e?.stderr ?? ""}${e?.stdout ?? ""}`;
|
|
460
|
+
if (!/not (found|installed)|no such|does not exist|unknown/i.test(out)) failed.push(args);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Claude Code's settings still point at the directory until the CLI drops
|
|
464
|
+
// them; deleting it first would leave a marketplace that fails to load at
|
|
465
|
+
// every start.
|
|
466
|
+
if (failed.length) {
|
|
467
|
+
console.log(" kept: Apple's Xcode skills link - claude could not remove it; run:");
|
|
468
|
+
for (const args of failed) console.log(` claude ${args.join(" ")}`);
|
|
469
|
+
console.log(` then delete ${mpDir}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
rmIfExists(mpDir);
|
|
473
|
+
console.log(" removed: Apple's Xcode skills link (xcode-apple marketplace)");
|
|
474
|
+
}
|
|
475
|
+
|
|
428
476
|
/**
|
|
429
477
|
* Copilot registers the same server, so leaving it behind would keep a stale entry
|
|
430
478
|
* pointing at a package the user just removed.
|
|
@@ -589,7 +637,14 @@ function rmMatchingFiles(parent, predicate) {
|
|
|
589
637
|
const PLUGIN_SKILLS_MANIFEST = ".plugin-skills-manifest.json";
|
|
590
638
|
|
|
591
639
|
function rmPluginDeliveredSkills(skillsDir) {
|
|
592
|
-
|
|
640
|
+
return (
|
|
641
|
+
rmManifestListedSkills(skillsDir, PLUGIN_SKILLS_MANIFEST) +
|
|
642
|
+
rmManifestListedSkills(skillsDir, XCODE_SKILLS_MANIFEST)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function rmManifestListedSkills(skillsDir, manifestName) {
|
|
647
|
+
const manifestPath = join(skillsDir, manifestName);
|
|
593
648
|
if (!existsSync(manifestPath)) return 0;
|
|
594
649
|
let names;
|
|
595
650
|
try {
|
|
@@ -855,6 +910,7 @@ export async function main() {
|
|
|
855
910
|
console.log(" [Claude Code] Removing...");
|
|
856
911
|
deregisterClaudeMcpServer();
|
|
857
912
|
const CLAUDE = join(HOME, ".claude");
|
|
913
|
+
unlinkClaudeXcodePlugin(HOME);
|
|
858
914
|
rmCommandsPreservingLocalOnly(join(CLAUDE, "commands", "multi-agent"));
|
|
859
915
|
rmIfExists(join(CLAUDE, "scripts"));
|
|
860
916
|
// Pipeline-managed trees the installer lays down alongside scripts/.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-09T05:08:19Z",
|
|
4
4
|
"skillCount": 228,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -569,7 +569,7 @@
|
|
|
569
569
|
},
|
|
570
570
|
{
|
|
571
571
|
"path": "shared/external/ios-iphone-duo/SKILL.md",
|
|
572
|
-
"sha256": "
|
|
572
|
+
"sha256": "934df39d42cac41473d4c7605891c4b15aca2d185099c21b36a3c1abbbea8359"
|
|
573
573
|
},
|
|
574
574
|
{
|
|
575
575
|
"path": "shared/external/ios-localization/SKILL.md",
|
|
@@ -19,6 +19,13 @@ APIs where the fold or the vertical bars need them.
|
|
|
19
19
|
in the iOS 27.1 SDK and may change before release. Check availability before
|
|
20
20
|
shipping code that depends on it, and gate it with `if #available`.
|
|
21
21
|
|
|
22
|
+
**Apple's skill first.** Xcode 27.1 and later ship Apple's `app-resizability`
|
|
23
|
+
skill, which the pipeline links from this Mac's Xcode (Claude Code:
|
|
24
|
+
`xcode-integration:app-resizability`). When it is available, run it first for
|
|
25
|
+
the code scan and the API modernization; use this skill for what it adds: the
|
|
26
|
+
fold and poses, reserved regions, arrangements, vertical bars and the design
|
|
27
|
+
review. Without Xcode 27.1, this skill covers the whole job.
|
|
28
|
+
|
|
22
29
|
## One rule behind every fix
|
|
23
30
|
|
|
24
31
|
Do not ask "is this iPhone Duo?". Ask "how much space do I have, and what is
|
|
@@ -54,6 +61,8 @@ Each hit is a candidate defect with a known replacement:
|
|
|
54
61
|
| Symmetric insets (`insets.left * 2`) | Side-mounted bars make insets asymmetric | Each edge's inset on its own |
|
|
55
62
|
| Hand-built `UIToolbar`, `UINavigationBar`, `UITabBar` | Only container-provided bars move to the side | Toolbar items on navigation containers |
|
|
56
63
|
| Full-bleed fill media | Wider displays crop the subject | Choose fill or fit from size, set a focal point |
|
|
64
|
+
| App-delegate-only life cycle | From iOS 27 an app built with the latest SDK must adopt scenes or it fails to launch | Scene manifest and scene delegate, `UIWindow(windowScene:)` |
|
|
65
|
+
| `topLayoutGuide`, keyboard frames, stored insets | Deprecated or computed for a full screen | `safeAreaLayoutGuide`, `keyboardLayoutGuide`, insets read at layout |
|
|
57
66
|
|
|
58
67
|
## Step 3: let containers adapt
|
|
59
68
|
|
|
@@ -45,6 +45,21 @@
|
|
|
45
45
|
"Uses ToolbarOverflowMenu or the system overflow instead of a custom menu",
|
|
46
46
|
"Treats disabling vertical bars as a rare, stable per-screen choice"
|
|
47
47
|
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": 3,
|
|
51
|
+
"name": "scene-lifecycle-and-legacy-guides",
|
|
52
|
+
"prompt": "Our UIKit app still does all its foreground and background work in AppDelegate (applicationDidBecomeActive and friends), creates its window with UIWindow(frame: UIScreen.main.bounds), pins the editor to topLayoutGuide, and moves the input bar up by reading keyboardFrameEndUserInfoKey. We are moving to the iOS 27 SDK and want iPhone Duo support. What breaks and what do we change?",
|
|
53
|
+
"expected_output": "States that from iOS 27 an app built with the latest SDK must adopt the scene-based life cycle or it fails to launch, and that foreground and background arrive per scene. Moves window creation to a scene delegate with UIWindow(windowScene:), the four lifecycle callbacks to their scene equivalents, keeps one-time work in didFinishLaunching, and treats multiple windows as optional. Replaces topLayoutGuide with safeAreaLayoutGuide and keyboard-frame math with keyboardLayoutGuide, and reads insets at layout time. Mentions running Apple's app-resizability skill first when Xcode provides it.",
|
|
54
|
+
"files": [],
|
|
55
|
+
"expectations": [
|
|
56
|
+
"Says an app built with the iOS 27 SDK without the scene life cycle fails to launch",
|
|
57
|
+
"Moves window creation to a scene delegate using UIWindow(windowScene:)",
|
|
58
|
+
"Maps the app delegate lifecycle callbacks to scene callbacks and keeps one-time work at launch",
|
|
59
|
+
"Replaces topLayoutGuide with safeAreaLayoutGuide",
|
|
60
|
+
"Replaces keyboard frame math with keyboardLayoutGuide",
|
|
61
|
+
"Points to Apple's app-resizability skill as the first route when Xcode 27.1+ provides it"
|
|
62
|
+
]
|
|
48
63
|
}
|
|
49
64
|
]
|
|
50
65
|
}
|
|
@@ -15,7 +15,8 @@ the iPhone Duo simulator. Exclude vendored and generated code from the count.
|
|
|
15
15
|
7. Insets
|
|
16
16
|
8. Bars
|
|
17
17
|
9. Media
|
|
18
|
-
10.
|
|
18
|
+
10. Life cycle, legacy guides and the keyboard
|
|
19
|
+
11. Recording the result
|
|
19
20
|
|
|
20
21
|
## 1. The search
|
|
21
22
|
|
|
@@ -33,9 +34,18 @@ rg -n --type swift \
|
|
|
33
34
|
-e '(width|height)\s*(==|>=|<=|>|<)\s*(320|375|390|393|402|414|428|430|440)\b' \
|
|
34
35
|
-e 'UIToolbar\(|UINavigationBar\(|UITabBar\(' \
|
|
35
36
|
-e 'scaleAspectFill|contentMode:\s*\.fill' \
|
|
37
|
+
-e 'topLayoutGuide|bottomLayoutGuide' \
|
|
38
|
+
-e 'keyboardWillShowNotification|keyboardFrameEndUserInfoKey' \
|
|
39
|
+
-e 'applicationDidBecomeActive|applicationWillResignActive|applicationDidEnterBackground|applicationWillEnterForeground' \
|
|
36
40
|
.
|
|
37
41
|
```
|
|
38
42
|
|
|
43
|
+
The scene manifest lives in Info.plist, which the Swift search does not read:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rg -l 'UIApplicationSceneManifest' -g '*.plist' . || echo "no scene manifest"
|
|
47
|
+
```
|
|
48
|
+
|
|
39
49
|
Add Objective-C (`--type objc`) and storyboards (`-g '*.storyboard'` for
|
|
40
50
|
fixed-size constraints) when the project has them.
|
|
41
51
|
|
|
@@ -138,7 +148,54 @@ Replace: choose fill or fit from the current size or aspect ratio, crop around
|
|
|
138
148
|
a focal point, and extend backgrounds under bars with
|
|
139
149
|
`backgroundExtensionEffect()` rather than stretching the image.
|
|
140
150
|
|
|
141
|
-
## 10.
|
|
151
|
+
## 10. Life cycle, legacy guides and the keyboard
|
|
152
|
+
|
|
153
|
+
**Scene life cycle.** Hits: the four `application...` foreground and background
|
|
154
|
+
callbacks doing real work in the app delegate, no `UIApplicationSceneManifest`
|
|
155
|
+
in Info.plist, no `UIWindowSceneDelegate`.
|
|
156
|
+
|
|
157
|
+
Why: from iOS 27, an app built with the latest SDK that has not adopted the
|
|
158
|
+
scene-based life cycle fails to launch. The system also delivers foreground and
|
|
159
|
+
background per scene, so app-level callbacks miss what happens to one window
|
|
160
|
+
of several.
|
|
161
|
+
|
|
162
|
+
Replace: a scene manifest and a scene delegate that creates the window with
|
|
163
|
+
`UIWindow(windowScene:)`, and the per-window work moved to the scene callbacks
|
|
164
|
+
(`sceneDidBecomeActive`, `sceneWillResignActive`, `sceneDidEnterBackground`,
|
|
165
|
+
`sceneWillEnterForeground`). Work that runs once per process stays in
|
|
166
|
+
`application(_:didFinishLaunchingWithOptions:)`. Supporting several windows is
|
|
167
|
+
a separate, optional step.
|
|
168
|
+
|
|
169
|
+
**Legacy layout guides.** Hits: `topLayoutGuide`, `bottomLayoutGuide`, and
|
|
170
|
+
constants standing in for the status bar or navigation bar height.
|
|
171
|
+
|
|
172
|
+
Why: those guides are deprecated and know nothing about side insets; a fixed
|
|
173
|
+
bar height is wrong as soon as a bar moves or the status bar changes.
|
|
174
|
+
|
|
175
|
+
Replace: `safeAreaLayoutGuide` and layout margins.
|
|
176
|
+
|
|
177
|
+
**The keyboard.** Hits: frames computed from `keyboardFrameEndUserInfoKey` in
|
|
178
|
+
a `keyboardWillShowNotification` handler.
|
|
179
|
+
|
|
180
|
+
Why: a screen-space keyboard frame needs converting into the window by hand,
|
|
181
|
+
and frame arithmetic misses the cases a resized window creates.
|
|
182
|
+
|
|
183
|
+
Replace: constraints to `view.keyboardLayoutGuide` (iOS 15); SwiftUI already
|
|
184
|
+
avoids the keyboard through the safe area.
|
|
185
|
+
|
|
186
|
+
**Stored insets.** Hits: a safe-area inset copied into a property at load
|
|
187
|
+
time.
|
|
188
|
+
|
|
189
|
+
Why: insets change with pose, bar placement and Split View position.
|
|
190
|
+
|
|
191
|
+
Replace: read them where layout happens, and react in
|
|
192
|
+
`viewSafeAreaInsetsDidChange()` / `safeAreaInsetsDidChange()`.
|
|
193
|
+
|
|
194
|
+
**Before replacing, classify.** An idiom or orientation check is a defect
|
|
195
|
+
only when it stands in for available space. A check for a genuine capability of
|
|
196
|
+
the device, or an orientation lock for a camera or game, stays.
|
|
197
|
+
|
|
198
|
+
## 11. Recording the result
|
|
142
199
|
|
|
143
200
|
For each hit, record `file:line`, the category, keep or fix, and for fixes the
|
|
144
201
|
replacement. Separate "confirmed on the simulator" from "found in code only".
|