tvos-specific-patterns

tvOS Specific Patterns — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tvos-specific-patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-tvos-specific-patterns

tvOS Specific Patterns — Expert Decisions

Expert decision frameworks for tvOS choices. Claude knows @FocusState and SwiftUI basics — this skill provides judgment calls for focus architecture, remote handling, and TV-optimized UI trade-offs.

Decision Trees

Focus Management Strategy

How complex is your focus navigation? ├─ Linear list/grid navigation │ └─ System focus handling │ Default behavior, no custom code needed │ ├─ Multiple focus sections with priority │ └─ .focusSection() grouping │ Group related elements, system handles within │ ├─ Custom initial focus on appear │ └─ @FocusState + .onAppear │ Set focused item programmatically │ ├─ Complex conditional focus logic │ └─ .prefersDefaultFocus() + @FocusState │ Combine for sophisticated control │ └─ Focus must follow data changes └─ Bind @FocusState to data model Update focus when selection changes

The trap: Overriding focus behavior when system defaults work fine. Custom focus logic adds complexity and can break expected navigation patterns.

Custom Focus Effect Decision

Should you create custom focus effects? ├─ Standard card/button scaling │ └─ Use .buttonStyle(.card) │ Apple's built-in TV-appropriate effect │ ├─ Need subtle highlight │ └─ .isFocused environment │ Opacity/border changes only │ ├─ Complex parallax/tilt effects │ └─ Custom view with rotation3DEffect │ Only for hero content, not lists │ └─ Media-browsing app (Netflix-like) └─ Custom focus with content preview Scale + shadow + metadata reveal

Top Shelf Content Strategy

What content should appear in Top Shelf? ├─ Personalized content (continue watching) │ └─ TVTopShelfCarouselContent with .actions style │ Play and display actions per item │ ├─ Browse/discovery content │ └─ TVTopShelfSectionedContent │ Multiple categories with items │ ├─ Single featured item │ └─ TVTopShelfCarouselContent single item │ Hero image with actions │ ├─ Time-sensitive content │ └─ Update via background refresh │ Schedule updates, cache for speed │ └─ User not logged in └─ Promotional content + CTA Drive app opens and registration

Siri Remote Gesture Selection

Which gesture for this interaction? ├─ Primary action (play, select) │ └─ Tap (click center) │ .onTapGesture or Button │ ├─ Secondary action (info, options) │ └─ Long press │ Context menu or info overlay │ ├─ Content navigation (carousel) │ └─ Swipe left/right │ DragGesture with threshold │ ├─ Volume/scrubbing │ └─ Swipe up/down │ System handles in video player │ └─ Cancel/back └─ Menu button .onExitCommand handler

NEVER Do

Focus Management

NEVER fight the Focus Engine:

// ❌ Trying to manually manage all focus struct BadNavigation: View { @State private var selectedIndex = 0

var body: some View {
    HStack {
        ForEach(0..<items.count) { index in
            ItemView(item: items[index])
                .opacity(selectedIndex == index ? 1 : 0.5)  // Fake focus
        }
    }
    .onMoveCommand { direction in
        // Manual index management — reinventing Focus Engine!
        switch direction {
        case .left: selectedIndex = max(0, selectedIndex - 1)
        case .right: selectedIndex = min(items.count - 1, selectedIndex + 1)
        default: break
        }
    }
}

}

// ✅ Let Focus Engine handle it struct GoodNavigation: View { @FocusState private var focusedItem: Item.ID?

var body: some View {
    HStack {
        ForEach(items) { item in
            ItemView(item: item)
                .focusable()
                .focused($focusedItem, equals: item.id)
        }
    }
}

}

NEVER use .focusable() without visual feedback:

// ❌ User can't see what's focused Text("Invisible Focus") .focusable() // Focusable but no visual change!

// ✅ Always show focus state struct FocusableText: View { @Environment(.isFocused) var isFocused

var body: some View {
    Text("Visible Focus")
        .scaleEffect(isFocused ? 1.1 : 1.0)
        .animation(.easeInOut(duration: 0.2), value: isFocused)
        .focusable()
}

}

NEVER animate focus changes slowly:

// ❌ Laggy feel — focus must be instant .animation(.easeInOut(duration: 0.5), value: isFocused) // Too slow!

// ✅ Quick transitions (200ms or less) .animation(.easeInOut(duration: 0.2), value: isFocused)

Remote Handling

NEVER use complex gestures on tvOS:

// ❌ Siri Remote doesn't support these .gesture( MagnificationGesture() // No pinch on Siri Remote! )

.gesture( RotationGesture() // No rotation on Siri Remote! )

// ✅ Stick to supported gestures .onTapGesture { } .onLongPressGesture { } .gesture(DragGesture()) // Swipe detection

NEVER ignore the Menu button:

// ❌ User trapped in screen struct PlayerView: View { var body: some View { VideoPlayer(player: player) // No way to exit! } }

// ✅ Always handle exit struct PlayerView: View { @Environment(.dismiss) var dismiss

var body: some View {
    VideoPlayer(player: player)
        .onExitCommand {
            dismiss()
        }
}

}

UI Layout

NEVER use iOS-sized touch targets:

// ❌ Way too small for TV Button("Play") { } .frame(width: 44, height: 44) // iOS size

// ✅ TV-appropriate sizes (minimum 80pt, prefer 100+) Button("Play") { } .frame(minWidth: 200, minHeight: 80) .buttonStyle(.card)

NEVER use small text on TV:

