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-patternsmultitasking.md

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

Multitasking, Multi-Window, and Adaptive Layout

Covers Stage Manager, multiple windows via UIScene, Split View and Slide Over adaptation, size classes, NavigationSplitView column configuration, and external display support.

UIScene Lifecycle

iPadOS 13+ uses the UIScene architecture. Each window is backed by a UISceneSession with a UIScene (specifically UIWindowScene).

Scene Configuration (UIKit)

In Info.plist, declare scene configurations:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>

Set UIApplicationSupportsMultipleScenes to true to enable multi-window.

Scene Delegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let windowScene = scene as? UIWindowScene else { return }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = MainViewController()
        window?.makeKeyAndVisible()

        // Restore state from user activity
        if let activity = connectionOptions.userActivities.first
            ?? session.stateRestorationActivity {
            restoreState(from: activity)
        }
    }

    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        // Return activity representing current state for restoration
        let activity = NSUserActivity(activityType: "com.app.document")
        activity.userInfo = ["documentID": currentDocumentID]
        return activity
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Scene released by system -- clean up resources
        // Do NOT delete user data here; the scene may reconnect
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Scene moved to foreground and is interactive
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Scene is about to move out of foreground
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Save state; scene may be disconnected next
    }
}

Scene Lifecycle States

Not Running --> Foreground Inactive --> Foreground Active
                       |                       |
                       v                       v
                  Background <-----------------+
                       |
                       v
                  Suspended (may be disconnected)

Key point: each scene transitions independently. One scene can be active while another is in the background.

Requesting New Windows (UIKit)

// Open a new window for a document
func openNewWindow(for document: Document) {
    let activity = NSUserActivity(activityType: "com.app.document")
    activity.userInfo = ["documentID": document.id.uuidString]

    UIApplication.shared.requestSceneSessionActivation(
        nil,                    // nil = create new session
        userActivity: activity, // Pass data to the new scene
        options: nil,
        errorHandler: { error in
            print("Failed to open window: \(error)")
        }
    )
}

// Close/destroy a scene session
func closeWindow(session: UISceneSession) {
    UIApplication.shared.requestSceneSessionDestruction(
        session,
        options: nil,
        errorHandler: nil
    )
}

Handling the Activity in Scene Delegate

func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
) {
    if let activity = connectionOptions.userActivities.first {
        if let documentID = activity.userInfo?["documentID"] as? String {
            showDocument(withID: documentID)
        }
    }
}

Multi-Window in SwiftUI

Basic Multi-Window Support

@main
struct MyApp: App {
    var body: some Scene {
        // Primary window group -- supports multiple instances
        WindowGroup {
            ContentView()
        }

        // Additional window type for a specific purpose
        WindowGroup("Document", id: "document", for: UUID.self) { $documentID in
            if let documentID {
                DocumentView(documentID: documentID)
            }
        }
    }
}

Opening Windows Programmatically

struct ContentView: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open Document") {
            openWindow(id: "document", value: document.id)
        }
    }
}

Handling External Events

WindowGroup {
    ContentView()
}
.handlesExternalEvents(matching: ["main"])

WindowGroup("Document", id: "document", for: URL.self) { $url in
    if let url {
        DocumentView(url: url)
    }
}
.handlesExternalEvents(matching: ["document"])

Stage Manager

Stage Manager (iPadOS 16+, M1 iPads) allows freely resizable windows. Apps must handle:

  1. Arbitrary window sizes -- do not assume fixed dimensions
  2. Multiple windows visible simultaneously -- each is its own scene
  3. Window chrome -- system provides title bar, resize handles

Handling Resizable Windows

// ❌ Wrong -- assuming full screen width
struct ContentView: View {
    var body: some View {
        HStack {
            sidebar
                .frame(width: 300)   // Fixed sidebar breaks at small widths
            detail
        }
    }
}

// ✅ Right -- use NavigationSplitView which handles resizing
struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            SidebarView()
        } detail: {
            DetailView()
        }
    }
}

Restricting Window Size (UIKit)

func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
) {
    guard let windowScene = scene as? UIWindowScene else { return }

    // Set minimum and maximum window size
    let restrictions = UISceneSizeRestrictions(minimumSize: CGSize(width: 400, height: 300))
    restrictions.maximumSize = CGSize(width: 1200, height: 900)

    if #available(iOS 16.0, *) {
        windowScene.sizeRestrictions?.minimumSize = CGSize(width: 400, height: 300)
        windowScene.sizeRestrictions?.maximumSize = CGSize(width: 1200, height: 900)
    }
}

Restricting Window Size (SwiftUI)

WindowGroup {
    ContentView()
}
.defaultSize(width: 800, height: 600)

Split View and Slide Over

When the user places the app in Split View (50/50 or 70/30) or Slide Over (narrow overlay), the app receives a compact or regular horizontal size class.

Size Class Detection (SwiftUI)

struct AdaptiveView: View {
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass

