All skills

Comprehensive iOS development guidance including Swift best practices, SwiftUI patterns, UI/UX review against HIG, and app planning. Use for iOS code review, best practices, accessibility audits, or planning new iOS apps.

  • 38 files
  • 438.7 KB
  • Updated 3 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/ios

This session only. Nothing lands on disk.

ipad-patternsinput-methods.md

≈4.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Input Methods: Keyboard, Pointer, Pencil, and Focus

Covers keyboard shortcuts, pointer/trackpad interactions, Apple Pencil support via PencilKit, and the SwiftUI focus system.

Keyboard Shortcuts

SwiftUI Keyboard Shortcuts

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            SidebarView()
        } detail: {
            DetailView()
        }
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button("New Item", systemImage: "plus") {
                    createNewItem()
                }
                .keyboardShortcut("n", modifiers: .command) // Cmd+N
            }
        }
    }
}

Common Shortcut Patterns

// Standard shortcuts users expect
Button("Save") { save() }
    .keyboardShortcut("s", modifiers: .command)          // Cmd+S

Button("Delete") { delete() }
    .keyboardShortcut(.delete, modifiers: .command)      // Cmd+Delete

Button("Find") { showSearch() }
    .keyboardShortcut("f", modifiers: .command)          // Cmd+F

Button("Select All") { selectAll() }
    .keyboardShortcut("a", modifiers: .command)          // Cmd+A

Button("Undo") { undo() }
    .keyboardShortcut("z", modifiers: .command)          // Cmd+Z

Button("Redo") { redo() }
    .keyboardShortcut("z", modifiers: [.command, .shift]) // Cmd+Shift+Z

// Default button (Return key) -- used for primary action in forms/dialogs
Button("Submit") { submit() }
    .keyboardShortcut(.defaultAction)                    // Return/Enter

// Cancel button (Escape key)
Button("Cancel") { cancel() }
    .keyboardShortcut(.cancelAction)                     // Escape

Shortcut Discoverability

When the user holds the Command key, iPadOS shows a discoverability overlay listing all available shortcuts. This works automatically for .keyboardShortcut() modifiers. Organize shortcuts with sections using menu structure:

var body: some Scene {
    WindowGroup {
        ContentView()
    }
    .commands {
        CommandGroup(after: .newItem) {
            Button("New Document") { newDocument() }
                .keyboardShortcut("n", modifiers: .command)
            Button("New Folder") { newFolder() }
                .keyboardShortcut("n", modifiers: [.command, .shift])
        }

        CommandMenu("Edit") {
            Button("Find") { find() }
                .keyboardShortcut("f", modifiers: .command)
            Button("Find and Replace") { findReplace() }
                .keyboardShortcut("f", modifiers: [.command, .option])
        }
    }
}

UIKit Keyboard Shortcuts (UIKeyCommand)

class DocumentViewController: UIViewController {
    override var keyCommands: [UIKeyCommand]? {
        return [
            UIKeyCommand(
                title: "Save",
                action: #selector(saveDocument),
                input: "s",
                modifierFlags: .command,
                discoverabilityTitle: "Save Document"
            ),
            UIKeyCommand(
                title: "New",
                action: #selector(newDocument),
                input: "n",
                modifierFlags: .command,
                discoverabilityTitle: "New Document"
            ),
            UIKeyCommand(
                title: "Close",
                action: #selector(closeDocument),
                input: "w",
                modifierFlags: .command,
                discoverabilityTitle: "Close Document"
            )
        ]
    }

    @objc func saveDocument() { /* ... */ }
    @objc func newDocument() { /* ... */ }
    @objc func closeDocument() { /* ... */ }
}

Arrow Key Navigation (UIKit)

class GridViewController: UIViewController {
    override var keyCommands: [UIKeyCommand]? {
        return [
            UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [],
                        action: #selector(moveUp)),
            UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [],
                        action: #selector(moveDown)),
            UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [],
                        action: #selector(moveLeft)),
            UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [],
                        action: #selector(moveRight)),
        ]
    }
}