// ❌ Unreadable from 10 feet Text("Description") .font(.caption) // ~11pt — can't read!

Text("Details") .font(.footnote) // ~13pt — still too small

// ✅ Use legible sizes Text("Description") .font(.title3) // Minimum for body text

Text("Title") .font(.system(size: 48, weight: .bold)) // Hero content

NEVER forget edge insets:

// ❌ Content touches screen edges ScrollView { LazyVGrid(columns: columns) { // Content starts at edge — looks cramped } }

// ✅ Use proper TV margins (40-60pt minimum) ScrollView { LazyVGrid(columns: columns) { ForEach(items) { item in ItemView(item: item) } } .padding(60) // TV safe area }

Top Shelf

NEVER use low-resolution images:

// ❌ Blurry on 4K TV item.setImageURL(thumbnailURL, for: .screenScale1x) // Only 1x

// ✅ Provide all resolutions item.setImageURL(url1x, for: .screenScale1x) item.setImageURL(url2x, for: .screenScale2x) // Essential for Retina

NEVER return empty Top Shelf for logged-out users:

// ❌ Wasted promotional space override func loadTopShelfContent(completionHandler: @escaping (TVTopShelfContent?) -> Void) { if !isLoggedIn { completionHandler(nil) // Empty! return } // ... }

// ✅ Show promotional content override func loadTopShelfContent(completionHandler: @escaping (TVTopShelfContent?) -> Void) { if !isLoggedIn { completionHandler(createPromotionalContent()) // Drive engagement return } // ... }

Essential Patterns

Focus-Aware Card Component

struct TVCard<Content: View>: View { @Environment(.isFocused) private var isFocused let content: () -> Content

var body: some View {
    content()
        .scaleEffect(isFocused ? 1.1 : 1.0)
        .shadow(
            color: .black.opacity(isFocused ? 0.3 : 0),
            radius: isFocused ? 20 : 0,
            y: isFocused ? 10 : 0
        )
        .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isFocused)
        .focusable()
}

}

// Usage TVCard { VStack { AsyncImage(url: movie.posterURL) { image in image.resizable().aspectRatio(contentMode: .fill) } placeholder: { Rectangle().fill(Color.gray.opacity(0.3)) } .frame(width: 300, height: 450) .cornerRadius(12)

    Text(movie.title)
        .font(.headline)
}

}

Focus Section Navigation

struct TVHomeView: View { var body: some View { ScrollView { VStack(spacing: 40) { // Hero section — gets initial focus heroSection .focusSection()

            // Continue watching — separate focus group
            sectionView(title: "Continue Watching", items: continueWatching)
                .focusSection()

            // Recommendations — separate focus group
            sectionView(title: "For You", items: recommendations)
                .focusSection()
        }
    }
}

private func sectionView(title: String, items: [Movie]) -> some View {
    VStack(alignment: .leading, spacing: 16) {
        Text(title)
            .font(.title2)
            .padding(.leading, 60)

        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 30) {
                ForEach(items) { item in
                    TVCard {
                        MoviePoster(movie: item)
                    }
                }
            }
            .padding(.horizontal, 60)
        }
    }
}

}

Top Shelf Provider

class ContentProvider: TVTopShelfContentProvider { override func loadTopShelfContent() async -> TVTopShelfContent? { do { let items = try await fetchContent() return TVTopShelfCarouselContent(style: .actions, items: items) } catch { return createFallbackContent() } }

private func fetchContent() async throws -> [TVTopShelfCarouselItem] {
    let movies = try await API.fetchFeatured()

    return movies.prefix(10).map { movie in
        let item = TVTopShelfCarouselItem(identifier: movie.id)

        item.setImageURL(movie.posterURL(size: .x1), for: .screenScale1x)
        item.setImageURL(movie.posterURL(size: .x2), for: .screenScale2x)

        item.title = movie.title
        item.subtitle = "\(movie.year) • \(movie.genre)"

        // Deep links
        item.displayAction = TVTopShelfAction(url: URL(string: "myapp://movie/\(movie.id)")!)
        item.playAction = TVTopShelfAction(url: URL(string: "myapp://play/\(movie.id)")!)

        return item
    }
}

}

Quick Reference

Focus Modifiers

Modifier Purpose

.focusable() Make view focusable

.focused($state, equals:) Bind to FocusState

.focusSection() Group related focusable items

.prefersDefaultFocus() Set preferred initial focus

.onExitCommand Handle Menu button

.onPlayPauseCommand Handle Play/Pause

.onMoveCommand Handle directional input

TV Size Guidelines

Element Minimum Size

Button/Card 200 × 80 pt

Poster card 300 × 450 pt

Hero banner Full width × 800 pt

Body text title3 (20pt)

Title text title (28pt)

Edge padding 60 pt

Top Shelf Content Types

Style Use Case

Carousel (.actions) Featured with play buttons

Carousel (.details) Info-focused items

Sectioned Multiple categories

Inset Banner-style single item

Red Flags

Smell Problem Fix

Manual index tracking for focus Reinventing Focus Engine Use @FocusState

.focusable() without visual change User can't see focus Add isFocused styling

Slow focus animations (>200ms) Laggy navigation feel Use quick transitions

44×44 buttons Too small for TV Minimum 200×80

.caption/.footnote text Unreadable at distance Use .title3 minimum

No .onExitCommand handler User trapped in screen Always handle Menu

Complex gestures (pinch/rotate) Not supported by Siri Remote Use tap/swipe only

Empty Top Shelf for logged-out Wasted promotional space Show promo content

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review