Files
libnovel/.opencode/skills/ios-ux/SKILL.md
Admin 7413313100
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
fix: update integration_test.go to match server.New signature (version, commit args)
2026-03-14 14:25:46 +05:00

7.8 KiB
Raw Blame History

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:

  1. 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
  2. Present a numbered list of suggested improvements with brief rationale for each.
  3. Ask for confirmation before writing any code: "Should I apply all of these, or only specific ones?"
  4. 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 .thinMaterial over hard-coded Color.black.opacity(x) or Color(.systemBackground).
  • Dark overlays (e.g. full-screen players): Use KFImage blurred background + Color.black.opacity(0.50.6) overlay. Never use a flat solid black background.
  • Semantic colors: Use .primary, .secondary, .tertiary foreground styles. Avoid hard-coded Color.white except on dark material contexts (full-screen player).
  • No hardcoded color literals — use Color+App.swift extensions 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 .minimumScaleFactor or 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 1620 pt horizontal padding on full-width containers; 12 pt for compact inner elements.
  • Use VStack(spacing:) and HStack(spacing:) explicitly — never rely on default spacing for production UI.
  • Corner radii: 1214 pt for cards/chips, 10 pt for small badges, 2024 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.450.5, dampingFraction: 0.7
    • Quick snappy feedback: response: 0.2, dampingFraction: 0.6
  • Reserve .easeInOut only 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 phaseAnimator for any looping animation that previously used manual @State + withAnimation chains.
  • Do not use Timer publishers for UI animation — prefer phaseAnimator or TimelineView.

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 List rows 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 using Button.
  • 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). Prefer onChange(of:) for side effects.
  • Lazy containers: Use LazyVStack / LazyHStack inside ScrollView for lists of 20+ items. List is inherently lazy and does not need this.
  • Image loading: Always use KFImage (Kingfisher) with .placeholder for remote images. Never use AsyncImage for cover art — it has no disk cache.
  • Avoid AnyView: It breaks structural identity and hurts diffing. Use @ViewBuilder or Group { } instead.

Offline & Error States

Every view that makes network calls must:

  1. Wrap the body in a VStack with OfflineBanner at the top, gated on networkMonitor.isConnected.
  2. Suppress network errors silently when offline via ErrorAlertModifier — do not show an alert when the device is offline.
  3. Gate .task / .onAppear network calls: guard networkMonitor.isConnected else { return }.
  4. 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, no DispatchQueue.main.async, no .animation(_:) without value:
  • 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