Pointer Interactions

SwiftUI Hover Effects

// Automatic hover effect (highlight)
Button("Action") { performAction() }
    .hoverEffect()           // Default: .automatic

// Specific hover effects
Image(systemName: "star")
    .hoverEffect(.highlight)  // Spotlight-like highlight
    .onTapGesture { toggleFavorite() }

Text("Hoverable Text")
    .hoverEffect(.lift)       // Lifts toward user, adds shadow

// Custom hover region
RoundedRectangle(cornerRadius: 12)
    .fill(Color.blue)
    .hoverEffect(.highlight)
    .contentShape(.hoverEffect, RoundedRectangle(cornerRadius: 12))

Hover State Detection (SwiftUI)

struct HoverableCard: View {
    @State private var isHovered = false

    var body: some View {
        VStack {
            Text("Card Content")
        }
        .padding()
        .background(isHovered ? Color.blue.opacity(0.1) : Color.clear)
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .onHover { hovering in
            isHovered = hovering
        }
    }
}

UIKit Pointer Interactions

class CustomButton: UIButton, UIPointerInteractionDelegate {
    override init(frame: CGRect) {
        super.init(frame: frame)
        addInteraction(UIPointerInteraction(delegate: self))
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addInteraction(UIPointerInteraction(delegate: self))
    }

    func pointerInteraction(
        _ interaction: UIPointerInteraction,
        styleFor region: UIPointerRegion
    ) -> UIPointerStyle? {
        // Lift effect -- element lifts up with shadow
        let targetedPreview = UITargetedPreview(view: self)
        return UIPointerStyle(effect: .lift(targetedPreview))
    }
}

Custom Pointer Shapes

func pointerInteraction(
    _ interaction: UIPointerInteraction,
    styleFor region: UIPointerRegion
) -> UIPointerStyle? {
    // Beam shape for text areas
    let shape = UIPointerShape.verticalBeam(length: 20)
    return UIPointerStyle(shape: shape)
}

// Available shapes:
// .defaultPointer     -- standard arrow
// .verticalBeam       -- text cursor (I-beam)
// .horizontalBeam     -- horizontal text cursor
// .roundedRect(frame) -- custom rectangle region

Standard UIKit Controls

Standard UIKit controls (UIButton, UIBarButtonItem, UISegmentedControl, UISlider, UISwitch, UITableViewCell, UICollectionViewCell) automatically have pointer effects. Do not add custom UIPointerInteraction to these unless you need a non-standard effect.

Apple Pencil and PencilKit

Basic PencilKit Setup

import PencilKit

struct DrawingView: UIViewRepresentable {
    @Binding var drawing: PKDrawing

    func makeUIView(context: Context) -> PKCanvasView {
        let canvasView = PKCanvasView()
        canvasView.drawing = drawing
        canvasView.delegate = context.coordinator
        canvasView.drawingPolicy = .pencilOnly  // Only Pencil draws; finger scrolls
        canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
        canvasView.backgroundColor = .systemBackground

        // Show the system tool picker
        if let windowScene = canvasView.window?.windowScene {
            let toolPicker = PKToolPicker.shared(for: windowScene)
            toolPicker?.setVisible(true, forFirstResponder: canvasView)
            toolPicker?.addObserver(canvasView)
            canvasView.becomeFirstResponder()
        }

        return canvasView
    }

    func updateUIView(_ canvasView: PKCanvasView, context: Context) {
        if canvasView.drawing != drawing {
            canvasView.drawing = drawing
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PKCanvasViewDelegate {
        let parent: DrawingView

        init(_ parent: DrawingView) {
            self.parent = parent
        }

        func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
            parent.drawing = canvasView.drawing
        }
    }
}

Drawing Policy

// Drawing policy controls what input triggers drawing
canvasView.drawingPolicy = .pencilOnly   // Pencil draws, finger scrolls/gestures
canvasView.drawingPolicy = .anyInput     // Both pencil and finger draw
canvasView.drawingPolicy = .default      // System default (pencilOnly on iPad with Pencil)

Touch Type Filtering (Non-PencilKit Views)

For custom drawing or annotation views that do not use PencilKit, filter touch types:

class CustomDrawingView: UIView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        switch touch.type {
        case .pencil:
            // Handle pencil input for drawing
            startDrawing(at: touch.location(in: self))
        case .direct, .indirect:
            // Handle finger/trackpad for navigation
            startPanning(at: touch.location(in: self))
        @unknown default:
            break
        }
    }
}

