CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

focus-patterns.mdskills/focus-engine/references/

Focus Patterns Reference

Contents

  • FocusState patterns
  • Default focus
  • Focused values and scene values
  • Focusable custom views
  • Focus sections for directional movement (macOS/tvOS)
  • Focus restoration after presentations
  • UIKit focus guides
  • Common mistakes checklist

FocusState patterns

struct CheckoutForm: View {
    enum Field: Hashable { case address, city, postalCode }

    @State private var address = ""
    @State private var city = ""
    @State private var postalCode = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            TextField("Address", text: $address)
                .focused($focusedField, equals: .address)
                .onSubmit { focusedField = .city }

            TextField("City", text: $city)
                .focused($focusedField, equals: .city)
                .onSubmit { focusedField = .postalCode }

            TextField("Postal Code", text: $postalCode)
                .focused($focusedField, equals: .postalCode)
        }
        .onAppear { focusedField = .address }
    }
}

Use Bool focus state only for one-off cases. Prefer a Hashable enum when more than one control can be focused.

Default focus

struct CommandPaletteView: View {
    enum Target: Hashable { case search }
    @FocusState private var target: Target?

    var body: some View {
        VStack {
            TextField("Search commands", text: $query)
                .focused($target, equals: .search)
        }
        .defaultFocus($target, .search)
    }
}

Choose one unambiguous default target. Competing defaults make focus feel unstable.

Focused values and scene values

struct SelectedDocumentKey: FocusedValueKey {
    typealias Value = Binding<Document>
}

extension FocusedValues {
    var selectedDocument: Binding<Document>? {
        get { self[SelectedDocumentKey.self] }
        set { self[SelectedDocumentKey.self] = newValue }
    }
}

struct DocumentEditor: View {
    @Binding var document: Document

    var body: some View {
        TextEditor(text: $document.body)
            .focusedSceneValue(\.selectedDocument, $document)
    }
}

Use focused scene values when command menus, toolbar actions, or scene-level controls need access to the current focused content.

Focusable custom views

struct TVCardButton: View {
    let title: String
    let action: () -> Void
    @FocusState private var isFocused: Bool

    var body: some View {
        Button(action: action) {
            RoundedRectangle(cornerRadius: 16)
                .fill(isFocused ? Color.accentColor.opacity(0.2) : Color.secondary.opacity(0.12))
                .overlay { Text(title) }
        }
        .buttonStyle(.plain)
        .focusable(interactions: .activate)
        .focused($isFocused)
        .scaleEffect(isFocused ? 1.04 : 1.0)
        .animation(.snappy, value: isFocused)
    }
}

Prefer semantic controls like Button first. Add .focusable(interactions:) only when the custom control needs explicit focus participation.

Focus sections for directional movement

focusSection() is available on macOS 13+ and tvOS 15+.

struct LibraryView: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Button("Recent") { }
                Button("Favorites") { }
                Button("Downloaded") { }
            }
            .focusSection()

            LazyVGrid(columns: columns) {
                ForEach(items) { item in
                    Button(item.title) { open(item) }
                }
            }
            .focusSection()
        }
    }
}

Use focusSection() on macOS or tvOS when the user should move through one group before jumping into another.

Focus restoration after presentations

struct SearchFiltersView: View {
    @State private var isPresentingFilters = false
    @FocusState private var isFiltersButtonFocused: Bool

    var body: some View {
        Button("Filters") {
            isPresentingFilters = true
        }
        .focused($isFiltersButtonFocused)
        .sheet(isPresented: $isPresentingFilters) {
            FiltersSheet()
                .onDisappear {
                    Task { @MainActor in
                        isFiltersButtonFocused = true
                    }
                }
        }
    }
}

Restore focus to the trigger or to the next logical destination after dismissing temporary UI.

UIKit focus guides

final class PlayerViewController: UIViewController {
    private let skipGuide = UIFocusGuide()
    @IBOutlet private weak var playButton: UIButton!
    @IBOutlet private weak var nextEpisodeButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addLayoutGuide(skipGuide)
        skipGuide.preferredFocusEnvironments = [nextEpisodeButton]

        NSLayoutConstraint.activate([
            skipGuide.leadingAnchor.constraint(equalTo: playButton.trailingAnchor),
            skipGuide.trailingAnchor.constraint(equalTo: nextEpisodeButton.leadingAnchor),
            skipGuide.topAnchor.constraint(equalTo: playButton.topAnchor),
            skipGuide.bottomAnchor.constraint(equalTo: playButton.bottomAnchor)
        ])
    }
}

A focus guide is an invisible layout guide. Constrain it like any other layout guide, then set preferredFocusEnvironments to the destination views.

Common mistakes checklist

  • Using shared model state to drive @FocusState
  • Forgetting to restore focus after dismissing transient UI
  • Making decorative containers focusable
  • Adding multiple defaults to the same focus region
  • Using UIKit focus guides for layouts SwiftUI can solve with focusSection()
  • Publishing scene-focused values that should only be view-local

skills

CHANGELOG.md

README.md

tile.json