All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
7.8 KiB
7.8 KiB
name, description, compatibility
| name | description | compatibility |
|---|---|---|
| ios-ux | iOS/SwiftUI UI & UX review and implementation guidelines for LibNovel. Enforces Apple HIG, iOS 17+ APIs, spring animations, haptics, accessibility, performance, and offline handling. Load this skill for any iOS view work. | opencode |
iOS UI/UX Skill — LibNovel
Load this skill whenever working on SwiftUI views in ios/. It defines design standards, review process for screenshots, and implementation rules.
Screenshot Review Process
When the user provides a screenshot of the app:
- Analyze first — identify specific UI/UX issues across these categories:
- Visual hierarchy and spacing
- Typography (size, weight, contrast)
- Color and material usage
- Animation and interactivity gaps
- Accessibility problems
- Deprecated or non-native patterns
- Present a numbered list of suggested improvements with brief rationale for each.
- Ask for confirmation before writing any code: "Should I apply all of these, or only specific ones?"
- Apply only what the user confirms.
Design System
Colors & Materials
- Accent:
Color.amber(project-defined). Use for active state, selection indicators, progress fills, and CTAs. - Backgrounds: Prefer
.regularMaterial,.ultraThinMaterial, or.thinMaterialover hard-codedColor.black.opacity(x)orColor(.systemBackground). - Dark overlays (e.g. full-screen players): Use
KFImageblurred background +Color.black.opacity(0.5–0.6)overlay. Never use a flat solid black background. - Semantic colors: Use
.primary,.secondary,.tertiaryforeground styles. Avoid hard-codedColor.whiteexcept on dark material contexts (full-screen player). - No hardcoded color literals — use
Color+App.swiftextensions or system semantic colors.
Typography
- Use the SF Pro system font via
.font(.title),.font(.body), etc. — never hardcode font names except for intentional stylistic accents (e.g. "Snell Roundhand" for voice watermark). - Apply
.fontWeight()and.fontDesign()modifiers rather than custom font families. - Support Dynamic Type — never hardcode a fixed font size as the sole option without a
.minimumScaleFactoror system font size modifier. - Hierarchy: title3.bold for primary labels, subheadline for secondary, caption/caption2 for metadata.
Spacing & Layout
- Minimum touch target: 44×44 pt. Use
.frame(minWidth: 44, minHeight: 44)or.contentShape(Rectangle())on small icons. - Prefer 16–20 pt horizontal padding on full-width containers; 12 pt for compact inner elements.
- Use
VStack(spacing:)andHStack(spacing:)explicitly — never rely on default spacing for production UI. - Corner radii: 12–14 pt for cards/chips, 10 pt for small badges, 20–24 pt for large cover art.
Animation Rules
Spring Animations (default for all interactive transitions)
- Use
.spring(response:dampingFraction:)for state-driven layout changes, selection feedback, and appear/disappear transitions. - Recommended defaults:
- Interactive elements:
response: 0.3, dampingFraction: 0.7 - Entrance animations:
response: 0.45–0.5, dampingFraction: 0.7 - Quick snappy feedback:
response: 0.2, dampingFraction: 0.6
- Interactive elements:
- Reserve
.easeInOutonly for non-interactive, ambient animations (e.g. opacity pulses, generating overlays).
SF Symbol Transitions
- Always use
contentTransition(.symbolEffect(.replace.downUp))when a symbol name changes based on state (play/pause, checkmark/circle, etc.). - Use
.symbolEffect(.variableColor.cumulative)for continuous animations (waveform, loading indicators). - Use
.symbolEffect(.bounce)for one-shot entrance emphasis (e.g. completion checkmark appearing). - Use
.symbolEffect(.pulse)for error/warning states that need attention.
Repeating Animations
- Use
phaseAnimatorfor any looping animation that previously used manual@State+withAnimationchains. - Do not use
Timerpublishers for UI animation — preferphaseAnimatororTimelineView.
Haptic Feedback
Add UIImpactFeedbackGenerator to every user-initiated interactive control:
.light— toggle switches, selection chips, secondary actions, slider drag start..medium— primary transport buttons (play/pause, chapter skip), significant confirmations..heavy— destructive actions (only if no confirmation dialog).
Pattern:
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
// action
} label: { ... }
Do not add haptics to:
- Programmatic state changes not directly triggered by a tap.
- Buttons inside
Listrows that already use swipe actions. - Scroll events.
iOS 17+ API Usage
Flag and replace any of the following deprecated patterns:
| Deprecated | Replace with |
|---|---|
NavigationView |
NavigationStack |
@StateObject / ObservableObject (new types only) |
@Observable macro |
DispatchQueue.main.async |
await MainActor.run or @MainActor |
Manual @State animation chains for repeating loops |
phaseAnimator |
.animation(_:) without value: |
.animation(_:value:) |
AnyView wrapping for conditional content |
@ViewBuilder + Group |
Do not refactor existing ObservableObject types to @Observable unless explicitly asked — only apply @Observable to new types.
Accessibility
Every view must:
- Support VoiceOver: add
.accessibilityLabel()to icon-only buttons and image views. - Support Dynamic Type: test that text doesn't truncate at xxxLarge without a layout adjustment.
- Meet contrast ratio: text on tinted backgrounds must be legible — avoid
.opacity(0.25)or lower for any user-readable text. - Touch targets ≥ 44pt (see Spacing above).
- Interactive controls must have
.accessibilityAddTraits(.isButton)if not usingButton. - Do not rely solely on color to convey state — pair color with icon or label.
Performance
- Isolate high-frequency observers: Any view that observes a
PlaybackProgress(timer-tick updates) must be a separate sub-view that@ObservedObject-observes only the progress object — not the parent view. This prevents the entire parent from re-rendering every 0.5 seconds. - Avoid
id()overuse: Only use.id()to force view recreation when necessary (e.g. background image on track change). PreferonChange(of:)for side effects. - Lazy containers: Use
LazyVStack/LazyHStackinsideScrollViewfor lists of 20+ items.Listis inherently lazy and does not need this. - Image loading: Always use
KFImage(Kingfisher) with.placeholderfor remote images. Never useAsyncImagefor cover art — it has no disk cache. - Avoid
AnyView: It breaks structural identity and hurts diffing. Use@ViewBuilderorGroup { }instead.
Offline & Error States
Every view that makes network calls must:
- Wrap the body in a
VStackwithOfflineBannerat the top, gated onnetworkMonitor.isConnected. - Suppress network errors silently when offline via
ErrorAlertModifier— do not show an alert when the device is offline. - Gate
.task/.onAppearnetwork calls:guard networkMonitor.isConnected else { return }. - Show a non-blocking inline empty state (not a full-screen error) for failed loads when online.
Component Checklist (before submitting any view change)
- All interactive elements ≥ 44pt touch target
- SF Symbol state changes use
contentTransition(.symbolEffect(...)) - State-driven layout transitions use
.spring(response:dampingFraction:) - Tappable controls have haptic feedback
- No
NavigationView, noDispatchQueue.main.async, no.animation(_:)withoutvalue: - High-frequency observers are isolated sub-views
- Offline state handled with
OfflineBanner+NetworkMonitor - VoiceOver labels on icon-only buttons
- No hardcoded
Color.black/Color.white/Color(.systemBackground)where a material applies