Fix and polish the Home edit-layout experience - #2
Merged
Merged
Conversation
The wiggle never visually oscillated because withAnimation(.repeatForever) was invoked from inside the .task continuation (resumed after Task.sleep), outside SwiftUI's update transaction. There SwiftUI commits the final rotation statically but never registers the repeating interpolation, so each tile snapped to a fixed ~2.5° tilt instead of wiggling. Drive the repeat declaratively via .animation(_:value:) so repeatForever is registered in the view graph and oscillates regardless of which context mutated the state. The animation switches to a gentle ease-out when the wiggle stops so tiles settle upright instead of snapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give every tile the same amplitude (2°) and frequency (0.14s) so they move identically, and derive a per-seed phase offset (a fraction of one cycle, applied via .delay) so they wiggle in lockstep yet desynchronized rather than all tilting the same way at once. Replaces the old per-tile amplitude, duration, and start-stagger variation. Gate `rotation` on `started` instead of `isWiggling` so that on exit the return to zero lands in the same transaction as `animating` flipping false. The ease-out then replaces the running repeatForever; gating on the external isWiggling zeroed the rotation a render too early, leaving the repeat with no delta to cancel against, so it never stopped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Edit mode used to survive tab switches: isEditing and the draft stayed resident in the view model, and if the session tore down in between the draft died uncommitted. Commit on disappear, matching the existing tap-outside-to-exit behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The context menu stayed attached with conditionally-empty content in edit mode, leaving its long-press recognizer competing with the reorder drag. Attach the modifier only outside edit mode instead. This flips tile view identity when edit mode toggles, which resets wiggle state: entry is unaffected (the task re-fires on appear) and exit trades the 0.15s settle ease for an instant 2-degree snap, which is imperceptible at that amplitude. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The resize button was a bare 28pt Image(systemName:) — under the HIG minimum tap target and invisible to VoiceOver. Wrap it in a Label (iconOnly) named "Make Larger"/"Make Smaller" by direction, and expand the hit area to 44pt around the unchanged visible circle. Also correct the Done button's localization comment, which described dismissing a screen rather than committing layout edits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detaching the context menu in edit mode recreates tile views when edit mode toggles, and the freshly inserted WiggleModifier appeared with isWiggling already true: the toggle-a-state-then-repeatForever approach never engaged and tiles froze statically mid-tilt. Compute rotation as a pure function of wall-clock time inside TimelineView(.animation) instead. No stored animation state means no way to get stuck, the per-seed phase becomes a true mid-cycle offset with no startup delay, and start/stop transitions inherit the caller's withAnimation. The timeline pauses outside edit mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
minimumInterval: nil let TimelineView tick at display refresh rate — on ProMotion that is 120 main-thread content-closure evaluations per second per tile while editing, competing with the reorder drag. A 0.28s sine cycle gains nothing above 60Hz. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The resize button hardcoded 44; design-system guardrails belong in Mortar tokens so future controls reference one definition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cancel was already inconsistent: tapping outside the grid and leaving the tab both commit, so discard only worked through one of three exit doors. It also sat next to the undo arrow, reading as back/cancel navigation. Follow the platform precedent (Apple Home, home screen jiggle mode): a leading undo/redo pair navigating the edit history, and Done to persist. Undo and redo are one operation walking opposite stacks: apply(_:) performs an EditAction on the draft and returns its inverse. New edits clear the redo stack (standard history semantics). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hover shuffle teleported: withAnimation around engine.hover() gets stomped when the 60Hz unanimated drag-offset updates land in the same render pass, so displaced tiles jumped whole slots in a single frame (confirmed by frame-by-frame video capture). Scope the animation to the values instead: .animation(_:value:) on each tile's origin and size, active only in edit mode. The dragged tile opts out so it tracks the finger exactly, and on drop the modifier re-arms and animates the settle into its slot. The ghost placeholder glides to its new drop slot for the same reason. Undo/redo layout changes flow through the same value-scoped animation, making them robust regardless of transaction plumbing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The badge only appeared after tapping a tile to select it, which made resizing hard to discover. Show it on all tiles while editing, matching iOS jiggle-mode badges. Selection existed solely to gate badge visibility, so remove the whole mechanism: selectedTileID leaves the view model, the modifier loses its binding, and the edit-mode tap gesture becomes a deliberate no-op so tile taps don't bubble to the grid's tap-outside-to-exit handler. CuratedHomeView no longer needs @bindable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the broken tile wiggle and polishes the whole edit-layout experience: uniform out-of-phase wiggle, undo/redo history instead of Cancel, animated drag reorder, always-visible resize badges, and several robustness/accessibility fixes found by on-device verification and code review.
Wiggle
withAnimation(.repeatForever)from an async task continuation applies the final value statically; driven declaratively instead, then rewritten to compute rotation as a pure function of wall-clock time inTimelineView(.animation)so it survives view identity changes and can never stick mid-tilt. Capped at 60Hz.Edit-mode UX
apply(_:)returns the action's inverse); new edits clear redo.withAnimationtransactions (confirmed frame-by-frame on video). Value-scoped.animation(_:value:)on each tile's frame fixes it; the dragged tile opts out to track the finger, then settles on drop. Undo/redo ride the same mechanism.Mortar.TapTarget.minimumtoken).Testing
CuratedHomeViewModelTests17/17 green.Known follow-ups (deliberately out of scope)
onDisappearinto the view model / session teardown🤖 Generated with Claude Code