Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Extended patterns for SwiftUI gesture handling. See the main SKILL.md for
core APIs and common mistakes.
Full implementation with clamped scale, double-tap reset, and smooth animation:
struct PinchToZoomView: View {
@State private var currentScale = 1.0
@State private var currentOffset = CGSize.zero
@GestureState private var gestureScale = 1.0
@GestureState private var dragOffset = CGSize.zero
private let minScale = 0.5
private let maxScale = 5.0
var body: some View {
Image("photo")
.resizable()
.scaledToFit()
.scaleEffect(currentScale * gestureScale)
.offset(
x: currentOffset.width + dragOffset.width,
y: currentOffset.height + dragOffset.height
)
.gesture(magnifyGesture)
.simultaneousGesture(panGesture)
.onTapGesture(count: 2) {
withAnimation(.spring) {
currentScale = 1.0
currentOffset = .zero
}
}
}
private var magnifyGesture: some Gesture {
MagnifyGesture()
.updating($gestureScale) { value, state, _ in
state = value.magnification
}
.onEnded { value in
let newScale = currentScale * value.magnification
withAnimation(.spring) {
currentScale = min(max(newScale, minScale), maxScale)
}
}
}
private var panGesture: some Gesture {
DragGesture()
.updating($dragOffset) { value, state, _ in
guard currentScale > 1.0 else { return }
state = value.translation
}
.onEnded { value in
guard currentScale > 1.0 else { return }
currentOffset.width += value.translation.width
currentOffset.height += value.translation.height
}
}
}Simultaneous rotation and magnification for image editing:
struct RotateScaleView: View {
@State private var currentAngle = Angle.zero
@State private var currentScale = 1.0
@GestureState private var gestureAngle = Angle.zero
@GestureState private var gestureScale = 1.0
var body: some View {
Image("sticker")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.rotationEffect(currentAngle + gestureAngle)
.scaleEffect(currentScale * gestureScale)
.gesture(
RotateGesture()
.updating($gestureAngle) { value, state, _ in
state = value.rotation
}
.onEnded { value in
currentAngle += value.rotation
}
.simultaneously(with:
MagnifyGesture()
.updating($gestureScale) { value, state, _ in
state = value.magnification
}
.onEnded { value in
currentScale *= value.magnification
currentScale = min(max(currentScale, 0.3), 5.0)
}
)
)
}
}Drag gesture with haptic feedback for list reordering:
struct ReorderableList: View {
@State private var items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
@State private var draggingItem: String?
@State private var dragOffset = CGSize.zero
var body: some View {
VStack {
ForEach(items, id: \.self) { item in
ItemRow(title: item, isDragging: draggingItem == item)
.offset(y: draggingItem == item ? dragOffset.height : 0)
.zIndex(draggingItem == item ? 1 : 0)
.gesture(
LongPressGesture(minimumDuration: 0.3)
.sequenced(before: DragGesture())
.onChanged { value in
switch value {
case .first(true):
withAnimation(.spring) {
draggingItem = item
}
case .second(true, let drag):
dragOffset = drag?.translation ?? .zero
updateOrder(for: item, translation: dragOffset.height)
default:
break
}
}
.onEnded { _ in
withAnimation(.spring) {
draggingItem = nil
dragOffset = .zero
}
}
)
}
}
}
private func updateOrder(for item: String, translation: CGFloat) {
guard let sourceIndex = items.firstIndex(of: item) else { return }
let rowHeight: CGFloat = 50
let offset = Int(translation / rowHeight)
let destinationIndex = min(max(sourceIndex + offset, 0), items.count - 1)
if sourceIndex != destinationIndex {
withAnimation(.spring) {
items.move(
fromOffsets: IndexSet(integer: sourceIndex),
toOffset: destinationIndex > sourceIndex
? destinationIndex + 1 : destinationIndex
)
}
}
}
}
struct ItemRow: View {
let title: String
let isDragging: Bool
var body: some View {
Text(title)
.frame(maxWidth: .infinity, minHeight: 46)
.background(isDragging ? Color.blue.opacity(0.2) : Color(.secondarySystemBackground))
.clipShape(.rect(cornerRadius: 8))
.shadow(radius: isDragging ? 4 : 0)
.scaleEffect(isDragging ? 1.05 : 1.0)
}
}Use DragGesture.Value.velocity (iOS 17+) or compute from
predictedEndTranslation for flick gestures:
struct FlickDismissView: View {
@State private var offset = CGSize.zero
@State private var isDismissed = false
private let dismissThreshold: CGFloat = 200
private let velocityThreshold: CGFloat = 800
var body: some View {
if !isDismissed {
CardView()
.offset(y: offset.height)
.opacity(opacity)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.height > 0 {
offset = value.translation
}
}
.onEnded { value in
let velocity = value.velocity.height
let distance = value.translation.height
if distance > dismissThreshold
|| velocity > velocityThreshold
{
withAnimation(.spring) {
isDismissed = true
}
} else {
withAnimation(.spring) {
offset = .zero
}
}
}
)
}
}
private var opacity: Double {
let progress = min(offset.height / dismissThreshold, 1.0)
return 1.0 - (progress * 0.5)
}
}Velocity from predicted end translation (pre-iOS 17 fallback):
DragGesture()
.onEnded { value in
// Approximate velocity from predicted vs actual
let predictedDelta = CGSize(
width: value.predictedEndTranslation.width - value.translation.width,
height: value.predictedEndTranslation.height - value.translation.height
)
let speed = sqrt(
predictedDelta.width * predictedDelta.width
+ predictedDelta.height * predictedDelta.height
)
if speed > 500 { handleFlick() }
}Model complex gesture states with an enum for clarity:
enum DragState {
case inactive
case pressing
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive, .pressing: return .zero
case .dragging(let t): return t
}
}
var isActive: Bool {
switch self {
case .inactive: return false
case .pressing, .dragging: return true
}
}
}
struct LongPressDragView: View {
@GestureState private var dragState = DragState.inactive
@State private var position = CGSize.zero
var body: some View {
Circle()
.fill(dragState.isActive ? .red : .blue)
.frame(width: 80, height: 80)
.shadow(radius: dragState.isActive ? 8 : 0)
.offset(
x: position.width + dragState.translation.width,
y: position.height + dragState.translation.height
)
.animation(.spring, value: dragState.isActive)
.gesture(
LongPressGesture(minimumDuration: 0.5)
.sequenced(before: DragGesture())
.updating($dragState) { value, state, _ in
switch value {
case .first(true):
state = .pressing
case .second(true, let drag):
state = .dragging(
translation: drag?.translation ?? .zero
)
default:
state = .inactive
}
}
.onEnded { value in
guard case .second(true, let drag?) = value else {
return
}
position.width += drag.translation.width
position.height += drag.translation.height
}
)
}
}Bridge UIKit gesture recognizers into SwiftUI:
struct PinchGestureView: UIGestureRecognizerRepresentable {
@Binding var scale: CGFloat
func makeUIGestureRecognizer(context: Context) -> UIPinchGestureRecognizer {
UIPinchGestureRecognizer()
}
func handleUIGestureRecognizerAction(
_ recognizer: UIPinchGestureRecognizer,
context: Context
) {
switch recognizer.state {
case .changed:
scale = recognizer.scale
case .ended:
scale = recognizer.scale
recognizer.scale = 1.0
default:
break
}
}
}
// Usage in SwiftUI
struct ContentView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Image("photo")
.scaleEffect(scale)
.gesture(PinchGestureView(scale: $scale))
}
}class GestureHostingController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swiftUIView = InteractiveCard()
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.frame = view.bounds
hostingController.didMove(toParent: self)
}
}
struct InteractiveCard: View {
@GestureState private var dragOffset = CGSize.zero
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(.blue.gradient)
.frame(width: 200, height: 300)
.offset(dragOffset)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
)
}
}When mixing gesture recognizers, use simultaneousGesture on the SwiftUI side
and UIGestureRecognizerDelegate on the UIKit side to prevent conflicts:
// In your UIViewRepresentable coordinator
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer
) -> Bool {
true // allow both UIKit and SwiftUI gestures to fire
}Always provide accessible alternatives for gesture-driven interactions:
Image("draggable")
.offset(offset)
.gesture(dragGesture)
.accessibilityAction(.default) { showAccessibleUI() }
.accessibilityAction(named: "Move up") {
withAnimation { offset.height -= 50 }
}
.accessibilityAction(named: "Move down") {
withAnimation { offset.height += 50 }
}skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references