swiftship/internal/skills/data/ui/scroll-patterns/SKILL.md
Scroll patterns: ScrollView, scrollPosition, parallax, paging. Use when implementing UI patterns related to scrolling.
npx skillsauth add abdullah4ai/apple-developer-toolkit scroll-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use .scrollIndicators(.hidden) modifier instead of initializer parameter.
// Modern (Correct)
ScrollView {
content
}
.scrollIndicators(.hidden)
// Legacy (Avoid)
ScrollView(showsIndicators: false) {
content
}
Use ScrollViewReader for scroll-to-top, scroll-to-bottom, and anchor-based jumps.
struct ChatView: View {
@State private var messages: [Message] = []
private let bottomID = "bottom"
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
.id(message.id)
}
Color.clear
.frame(height: 1)
.id(bottomID)
}
}
.onChange(of: messages.count) { _, _ in
withAnimation {
proxy.scrollTo(bottomID, anchor: .bottom)
}
}
.onAppear {
proxy.scrollTo(bottomID, anchor: .bottom)
}
}
}
}
struct FeedView: View {
@State private var items: [Item] = []
@State private var scrollToTop = false
private let topID = "top"
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
Color.clear
.frame(height: 1)
.id(topID)
ForEach(items) { item in
ItemRow(item: item)
}
}
}
.onChange(of: scrollToTop) { _, shouldScroll in
if shouldScroll {
withAnimation {
proxy.scrollTo(topID, anchor: .top)
}
scrollToTop = false
}
}
}
}
}
Why: ScrollViewReader provides programmatic scroll control with stable anchors. Always use stable IDs and explicit animations.
Avoid - Storing scroll position directly triggers view updates on every scroll frame:
// ❌ Bad Practice - causes unnecessary re-renders
struct ContentView: View {
@State private var scrollPosition: CGFloat = 0
var body: some View {
ScrollView {
content
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scroll")).minY
)
}
)
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollPosition = value
}
}
}
Preferred - Check scroll position and update a flag based on thresholds for smoother, more efficient scrolling:
// ✅ Good Practice - only updates state when crossing threshold
struct ContentView: View {
@State private var startAnimation: Bool = false
var body: some View {
ScrollView {
content
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scroll")).minY
)
}
)
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
if value < -100 {
startAnimation = true
} else {
startAnimation = false
}
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
@State private var showHeader = true
var body: some View {
VStack(spacing: 0) {
if showHeader {
HeaderView()
.transition(.move(edge: .top))
}
ScrollView {
content
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scroll")).minY
)
}
)
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
if offset < -50 { // Scrolling down
withAnimation { showHeader = false }
} else if offset > 50 { // Scrolling up
withAnimation { showHeader = true }
}
}
}
}
}
iOS 17+: All APIs in this section require iOS 17 or later.
struct ParallaxView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 20) {
ForEach(items) { item in
ItemCard(item: item)
.visualEffect { content, geometry in
let frame = geometry.frame(in: .scrollView)
let distance = min(0, frame.minY)
return content
.opacity(1 + distance / 200)
}
}
}
}
}
}
struct ParallaxHeader: View {
var body: some View {
ScrollView {
VStack(spacing: 0) {
Image("hero")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 300)
.visualEffect { content, geometry in
let offset = geometry.frame(in: .scrollView).minY
return content
.offset(y: offset > 0 ? -offset * 0.5 : 0)
}
.clipped()
ContentView()
}
}
}
}
iOS 17+: All APIs in this section require iOS 17 or later.
struct PagingView: View {
var body: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 0) {
ForEach(pages) { page in
PageView(page: page)
.containerRelativeFrame(.horizontal)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
}
struct SnapScrollView: View {
var body: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 16) {
ForEach(items) { item in
ItemCard(item: item)
.frame(width: 280)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.contentMargins(.horizontal, 20)
}
}
.scrollIndicators(.hidden) instead of initializer parameterScrollViewReader with stable IDs for programmatic scrollingscrollTo().visualEffect for scroll-based visual changes.scrollTargetBehavior(.paging) for paging behavior.scrollTargetBehavior(.viewAligned) for snap-to-item behaviortools
Apple platform skill for docs, WWDC lookup, App Store Connect work, and SwiftUI app generation. Use repo-local `node cli.js` for Apple docs and WWDC search, `appledev store` for App Store Connect workflows, and `appledev build` for app scaffolding or fix loops on macOS. USE WHEN: Apple APIs, WWDC sessions, TestFlight/App Store tasks, or building/fixing Apple-platform apps. DON'T USE WHEN: non-Apple platforms, generic backend work, or general web research. EDGE CASES: docs-only queries use `node cli.js` in this repo, not `appledev`; release workflows use `appledev store`; app scaffolding uses `appledev build`; rules-only requests can read `references/ios-rules/` or `references/swiftui-guides/` progressively without invoking binaries.
tools
All-in-one Apple developer skill with three integrated tools shipped as a single unified binary. (1) Documentation search across Apple frameworks, symbols, and 1,267 WWDC sessions from 2014-2025. No credentials needed. (2) App Store Connect CLI with 120+ commands covering builds (find/wait/upload), TestFlight, pre-submission validate, submissions, signing, subscriptions (family-sharable), IAP, analytics, Xcode Cloud, metadata workflows, release pipeline dashboard, insights, win-back offers, promoted purchases, product pages, nominations, accessibility declarations, pre-orders, pricing filters, localizations update, diff, webhooks with local receiver, workflow automation, and more. Requires App Store Connect API key. (3) Multi-platform app builder (iOS/watchOS/tvOS/iPad/macOS/visionOS) that generates complete Swift/SwiftUI apps from natural language with auto-fix, simulator launch, interactive chat mode, and open-in-Xcode. Requires an LLM API key and Xcode. Includes 38 iOS development rules and 12 SwiftUI best practice guides for Liquid Glass, navigation, state management, and modern APIs. All three tools ship as one binary (appledev). USE WHEN: Apple API docs, App Store Connect management, WWDC lookup, or building iOS/watchOS/tvOS/macOS/visionOS apps from scratch. DON'T USE WHEN: non-Apple platforms or general coding.
testing
watchOS complications: WidgetKit complication families, accessory sizes, timeline providers for watch face. Use when implementing watchOS-specific patterns related to widgets.
development
watchOS haptic feedback: WKInterfaceDevice preset haptic types for wrist-based feedback. Use when implementing watchOS-specific patterns related to haptics.