From 875464a61ada5cc884fc5e05fb333f10f700170f Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Tue, 20 Oct 2026 22:41:19 +0200 Subject: [PATCH] Guide smooth SwiftUI interactions and efficient image rendering --- .../skills/agent-guidelines-audit/SKILL.md | 24 ++++++ CHANGELOG.md | 8 ++ Guidelines/Swift/SwiftUI.md | 17 ++++- README.md | 4 +- Scripts/validate_guidelines.swift | 65 ++++++++++++++++ Tests/run_tests.swift | 76 +++++++++++++++++++ VERSION | 2 +- 7 files changed, 192 insertions(+), 4 deletions(-) diff --git a/.agents/skills/agent-guidelines-audit/SKILL.md b/.agents/skills/agent-guidelines-audit/SKILL.md index 80ac0b5..7761545 100644 --- a/.agents/skills/agent-guidelines-audit/SKILL.md +++ b/.agents/skills/agent-guidelines-audit/SKILL.md @@ -31,6 +31,7 @@ Review the actual change rather than only checking whether files exist: - For each new or changed SwiftUI `ScrollView`, apply the shared [scrolling guidance](../../../Guidelines/Swift/SwiftUI.md#scrolling): verify `.scrollBounceBehavior(.basedOnSize)` covers its scrolling axes on supported deployment targets, including scroll views in modal sheets. Check the modifier's environment scope around nested scrollables and require a documented interaction reason for a different bounce behavior. - Perform the mandatory [native-component and presentation decision check](#audit-native-components-and-presentation) for changed SwiftUI controls and presentation behavior. - Perform the mandatory [system-control placement check](#audit-system-control-placement) for changed navigation, toolbars, dismissal controls, and window chrome, including affected UIKit/AppKit wrappers. +- Perform the mandatory [animation and image-rendering check](#audit-animation-and-image-rendering) for changed visual transitions, reusable control styles, and dynamic image paths, including directly affected consumers. - Check tests for the required framework, mirrored paths, shared tags, Given/When/Then structure, deterministic seams, and coverage of changed behavior and failure paths. - Trace every new or changed stateful, asynchronous, fallible, or lifecycle-oriented behavior and verify that its owning artifact emits privacy-safe AppLogger events for the meaningful success, failure, cancellation, recovery, and state-transition outcomes needed to diagnose it. Dependency declaration and target linkage alone do not establish logging coverage. Accept silence for pure values or utilities only when there is no meaningful event boundary and the implementation handoff records that deliberate decision. - Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, noise controls, and focused formatter or sink tests when logging changed. @@ -63,6 +64,29 @@ Apply the authoritative [system control placement policy](../../../Guidelines/Sw 4. For changed navigation or toolbar behavior, record supported target/OS and targeted runtime actions and outcomes: Back navigates, dismissal closes the intended presentation, and cancellation/confirmation retain their actual semantics. Check relevant layout direction, accessibility, keyboard/remote, and gesture behavior. When presentation lifecycle also changes, apply the existing [interaction evidence requirements](../../../Guidelines/Swift/SwiftUI.md#required-interaction-evidence) to that affected flow. Documentation-only policy changes do not require simulator evidence. 5. An unjustified placement override or missing relevant interaction evidence blocks audit completion. Report the exact control, ownership, violated policy, and missing evidence; do not report an Apple-selected side as a defect. +## Audit animation and image rendering + +Apply the authoritative [animation policy](../../../Guidelines/Swift/SwiftUI.md#animation) and [image-rendering policy](../../../Guidelines/Swift/SwiftUI.md#image-rendering) to changed components and directly affected consumers; do not expand the audit into unrelated screens. + +1. Identify each meaningful visual transition, its owner, and its intended animation or justified immediate feedback. Inspect stable view identity and transaction scope, including simultaneous changes and surrounding layout animations; an `.animation` modifier alone is not evidence of correct scope. Accept appropriate native feedback without additional animation. +2. Verify reusable defaults without caller-added animation. Exercise enabled to disabled and disabled to enabled, ordinary presses, rapid reversals, and relevant show/hide behavior. Confirm enabled state, hit-testing, accessibility state, and action availability change immediately while appearance animates. +3. Inspect affected image paths for repeated preparation, oversized drawable content, unstable identity, unbounded caches, and stale publication. Verify derivative refresh for changed source or presentation requirements, bounded intermediate memory, preservation of authoritative originals, and rejection of stale results after replacement, cancellation, or removal. Use applicable deterministic tests for asynchronous races alongside interaction evidence. +4. Exercise representative large-image content when images participate in the interaction. For image-related regressions, compare with an image-free scenario and record the actual representation drawn; do not infer a provider defect from the source of one expensive image. +5. Require recorded runtime actions and outcomes for meaningful motion changes: platform/OS, scenario, actions, and observed behavior. Compilation, static screenshots, and idle captures do not prove smooth interaction. Documentation-only policy changes require scenario walkthroughs rather than simulator evidence. +6. For reported stutter, use Apple performance tools where available and authorized. Record the active interaction window, workload, observations, and limitations. Distinguish recorded manual verification from instrumented measurements; an idle capture reporting zero hitches is insufficient. Do not require profiling every cosmetic change or impose a universal frame-rate threshold. +7. Unexplained abrupt transitions in affected reusable controls, demonstrated performance regressions, or missing applicable interaction evidence prevent audit completion. Record the exact component, violated policy, required correction or justified intentional exception, and unavailable evidence. + +Use these scenario walkthroughs when evaluating the check: + +| Scenario | Audit outcome | +| --- | --- | +| Custom main-button style changes colors abruptly | Blocked until shared behavior is corrected or an intentional immediate transition is justified | +| Shared style animates both directions; actions remain immediate | Accepted with runtime evidence, including ordinary presses and rapid reversals | +| Appropriate native control already supplies feedback | Accepted without additional animation | +| Animated thumbnail repeatedly processes a large original | Requires correction and relevant verification | +| Prepared derivative preserves originals and rejects stale results | Accepted with applicable tests and interaction evidence | +| Zero hitches recorded without actual interaction | Insufficient performance evidence; applicable verification remains blocked | + ## Audit Xcode project settings For every checked-in `.xcodeproj`, read and apply the shared [Xcode project-settings guide](../../../Guidelines/Xcode/ProjectSettings.md): diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a63aa9..6e2ad2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project are documented in this file. +## [0.0.41] - 2026-10-20 + +### Added + +- Required reusable custom-styled controls to own smooth visual transitions while functional state changes immediately, preserving native feedback, stable identity, and narrow animation scope. +- Added display-appropriate image preparation, bounded memory, derivative invalidation, original preservation, and stale-result rejection guidance. +- Added an animation and image-rendering audit with targeted runtime evidence, honest performance measurements, scenario walkthroughs, and validator mutation coverage. Existing Reduce Motion policy is unchanged. + ## [0.0.40] - 2026-10-20 ### Added diff --git a/Guidelines/Swift/SwiftUI.md b/Guidelines/Swift/SwiftUI.md index 2ef418b..4fa1ac3 100644 --- a/Guidelines/Swift/SwiftUI.md +++ b/Guidelines/Swift/SwiftUI.md @@ -105,9 +105,24 @@ Use these decisions when auditing an implementation: - Use native SwiftUI animation to make apps fluid rather than static. Apply it purposefully to buttons, transitions, interactions, selection, colors, and other visible state changes that benefit from feedback; do not animate every update or add motion for its own sake. - Prefer the default SwiftUI animation, using `withAnimation { ... }` or `.animation(.default, value: ...)`, unless the task prompt explicitly requests a specific animation. Prefer system-provided transitions and SF Symbol effects when they fit the interaction instead of recreating them with custom timing, curves, or rendering. -- Scope animation to the relevant state change or view. Preserve responsive controls, stable layout, and uninterrupted user interaction; avoid blanket animations that also animate unrelated state changes. +- Reusable custom-styled controls own their basic visual transitions. Animate enabled/disabled appearance smoothly by default in both directions, without screen-level opt-in. A native `Button` preserves interaction semantics, but does not define transitions for every custom foreground, background, or shadow. Preserve system-owned feedback instead of layering duplicate animation over it. +- Change enabled state, hit-testing, accessibility state, and action availability immediately. Animate appearance; never delay functional state until an animation finishes. Immediate press, focus, or selection feedback remains appropriate when it improves responsiveness; record an intentional immediate transition and its reason. +- Preserve stable view identity when only appearance changes. Prefer changing fill, opacity, or shadow values over replacing equivalent view branches. +- Scope animation to the intended state and visual attributes. Preserve responsive controls, stable layout, and uninterrupted user interaction; animating surrounding layout must not accidentally animate every control inside it. The presence of `.animation` does not prove correct scope: value-triggered animation can affect other changes in the same transaction. Use narrower modifier or closure scope where necessary and supported by the deployment target. - Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them. This includes reading `accessibilityReduceMotion` to replace animation with `nil`, remove symbol effects, or otherwise suppress app-authored animation. Preserve any accessibility behavior provided automatically by native system components; this policy governs additional app-level overrides. +Consult Apple's [Explore SwiftUI animation](https://developer.apple.com/videos/play/wwdc2023/10156/) for transaction propagation and scoped animation behavior. The reusable-control defaults above are repository policy. + +## Image rendering + +- Use retained, display-appropriate derivatives for large dynamic images in small, frequently updated views instead of repeatedly rendering the original bitmap. This applies to photos, downloaded images, generated images, and other bitmap sources; an expensive rendering path does not by itself establish a provider defect. +- Account for display scale, aspect-fit/fill requirements, orientation, fidelity, and bounded intermediate/cache memory. Refresh derivatives when their source or relevant presentation requirements change. +- Keep expensive loading, decoding, resizing, and conversion outside view construction and repeated interaction paths. Perform expensive preparation off the main actor when supported and concurrency-safe; do not prescribe detached tasks as a universal solution. +- Reject stale asynchronous results after replacement, cancellation, or removal. Keep presentation derivatives separate from authoritative originals needed for editing, saving, or export. +- Inspect the representation actually drawn. A small display frame does not establish a small decoded image, and caching a `UIImage` alone does not remove rendering cost. Choose preparation based on measured cost and required fidelity; do not impose blanket bans on `Image`, `UIImage(data:)`, or full-resolution presentation, or universal pixel sizes, color formats, cache architectures, or `drawingGroup()` requirements. + +Apple's [iOS Memory Deep Dive](https://developer.apple.com/videos/play/wwdc2018/416/) explains image memory costs and demonstrates ImageIO downsampling for thumbnails without first decoding the full image. Use the [animation and image-rendering audit](../../.agents/skills/agent-guidelines-audit/SKILL.md#audit-animation-and-image-rendering) to verify affected interactions and record performance evidence. + ## Previews - Put previews at the end of the file under `// MARK: - Preview`. diff --git a/README.md b/README.md index f124b92..a45fc96 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.40 \ + 0.0.41 \ --squash ``` @@ -148,7 +148,7 @@ Review the target release's changelog, then pull it deliberately: git subtree pull \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.40 \ + 0.0.41 \ --squash ``` diff --git a/Scripts/validate_guidelines.swift b/Scripts/validate_guidelines.swift index b82269f..f8aad7e 100755 --- a/Scripts/validate_guidelines.swift +++ b/Scripts/validate_guidelines.swift @@ -714,6 +714,70 @@ func validateNativeComponentPolicy(_ errors: inout [String]) { } } +/// Protects interaction policy and audit evidence requirements, not consumer runtime performance. +func validateInteractionRenderingPolicy(_ errors: inout [String]) { + let requirements: [(URL, [String])] = [ + ( + swiftUIGuideline, + [ + "## Animation", + "Reusable custom-styled controls own their basic visual transitions", + "both directions, without screen-level opt-in", + "Preserve system-owned feedback instead of layering duplicate animation", + "never delay functional state until an animation finishes", + "Preserve stable view identity when only appearance changes", + "value-triggered animation can affect other changes in the same transaction", + "Use narrower modifier or closure scope", + "Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them", + "## Image rendering", + "retained, display-appropriate derivatives", + "display scale, aspect-fit/fill requirements, orientation, fidelity, and bounded intermediate/cache memory", + "Refresh derivatives when their source or relevant presentation requirements change", + "outside view construction and repeated interaction paths", + "off the main actor when supported and concurrency-safe", + "Reject stale asynchronous results after replacement, cancellation, or removal", + "separate from authoritative originals needed for editing, saving, or export", + "Inspect the representation actually drawn", + "do not impose blanket bans", + ] + ), + ( + auditSkill, + [ + "Perform the mandatory [animation and image-rendering check](#audit-animation-and-image-rendering)", + "## Audit animation and image rendering", + "Guidelines/Swift/SwiftUI.md#animation", + "Guidelines/Swift/SwiftUI.md#image-rendering", + "do not expand the audit into unrelated screens", + "its owner, and its intended animation or justified immediate feedback", + "Verify reusable defaults without caller-added animation", + "enabled to disabled and disabled to enabled, ordinary presses, rapid reversals", + "hit-testing, accessibility state, and action availability change immediately", + "repeated preparation, oversized drawable content, unstable identity, unbounded caches, and stale publication", + "Exercise representative large-image content", + "compare with an image-free scenario", + "Require recorded runtime actions and outcomes for meaningful motion changes", + "Compilation, static screenshots, and idle captures do not prove smooth interaction", + "Documentation-only policy changes require scenario walkthroughs", + "use Apple performance tools where available and authorized", + "Record the active interaction window", + "Distinguish recorded manual verification from instrumented measurements", + "Do not require profiling every cosmetic change or impose a universal frame-rate threshold", + "missing applicable interaction evidence prevent audit completion", + "| Scenario | Audit outcome |", + "| Appropriate native control already supplies feedback | Accepted without additional animation |", + "| Zero hitches recorded without actual interaction | Insufficient performance evidence", + ] + ), + ] + for (file, required) in requirements { + guard let contents = readText(file, errors: &errors) else { continue } + for value in required where !contents.contains(value) { + errors.append("\(file.lastPathComponent): missing interaction/rendering requirement: '\(value)'") + } + } +} + /// Validates the completion-audit skill and native helper scripts. func validateAuditSkill(_ errors: inout [String]) { guard let contents = readText(auditSkill, errors: &errors) else { @@ -924,6 +988,7 @@ func main() -> Int32 { validatePackageCompilerSettingsGuideline(&errors) validateExternalDependencyPolicy(&errors) validateNativeComponentPolicy(&errors) + validateInteractionRenderingPolicy(&errors) validateCodexReviewEvidence(&errors) validateGitignoreGuidance(&errors) validateExecutable(consumerSetupScript, description: "consumer setup validator", errors: &errors) diff --git a/Tests/run_tests.swift b/Tests/run_tests.swift index 5d1de3f..87ecf75 100755 --- a/Tests/run_tests.swift +++ b/Tests/run_tests.swift @@ -120,6 +120,82 @@ func localization(_ value: String, state: String = "translated") -> String { } let tests: [(String, () throws -> Void)] = [ + ( + "interaction/rendering validator rejects guide and audit drift", + { + let mutations: [(String, [String])] = [ + ( + "Guidelines/Swift/SwiftUI.md", + [ + "## Animation", + "both directions, without screen-level opt-in", + "Preserve system-owned feedback instead of layering duplicate animation", + "never delay functional state until an animation finishes", + "Preserve stable view identity when only appearance changes", + "value-triggered animation can affect other changes in the same transaction", + "Use narrower modifier or closure scope", + "Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them", + "## Image rendering", + "retained, display-appropriate derivatives", + "display scale, aspect-fit/fill requirements, orientation, fidelity, and bounded intermediate/cache memory", + "Refresh derivatives when their source or relevant presentation requirements change", + "outside view construction and repeated interaction paths", + "off the main actor when supported and concurrency-safe", + "Reject stale asynchronous results after replacement, cancellation, or removal", + "separate from authoritative originals needed for editing, saving, or export", + "Inspect the representation actually drawn", + "do not impose blanket bans", + ] + ), + ( + ".agents/skills/agent-guidelines-audit/SKILL.md", + [ + "Perform the mandatory [animation and image-rendering check](#audit-animation-and-image-rendering)", + "## Audit animation and image rendering", + "Guidelines/Swift/SwiftUI.md#animation", + "Guidelines/Swift/SwiftUI.md#image-rendering", + "do not expand the audit into unrelated screens", + "its owner, and its intended animation or justified immediate feedback", + "Verify reusable defaults without caller-added animation", + "enabled to disabled and disabled to enabled, ordinary presses, rapid reversals", + "hit-testing, accessibility state, and action availability change immediately", + "repeated preparation, oversized drawable content, unstable identity, unbounded caches, and stale publication", + "Exercise representative large-image content", + "compare with an image-free scenario", + "Require recorded runtime actions and outcomes for meaningful motion changes", + "Compilation, static screenshots, and idle captures do not prove smooth interaction", + "Documentation-only policy changes require scenario walkthroughs", + "use Apple performance tools where available and authorized", + "Record the active interaction window", + "Distinguish recorded manual verification from instrumented measurements", + "Do not require profiling every cosmetic change or impose a universal frame-rate threshold", + "missing applicable interaction evidence prevent audit completion", + "| Appropriate native control already supplies feedback | Accepted without additional animation |", + "| Zero hitches recorded without actual interaction | Insufficient performance evidence", + ] + ), + ] + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let validator = fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path + let baseline = try run([validator]) + try require(baseline.succeeded, "unmodified interaction policy failed: \(baseline.output)") + for (path, fragments) in mutations { + let file = fixture.appendingPathComponent(path) + let contents = try String(contentsOf: file, encoding: .utf8) + for original in fragments { + try require(contents.contains(original), "missing test fixture text: \(original)") + try write(contents.replacingOccurrences(of: original, with: "REMOVED"), to: file) + let result = try run([validator]) + try require(!result.succeeded, "interaction policy mutation unexpectedly passed: \(original)") + try require(result.output.contains("missing interaction/rendering requirement"), result.output) + try write(contents, to: file) + } + } + } + } + ), ( "native-component validator rejects policy and decision drift", { diff --git a/VERSION b/VERSION index 4fe2fe8..89a67e8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.40 +0.0.41