Extracting Drawing Data

// Save drawing as data
let drawingData = drawing.dataRepresentation()

// Load drawing from data
if let restored = try? PKDrawing(data: drawingData) {
    canvasView.drawing = restored
}

// Generate image from drawing
let image = drawing.image(from: drawing.bounds, scale: UIScreen.main.scale)

// Access individual strokes
for stroke in drawing.strokes {
    let ink = stroke.ink        // Ink type and color
    let path = stroke.path      // Array of PKStrokePoint
    let bounds = stroke.renderBounds
}

UIPencilInteraction (Double Tap)

Handle Apple Pencil double-tap gesture (Pencil 2nd generation):

class DrawingViewController: UIViewController, UIPencilInteractionDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        let pencilInteraction = UIPencilInteraction()
        pencilInteraction.delegate = self
        view.addInteraction(pencilInteraction)
    }

    func pencilInteractionDidTap(_ interaction: UIPencilInteraction) {
        // Respond to the user's preferred action
        switch UIPencilInteraction.preferredTapAction {
        case .switchEraser:
            toggleEraser()
        case .showColorPalette:
            showColorPicker()
        case .switchPrevious:
            switchToPreviousTool()
        case .showInkAttributes:
            showInkSettings()
        case .ignore:
            break
        @unknown default:
            break
        }
    }
}

UIIndirectScribbleInteraction

Scribble lets users write with Apple Pencil into text fields. Standard UITextField and UITextView support it automatically. For custom text input views:

class CustomTextView: UIView, UIIndirectScribbleInteractionDelegate {
    override init(frame: CGRect) {
        super.init(frame: frame)
        let scribbleInteraction = UIIndirectScribbleInteraction(delegate: self)
        addInteraction(scribbleInteraction)
    }

    required init?(coder: NSCoder) { fatalError() }

    func indirectScribbleInteraction(
        _ interaction: UIIndirectScribbleInteraction,
        isElementFocused elementIdentifier: String
    ) -> Bool {
        return elementIdentifier == focusedElementID
    }

    func indirectScribbleInteraction(
        _ interaction: UIIndirectScribbleInteraction,
        focusElementIfNeeded elementIdentifier: String,
        referencePoint point: CGPoint,
        completion: @escaping ((UIResponder & UITextInput)?) -> Void
    ) {
        focusElement(elementIdentifier)
        completion(textInputResponder)
    }

    func indirectScribbleInteraction(
        _ interaction: UIIndirectScribbleInteraction,
        requestElementsIn rect: CGRect,
        completion: @escaping ([String]) -> Void
    ) {
        let elements = textElements(in: rect).map(\.identifier)
        completion(elements)
    }
}

Focus System

SwiftUI Focus

struct SearchableList: View {
    @FocusState private var isSearchFocused: Bool
    @State private var searchText = ""

    var body: some View {
        VStack {
            TextField("Search", text: $searchText)
                .focused($isSearchFocused)

            List(filteredItems) { item in
                ItemRow(item: item)
            }
        }
        .onAppear {
            isSearchFocused = true // Auto-focus search field
        }
        .keyboardShortcut("f", modifiers: .command) // Cmd+F focuses search
    }
}

Focus with Enum

enum FormField: Hashable {
    case title
    case description
    case tags
}

struct FormView: View {
    @FocusState private var focusedField: FormField?
    @State private var title = ""
    @State private var description = ""
    @State private var tags = ""