    var body: some View {
        if horizontalSizeClass == .compact {
            // Single-column layout (Slide Over, Split View narrow, iPhone)
            NavigationStack {
                ItemListView()
            }
        } else {
            // Multi-column layout (full screen, Split View wide)
            NavigationSplitView {
                ItemListView()
            } detail: {
                DetailView()
            }
        }
    }
}

Size Class Detection (UIKit)

class AdaptiveViewController: UIViewController {
    override func traitCollectionDidChange(
        _ previousTraitCollection: UITraitCollection?
    ) {
        super.traitCollectionDidChange(previousTraitCollection)

        if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
            updateLayout()
        }
    }

    // iOS 17+: Modern trait change registration
    func setupTraitObservation() {
        registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: Self, _) in
            self.updateLayout()
        }
    }

    private func updateLayout() {
        if traitCollection.horizontalSizeClass == .compact {
            showSingleColumnLayout()
        } else {
            showMultiColumnLayout()
        }
    }
}

NavigationSplitView Column Configuration

struct ContentView: View {
    @State private var columnVisibility: NavigationSplitViewVisibility = .all

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            SidebarView()
                .navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 350)
        } content: {
            ContentListView()
                .navigationSplitViewColumnWidth(min: 250, ideal: 350, max: 500)
        } detail: {
            DetailView()
                .navigationSplitViewColumnWidth(min: 300)
        }
        .navigationSplitViewStyle(.balanced) // or .prominentDetail
    }
}

Adapting Toolbar for Size Class

struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var sizeClass

    var body: some View {
        content
            .toolbar {
                if sizeClass == .regular {
                    ToolbarItem(placement: .primaryAction) {
                        Button("New", systemImage: "plus") { }
                    }
                    ToolbarItem(placement: .secondaryAction) {
                        Button("Filter", systemImage: "line.3.horizontal.decrease.circle") { }
                    }
                } else {
                    ToolbarItem(placement: .bottomBar) {
                        HStack {
                            Button("New", systemImage: "plus") { }
                            Spacer()
                            Button("Filter", systemImage: "line.3.horizontal.decrease.circle") { }
                        }
                    }
                }
            }
    }
}

External Display Support

Modern Approach (SwiftUI, iPadOS 16+)

Use a dedicated WindowGroup for external displays:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }

        // This WindowGroup can be placed on an external display
        WindowGroup("Presentation", id: "presentation") {
            PresentationView()
        }
    }
}

Legacy Approach (UIKit)

class ExternalDisplayManager {
    private var externalWindow: UIWindow?

    func startObservingScreens() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(screenDidConnect),
            name: UIScreen.didConnectNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(screenDidDisconnect),
            name: UIScreen.didDisconnectNotification,
            object: nil
        )
    }

    @objc private func screenDidConnect(_ notification: Notification) {
        guard let screen = notification.object as? UIScreen else { return }

        let window = UIWindow(frame: screen.bounds)
        window.screen = screen
        window.rootViewController = ExternalDisplayViewController()
        window.makeKeyAndVisible()
        externalWindow = window
    }

    @objc private func screenDidDisconnect(_ notification: Notification) {
        externalWindow?.isHidden = true
        externalWindow = nil
    }
}

Note: UIScreen.didConnectNotification and UIScreen.didDisconnectNotification are the legacy pattern. For SwiftUI apps, prefer multiple WindowGroup scenes.

Common Mistakes

Not Setting UIApplicationSupportsMultipleScenes

// ❌ Wrong -- Info.plist has UIApplicationSupportsMultipleScenes = false
// App only ever has one window, Expose shows no multi-window option

// ✅ Right -- set to true in Info.plist
// <key>UIApplicationSupportsMultipleScenes</key>
// <true/>

Storing State in AppDelegate Instead of Per-Scene

// ❌ Wrong -- global state shared across all windows
class AppDelegate: UIResponder, UIApplicationDelegate {
    var currentDocument: Document?  // Which window does this belong to?
}

// ✅ Right -- state lives in scene delegate or per-scene model
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var documentState: DocumentState?  // Each scene has its own state
}

Fixed Layouts That Break in Split View

// ❌ Wrong -- assumes full screen width
.frame(width: UIScreen.main.bounds.width)

// ✅ Right -- use GeometryReader or flexible layouts
GeometryReader { geometry in
    content
        .frame(width: geometry.size.width)
}

// ✅ Even better -- use flexible SwiftUI layout without explicit widths
HStack {
    sidebar
        .frame(minWidth: 200, maxWidth: 300)
    detail
        .frame(maxWidth: .infinity)
}

Checklist

  • UIApplicationSupportsMultipleScenes is true in Info.plist
  • State is per-scene, not global in AppDelegate
  • stateRestorationActivity(for:) returns meaningful NSUserActivity
  • Scene handles userActivities in willConnectTo for restoring state
  • sceneDidDisconnect releases heavy resources without deleting user data
  • Layout adapts to horizontalSizeClass (compact vs regular)
  • No use of UIScreen.main.bounds for sizing (use GeometryReader or flexible layout)
  • NavigationSplitView uses min/ideal/max column widths
  • Toolbar items adapt placement for compact vs regular size class
  • External display content is handled via WindowGroup or screen notifications

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