Platform Copy Paste

Status: ready

Symptom

The iPhone screen ships to iPad, Mac or Catalyst unchanged: same gestures, no menu commands, no keyboard path, no table semantics.

Action:          reorder an item in a saved list
iPhone:          drag handle, works
iPad + keyboard: no command
Mac Catalyst:    no menu item, no shortcut

The shared code compiles. The task doesn't travel.

Where it shows up

Catalyst apps, iPad sidebars, Mac ports, shared SwiftUI components, list-detail screens, transaction tables, order history, media libraries.

Why it blocks the task

Each platform has its own input. A Mac user expects the menu bar, keyboard shortcuts, a visible focus ring and tables that VoiceOver reads by row and column. An iPad user with a Magic Keyboard expects Tab to move focus and ⌘ shortcuts to work. A swipe-only action is unreachable from a keyboard. VoiceOver on Mac is a different review from VoiceOver on iPhone.

Root cause

UI is shared without deciding how each platform reaches each task.

  1. The touch path is treated as universal.
  2. A swipe is the only row action.
  3. No menu commands.
  4. The keyboard focus path is incomplete.
  5. Hover carries meaning with no fallback.
  6. The focus ring is hidden for visual polish.
  7. iPad QA is counted as Mac QA.

Fix

Verdict: one task, one path per platform. Write the paths down per task, then test each one on its platform.

Why: the task stays the same, the input changes. Without a written path per platform, the Mac version inherits iPhone gestures by default.

Task:    reorder an item
iPhone:  drag + "Move up" / "Move down" accessibility actions
iPad:    Edit mode, context menu, ⌘↑ / ⌘↓
Mac:     menu command, row context menu, ⌘↑ / ⌘↓

SwiftUI menu commands go on the scene. The selected item comes in through @FocusedValue:

@main
struct LeafApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
            .commands { OrderCommands() }
    }
}

struct OrderCommands: Commands {
    @FocusedValue(\.selectedOrder) private var order

    var body: some Commands {
        CommandMenu("Order") {
            Button("Reorder") { order?.reorder() }
                .keyboardShortcut("r")
                .disabled(order == nil)
        }
    }
}

The view publishes the selection with .focusedSceneValue(\.selectedOrder, selection). Full example: Chapter 12.

A frequent action on iPad also gets a shortcut:

Button("Archive message") { archive(message) }
    .keyboardShortcut(.delete, modifiers: .command)

Data that's a table on Mac is a Table, not a stack of HStacks. VoiceOver reads a Table by row and column.

What we don't do

  • We don't hide the focus ring for a cleaner look.
  • We don't count an iPad check as a Mac check.

Check

Profile:   iPad with keyboard, Full Keyboard Access on
Task:      archive a message, reorder an item, open details
Expected:  Tab reaches each row and action
           each action works without touch
           the focus ring is visible
Profile:   Mac, VoiceOver (⌘F5)
Task:      reorder from order history
Expected:  "Reorder" is in the menu bar and works with ⌘R
           rows are read with their values
Profile:   iPad Split View, narrow and wide
Task:      the same flow
Expected:  no action disappears in the narrow layout

WCAG mapping

  1. 2.1.1 Keyboard: every task works without a pointer gesture.
  2. 2.4.3 Focus Order: focus order holds across layouts.
  3. 2.4.7 Focus Visible: the focus indicator is visible.
  4. 4.1.2 Name, Role, Value: platform controls expose role and state.
  1. Platform Interaction Contract
  2. Gesture Alternative
  1. Chapter 12
  2. Chapter 16
  3. Chapter 18

Sources

  1. Apple Developer: Commands
  2. Apple Developer: FocusedValue
  3. Apple Developer: keyboardShortcut
  4. Apple Developer: Table
  5. Apple Human Interface Guidelines: The menu bar