    var body: some View {
        Form {
            TextField("Title", text: $title)
                .focused($focusedField, equals: .title)

            TextField("Description", text: $description)
                .focused($focusedField, equals: .description)

            TextField("Tags", text: $tags)
                .focused($focusedField, equals: .tags)
        }
        .onSubmit {
            // Move focus to next field
            switch focusedField {
            case .title: focusedField = .description
            case .description: focusedField = .tags
            case .tags: focusedField = nil
            case nil: break
            }
        }
    }
}

Focusable Views (Non-Text)

struct FocusableCard: View {
    @FocusState private var isFocused: Bool

    var body: some View {
        VStack {
            Text("Card")
        }
        .focusable()
        .focused($isFocused)
        .focusEffectDisabled()  // Disable default focus ring if custom styling
        .border(isFocused ? Color.blue : Color.clear, width: 2)
        .onKeyPress(.return) {
            activateCard()
            return .handled
        }
    }
}

FocusedValue for Cross-View Communication

// Define focused value key
struct FocusedDocumentKey: FocusedValueKey {
    typealias Value = Document
}

extension FocusedValues {
    var focusedDocument: Document? {
        get { self[FocusedDocumentKey.self] }
        set { self[FocusedDocumentKey.self] = newValue }
    }
}

// Set from the focused view
struct DocumentView: View {
    let document: Document

    var body: some View {
        content
            .focusedValue(\.focusedDocument, document)
    }
}

// Read from commands/menus
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(after: .pasteboard) {
                Button("Export Document") {
                    // Uses focused document from whichever window is focused
                }
                .keyboardShortcut("e", modifiers: .command)
            }
        }
    }
}

Common Mistakes

No Keyboard Shortcuts for Primary Actions

// ❌ Wrong -- no shortcuts; keyboard users must tap screen
Button("New") { createNew() }
Button("Delete") { deleteSelected() }

// ✅ Right -- discoverable shortcuts for primary actions
Button("New") { createNew() }
    .keyboardShortcut("n", modifiers: .command)
Button("Delete") { deleteSelected() }
    .keyboardShortcut(.delete, modifiers: .command)

Missing Hover Effects

// ❌ Wrong -- no visual feedback with trackpad/mouse
Button("Action") { doSomething() }

// ✅ Right -- pointer gets hover effect
Button("Action") { doSomething() }
    .hoverEffect()

Note: standard SwiftUI Button already has basic pointer behavior. Add .hoverEffect() to custom interactive elements built with .onTapGesture.

Not Respecting Pencil Double-Tap Preference

// ❌ Wrong -- hardcoded double-tap behavior
func pencilInteractionDidTap(_ interaction: UIPencilInteraction) {
    toggleEraser()  // Ignores user preference
}

// ✅ Right -- check user's preferred action
func pencilInteractionDidTap(_ interaction: UIPencilInteraction) {
    switch UIPencilInteraction.preferredTapAction {
    case .switchEraser: toggleEraser()
    case .showColorPalette: showColorPicker()
    case .switchPrevious: switchToPreviousTool()
    case .showInkAttributes: showInkSettings()
    case .ignore: break
    @unknown default: break
    }
}

Checklist

  • Primary actions have .keyboardShortcut() modifiers
  • Shortcuts follow platform conventions (Cmd+S save, Cmd+N new, Cmd+Z undo)
  • Custom interactive elements have .hoverEffect()
  • PencilKit drawing policy is .pencilOnly (not .anyInput) unless intentional
  • Pencil double-tap respects UIPencilInteraction.preferredTapAction
  • Focus management enables Tab/Shift+Tab navigation through form fields
  • @FocusState used for programmatic focus control
  • Context menus on relevant items (also serve right-click on pointer devices)
  • Arrow key navigation for grid/list views with keyboard
  • Scribble works in custom text input views (UIIndirectScribbleInteraction)

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 32566aa. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files Network
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 4 allowed tools
ReadGlobGrepWebFetch

README badge

README badge for rshankras/claude-code-apple-skills/ios