60 lines
1.6 KiB
Markdown
60 lines
1.6 KiB
Markdown
# Matched transitions
|
||
|
||
## Intent
|
||
|
||
Use matched transitions to create smooth continuity between a source view (thumbnail, avatar) and a destination view (sheet, detail, viewer).
|
||
|
||
## Core patterns
|
||
|
||
- Use a shared `Namespace` and a stable ID for the source.
|
||
- Use `matchedTransitionSource` + `navigationTransition(.zoom(...))` on iOS 26+.
|
||
- Use `matchedGeometryEffect` for in-place transitions within a view hierarchy.
|
||
- Keep IDs stable across view updates (avoid random UUIDs).
|
||
|
||
## Example: media preview to full-screen viewer (iOS 26+)
|
||
|
||
```swift
|
||
struct MediaPreview: View {
|
||
@Namespace private var namespace
|
||
@State private var selected: MediaAttachment?
|
||
|
||
var body: some View {
|
||
ThumbnailView()
|
||
.matchedTransitionSource(id: selected?.id ?? "", in: namespace)
|
||
.sheet(item: $selected) { item in
|
||
MediaViewer(item: item)
|
||
.navigationTransition(.zoom(sourceID: item.id, in: namespace))
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Example: matched geometry within a view
|
||
|
||
```swift
|
||
struct ToggleBadge: View {
|
||
@Namespace private var space
|
||
@State private var isOn = false
|
||
|
||
var body: some View {
|
||
Button {
|
||
withAnimation(.spring) { isOn.toggle() }
|
||
} label: {
|
||
Image(systemName: isOn ? "eye" : "eye.slash")
|
||
.matchedGeometryEffect(id: "icon", in: space)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Design choices to keep
|
||
|
||
- Prefer `matchedTransitionSource` for cross-screen transitions.
|
||
- Keep source and destination sizes reasonable to avoid jarring scale changes.
|
||
- Use `withAnimation` for state-driven transitions.
|
||
|
||
## Pitfalls
|
||
|
||
- Don’t use unstable IDs; it breaks the transition.
|
||
- Avoid mismatched shapes (e.g., square to circle) unless the design expects it.
|