mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
Compare commits
3 commits
3b61b27eac
...
6745e51597
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6745e51597 | ||
|
|
9f3533a773 | ||
|
|
e0e8b34203 |
4 changed files with 147 additions and 0 deletions
|
|
@ -13,6 +13,15 @@ Install: place these skill folders under `$CODEX_HOME/skills/public` (or symlink
|
|||
Optional: enable the pre-commit hook to keep `docs/skills.json` in sync:
|
||||
`git config core.hooksPath scripts/git-hooks`
|
||||
|
||||
### sk
|
||||
|
||||
Install via [sk](https://github.com/803/skills-supply), the universal package manager for AI agent skills (supports Claude, Codex, OpenCode, etc...).
|
||||
|
||||
```bash
|
||||
sk pkg add github Dimillian/Skills
|
||||
sk sync
|
||||
```
|
||||
|
||||
## Skills
|
||||
|
||||
### 📝 App Store Changelog
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Choose a track based on your goal:
|
|||
- Find a nearby example in the repo with `rg "TabView\("` or similar, then read the closest SwiftUI view.
|
||||
- Apply local conventions: prefer SwiftUI-native state, keep state local when possible, and use environment injection for shared dependencies.
|
||||
- Choose the relevant component reference from `references/components-index.md` and follow its guidance.
|
||||
- If the interaction reveals secondary content by dragging or scrolling the primary content away, read `references/scroll-reveal.md` before implementing gestures manually.
|
||||
- Build the view with small, focused subviews and SwiftUI-native data flow.
|
||||
|
||||
### New project scaffolding
|
||||
|
|
@ -32,6 +33,7 @@ Choose a track based on your goal:
|
|||
- Maintain existing legacy patterns only when editing legacy files.
|
||||
- Follow the project's formatter and style guide.
|
||||
- **Sheets**: Prefer `.sheet(item:)` over `.sheet(isPresented:)` when state represents a selected model. Avoid `if let` inside a sheet body. Sheets should own their actions and call `dismiss()` internally instead of forwarding `onCancel`/`onConfirm` closures.
|
||||
- **Scroll-driven reveals**: Prefer deriving a normalized progress value from scroll offset and driving the visual state from that single source of truth. Avoid parallel gesture state machines unless scroll alone cannot express the interaction.
|
||||
|
||||
## Workflow for a new SwiftUI view
|
||||
|
||||
|
|
@ -50,6 +52,8 @@ Use `references/components-index.md` as the entry point. Each component referenc
|
|||
- Pitfalls and performance notes.
|
||||
- Paths to existing examples in the current repo.
|
||||
|
||||
For detail surfaces that progressively reveal actions, metadata, or contextual panels, use `references/scroll-reveal.md`.
|
||||
|
||||
## Sheet patterns
|
||||
|
||||
### Item-driven sheet (preferred)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Use this file to find component-specific guidance. Each entry lists when to use
|
|||
- Split views and columns: `references/split-views.md` — Use for iPad/macOS multi-column layouts or custom secondary columns.
|
||||
- List and Section: `references/list.md` — Use for feed-style content and settings rows.
|
||||
- ScrollView and Lazy stacks: `references/scrollview.md` — Use for custom layouts, horizontal scrollers, or grids.
|
||||
- Scroll-reveal detail surfaces: `references/scroll-reveal.md` — Use when a detail screen reveals secondary content or actions as the user scrolls or swipes between full-screen sections.
|
||||
- Grids: `references/grids.md` — Use for icon pickers, media galleries, and tiled layouts.
|
||||
- Theming and dynamic type: `references/theming.md` — Use for app-wide theme tokens, colors, and type scaling.
|
||||
- Controls (toggles, pickers, sliders): `references/controls.md` — Use for settings controls and input selection.
|
||||
|
|
|
|||
133
swiftui-ui-patterns/references/scroll-reveal.md
Normal file
133
swiftui-ui-patterns/references/scroll-reveal.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Scroll-reveal detail surfaces
|
||||
|
||||
## Intent
|
||||
|
||||
Use this pattern when a detail screen has a primary surface first and secondary content behind it, and you want the user to reveal that secondary layer by scrolling or swiping instead of tapping a separate button.
|
||||
|
||||
Typical fits:
|
||||
|
||||
- media detail screens that reveal actions or metadata
|
||||
- maps, cards, or canvases that transition into structured detail
|
||||
- full-screen viewers with a second "actions" or "insights" page
|
||||
|
||||
## Core pattern
|
||||
|
||||
Build the interaction as a paged vertical `ScrollView` with two sections:
|
||||
|
||||
1. a primary section sized to the viewport
|
||||
2. a secondary section below it
|
||||
|
||||
Derive a normalized `progress` value from the vertical content offset and drive all visual changes from that one value.
|
||||
|
||||
Avoid treating the reveal as a separate gesture system unless scroll alone cannot express it.
|
||||
|
||||
## Minimal structure
|
||||
|
||||
```swift
|
||||
private enum DetailSection: Hashable {
|
||||
case primary
|
||||
case secondary
|
||||
}
|
||||
|
||||
struct DetailSurface: View {
|
||||
@State private var revealProgress: CGFloat = 0
|
||||
@State private var secondaryHeight: CGFloat = 1
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
PrimaryContent(progress: revealProgress)
|
||||
.frame(height: geometry.size.height)
|
||||
.id(DetailSection.primary)
|
||||
|
||||
SecondaryContent(progress: revealProgress)
|
||||
.id(DetailSection.secondary)
|
||||
.onGeometryChange(for: CGFloat.self) { geo in
|
||||
geo.size.height
|
||||
} action: { newHeight in
|
||||
secondaryHeight = max(newHeight, 1)
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
}
|
||||
.scrollTargetBehavior(.paging)
|
||||
.onScrollGeometryChange(for: CGFloat.self, of: { scroll in
|
||||
scroll.contentOffset.y + scroll.contentInsets.top
|
||||
}) { _, offset in
|
||||
revealProgress = (offset / secondaryHeight).clamped(to: 0...1)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
ChevronAffordance(progress: revealProgress) {
|
||||
withAnimation(.smooth) {
|
||||
let target: DetailSection = revealProgress < 0.5 ? .secondary : .primary
|
||||
proxy.scrollTo(target, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design choices to keep
|
||||
|
||||
- Make the primary section exactly viewport-sized when the interaction should feel like paging between states.
|
||||
- Compute `progress` from real scroll offset, not from duplicated booleans like `isExpanded`, `isShowingSecondary`, and `isSnapped`.
|
||||
- Use `progress` to drive `offset`, `opacity`, `blur`, `scaleEffect`, and toolbar state so the whole surface stays synchronized.
|
||||
- Use `ScrollViewReader` for programmatic snapping from taps on the primary content or chevron affordances.
|
||||
- Use `onScrollTargetVisibilityChange` when you need a settled section state for haptics, tooltip dismissal, analytics, or accessibility announcements.
|
||||
|
||||
## Morphing a shared control
|
||||
|
||||
If a control appears to move from the primary surface into the secondary content, do not render two fully visible copies.
|
||||
|
||||
Instead:
|
||||
|
||||
- expose a source anchor in the primary area
|
||||
- expose a destination anchor in the secondary area
|
||||
- render one overlay that interpolates position and size using `progress`
|
||||
|
||||
```swift
|
||||
Color.clear
|
||||
.anchorPreference(key: ControlAnchorKey.self, value: .bounds) { anchor in
|
||||
["source": anchor]
|
||||
}
|
||||
|
||||
Color.clear
|
||||
.anchorPreference(key: ControlAnchorKey.self, value: .bounds) { anchor in
|
||||
["destination": anchor]
|
||||
}
|
||||
|
||||
.overlayPreferenceValue(ControlAnchorKey.self) { anchors in
|
||||
MorphingControlOverlay(anchors: anchors, progress: revealProgress)
|
||||
}
|
||||
```
|
||||
|
||||
This keeps the motion coherent and avoids duplicate-hit-target bugs.
|
||||
|
||||
## Haptics and affordances
|
||||
|
||||
- Use light threshold haptics when the reveal begins and stronger haptics near the committed state.
|
||||
- Keep a visible affordance like a chevron or pill while `progress` is near zero.
|
||||
- Flip, fade, or blur the affordance as the secondary section becomes active.
|
||||
|
||||
## Interaction guards
|
||||
|
||||
- Disable vertical scrolling when a conflicting mode is active, such as pinch-to-zoom, crop, or full-screen media manipulation.
|
||||
- Disable hit testing on overlays that should disappear once the secondary content is revealed.
|
||||
- Avoid same-axis nested scroll views unless the inner view is effectively static or disabled during the reveal.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not hard-code the progress divisor. Measure the secondary section height or another real reveal distance.
|
||||
- Do not mix multiple animation sources for the same property. If `progress` drives it, keep other animations off that property.
|
||||
- Do not store derived state like `isSecondaryVisible` unless another API requires it. Prefer deriving it from `progress` or visible scroll targets.
|
||||
- Beware of layout feedback loops when measuring heights. Clamp zero values and update only when the measured height actually changes.
|
||||
|
||||
## Concrete example
|
||||
|
||||
- Pool iOS tile detail reveal: `/Users/dimillian/Documents/Dev/Pool/pool-ios/Pool/Sources/Features/Tile/Detail/TileDetailView.swift`
|
||||
- Secondary content anchor example: `/Users/dimillian/Documents/Dev/Pool/pool-ios/Pool/Sources/Features/Tile/Detail/TileDetailIntentListView.swift`
|
||||
Loading…
Reference in a new issue