| 1 | // |
| 2 | // TVStreamingHomeViewController.swift |
| 3 | // JomloCV tvOS Example |
| 4 | // |
| 5 | // Created by Seto Elkahfi on 2026-01-27. |
| 6 | // Copyright © 2026 Seto Elkahfi. All rights reserved. |
| 7 | // |
| 8 | |
| 9 | import JomloCollectionView |
| 10 | import UIKit |
| 11 | |
| 12 | /// The main view controller for the tvOS example app. |
| 13 | /// |
| 14 | /// Demonstrates a streaming-service home screen optimized for Apple TV: |
| 15 | /// - Full-width hero section with group-paging for Siri Remote swipes |
| 16 | /// - "Continue Watching" shelf with progress indicators |
| 17 | /// - Multiple content shelves with large poster cards |
| 18 | /// - Category navigation row |
| 19 | /// - All cells use the tvOS focus engine with scale + shadow animations |
| 20 | @MainActor |
| 21 | final class TVStreamingHomeViewController: JomloCollectionViewController { |
| 22 | |
| 23 | init() { |
| 24 | super.init(sections: []) |
| 25 | } |
| 26 | |
| 27 | override func viewDidLoad() { |
| 28 | super.viewDidLoad() |
| 29 | view.backgroundColor = UIColor(white: 0.05, alpha: 1.0) |
| 30 | |
| 31 | // Register TV-optimized cells |
| 32 | collectionView.registerJomloCell(TVHeroCell.self) |
| 33 | collectionView.registerJomloCell(TVPosterCell.self) |
| 34 | collectionView.registerJomloCell(TVSeeAllCell.self) |
| 35 | collectionView.registerJomloCell(TVCategoryCell.self) |
| 36 | collectionView.registerJomloCell(TVResumeCell.self) |
| 37 | |
| 38 | // Register TV section header |
| 39 | collectionView.register( |
| 40 | TVSectionHeaderView.self, |
| 41 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, |
| 42 | withReuseIdentifier: TVSectionHeaderView.reuseIdentifier |
| 43 | ) |
| 44 | |
| 45 | // Ensure cells can receive focus outside visible bounds during scrolling |
| 46 | collectionView.clipsToBounds = false |
| 47 | collectionView.remembersLastFocusedIndexPath = true |
| 48 | |
| 49 | loadContent() |
| 50 | } |
| 51 | |
| 52 | // MARK: - Content Assembly |
| 53 | |
| 54 | private func loadContent() { |
| 55 | let sections: [any JomloSection] = [ |
| 56 | makeHeroSection(), |
| 57 | makeResumeSection(), |
| 58 | makeTrendingShelf(), |
| 59 | makeCategorySection(), |
| 60 | makeSeriesShelf(), |
| 61 | makeMoviesShelf(), |
| 62 | makeSportsShelf(), |
| 63 | ] |
| 64 | setSections(sections, animated: false) |
| 65 | } |
| 66 | |
| 67 | // MARK: - Section Factories |
| 68 | |
| 69 | private func makeHeroSection() -> TVHeroSection { |
| 70 | TVHeroSection(items: [ |
| 71 | TVHeroItem( |
| 72 | id: "hero-1", |
| 73 | title: "The Last Kingdom", |
| 74 | tagline: "The epic saga concludes", |
| 75 | metadata: "Season 5 \u{2022} Drama \u{2022} 2022", |
| 76 | gradientTop: "#1a237e", |
| 77 | gradientBottom: "#0d47a1" |
| 78 | ), |
| 79 | TVHeroItem( |
| 80 | id: "hero-2", |
| 81 | title: "Formula 1:\nDrive to Survive", |
| 82 | tagline: "Every race. Every rivalry.", |
| 83 | metadata: "Season 6 \u{2022} Documentary \u{2022} 2024", |
| 84 | gradientTop: "#b71c1c", |
| 85 | gradientBottom: "#880e4f" |
| 86 | ), |
| 87 | TVHeroItem( |
| 88 | id: "hero-3", |
| 89 | title: "Premier League", |
| 90 | tagline: "Every match. Every week. Live.", |
| 91 | metadata: "Live Sport \u{2022} Football \u{2022} 2024-25", |
| 92 | gradientTop: "#1b5e20", |
| 93 | gradientBottom: "#004d40" |
| 94 | ), |
| 95 | TVHeroItem( |
| 96 | id: "hero-4", |
| 97 | title: "Dune: Part Two", |
| 98 | tagline: "The legend continues", |
| 99 | metadata: "2024 \u{2022} Sci-Fi \u{2022} 2h 46m", |
| 100 | gradientTop: "#e65100", |
| 101 | gradientBottom: "#bf360c" |
| 102 | ), |
| 103 | ]) |
| 104 | } |
| 105 | |
| 106 | private func makeResumeSection() -> TVResumeSection { |
| 107 | TVResumeSection(items: [ |
| 108 | TVResumeItem( |
| 109 | id: "r-1", title: "Breaking Bad", episodeInfo: "S3 E7 \u{2022} One Minute", |
| 110 | progress: 0.65, colorHex: "#2e7d32", remainingTime: "18 min left"), |
| 111 | TVResumeItem( |
| 112 | id: "r-2", title: "The Bear", episodeInfo: "S2 E3 \u{2022} Sundae", progress: 0.3, |
| 113 | colorHex: "#f57f17", remainingTime: "24 min left"), |
| 114 | TVResumeItem( |
| 115 | id: "r-3", title: "Succession", episodeInfo: "S4 E1 \u{2022} The Munsters", |
| 116 | progress: 0.82, colorHex: "#1565c0", remainingTime: "11 min left"), |
| 117 | TVResumeItem( |
| 118 | id: "r-4", title: "Slow Horses", episodeInfo: "S3 E4 \u{2022} Uninvited", |
| 119 | progress: 0.45, colorHex: "#6a1b9a", remainingTime: "28 min left"), |
| 120 | TVResumeItem( |
| 121 | id: "r-5", title: "True Detective", episodeInfo: "S4 E2 \u{2022} Night Country", |
| 122 | progress: 0.15, colorHex: "#37474f", remainingTime: "52 min left"), |
| 123 | ]) |
| 124 | } |
| 125 | |
| 126 | private func makeTrendingShelf() -> TVShelfSection { |
| 127 | TVShelfSection( |
| 128 | id: "trending", |
| 129 | title: "Trending Now", |
| 130 | items: [ |
| 131 | TVContentItem( |
| 132 | id: "t-1", title: "Oppenheimer", subtitle: "2023 \u{2022} Drama", |
| 133 | colorHex: "#ff6f00", isFeatured: true), |
| 134 | TVContentItem( |
| 135 | id: "t-2", title: "Killers of the\nFlower Moon", |
| 136 | subtitle: "2023 \u{2022} Crime", colorHex: "#4e342e", isFeatured: false), |
| 137 | TVContentItem( |
| 138 | id: "t-3", title: "Past Lives", subtitle: "2023 \u{2022} Romance", |
| 139 | colorHex: "#00838f", isFeatured: false), |
| 140 | TVContentItem( |
| 141 | id: "t-4", title: "Anatomy\nof a Fall", subtitle: "2023 \u{2022} Thriller", |
| 142 | colorHex: "#283593", isFeatured: false), |
| 143 | TVContentItem( |
| 144 | id: "t-5", title: "The Holdovers", subtitle: "2023 \u{2022} Comedy", |
| 145 | colorHex: "#2e7d32", isFeatured: false), |
| 146 | TVContentItem( |
| 147 | id: "t-6", title: "Poor Things", subtitle: "2023 \u{2022} Fantasy", |
| 148 | colorHex: "#ad1457", isFeatured: true), |
| 149 | TVContentItem( |
| 150 | id: "t-7", title: "The Zone\nof Interest", subtitle: "2023 \u{2022} Drama", |
| 151 | colorHex: "#37474f", isFeatured: false), |
| 152 | ], |
| 153 | presenter: self |
| 154 | ) |
| 155 | } |
| 156 | |
| 157 | private func makeCategorySection() -> TVCategorySection { |
| 158 | TVCategorySection(items: [ |
| 159 | TVCategoryItem(id: "cat-1", name: "Movies", icon: "\u{1F3AC}", colorHex: "#c62828"), |
| 160 | TVCategoryItem(id: "cat-2", name: "Series", icon: "\u{1F4FA}", colorHex: "#6a1b9a"), |
| 161 | TVCategoryItem(id: "cat-3", name: "Sport", icon: "\u{26BD}", colorHex: "#1565c0"), |
| 162 | TVCategoryItem(id: "cat-4", name: "Kids", icon: "\u{1F9F8}", colorHex: "#2e7d32"), |
| 163 | TVCategoryItem( |
| 164 | id: "cat-5", name: "Documentaries", icon: "\u{1F30D}", colorHex: "#e65100"), |
| 165 | TVCategoryItem(id: "cat-6", name: "Live TV", icon: "\u{1F4E1}", colorHex: "#283593"), |
| 166 | ]) |
| 167 | } |
| 168 | |
| 169 | private func makeSeriesShelf() -> TVShelfSection { |
| 170 | TVShelfSection( |
| 171 | id: "series", |
| 172 | title: "Popular Series", |
| 173 | items: [ |
| 174 | TVContentItem( |
| 175 | id: "s-1", title: "The Bear", subtitle: "2022\u{2013} \u{2022} Comedy", |
| 176 | colorHex: "#e65100", isFeatured: true), |
| 177 | TVContentItem( |
| 178 | id: "s-2", title: "Slow Horses", subtitle: "2022\u{2013} \u{2022} Spy", |
| 179 | colorHex: "#1a237e", isFeatured: false), |
| 180 | TVContentItem( |
| 181 | id: "s-3", title: "Severance", subtitle: "2022\u{2013} \u{2022} Sci-Fi", |
| 182 | colorHex: "#004d40", isFeatured: false), |
| 183 | TVContentItem( |
| 184 | id: "s-4", title: "Shogun", subtitle: "2024 \u{2022} Historical", |
| 185 | colorHex: "#b71c1c", isFeatured: true), |
| 186 | TVContentItem( |
| 187 | id: "s-5", title: "True Detective", subtitle: "2024 \u{2022} Crime", |
| 188 | colorHex: "#311b92", isFeatured: false), |
| 189 | TVContentItem( |
| 190 | id: "s-6", title: "Fallout", subtitle: "2024 \u{2022} Sci-Fi", |
| 191 | colorHex: "#33691e", isFeatured: false), |
| 192 | ], |
| 193 | presenter: self |
| 194 | ) |
| 195 | } |
| 196 | |
| 197 | private func makeMoviesShelf() -> TVShelfSection { |
| 198 | TVShelfSection( |
| 199 | id: "movies", |
| 200 | title: "New Releases", |
| 201 | items: [ |
| 202 | TVContentItem( |
| 203 | id: "m-1", title: "Dune:\nPart Two", subtitle: "2024 \u{2022} Sci-Fi", |
| 204 | colorHex: "#ff8f00", isFeatured: true), |
| 205 | TVContentItem( |
| 206 | id: "m-2", title: "Civil War", subtitle: "2024 \u{2022} Action", |
| 207 | colorHex: "#455a64", isFeatured: false), |
| 208 | TVContentItem( |
| 209 | id: "m-3", title: "Challengers", subtitle: "2024 \u{2022} Drama", |
| 210 | colorHex: "#00695c", isFeatured: false), |
| 211 | TVContentItem( |
| 212 | id: "m-4", title: "Furiosa", subtitle: "2024 \u{2022} Action", |
| 213 | colorHex: "#795548", isFeatured: false), |
| 214 | TVContentItem( |
| 215 | id: "m-5", title: "Inside Out 2", subtitle: "2024 \u{2022} Animation", |
| 216 | colorHex: "#7b1fa2", isFeatured: true), |
| 217 | ], |
| 218 | presenter: self |
| 219 | ) |
| 220 | } |
| 221 | |
| 222 | private func makeSportsShelf() -> TVShelfSection { |
| 223 | TVShelfSection( |
| 224 | id: "sports", |
| 225 | title: "Live & Upcoming Sport", |
| 226 | items: [ |
| 227 | TVContentItem( |
| 228 | id: "sp-1", title: "Premier\nLeague", subtitle: "Live \u{2022} Football", |
| 229 | colorHex: "#1a237e", isFeatured: true), |
| 230 | TVContentItem( |
| 231 | id: "sp-2", title: "Champions\nLeague", subtitle: "Tomorrow \u{2022} Football", |
| 232 | colorHex: "#01579b", isFeatured: false), |
| 233 | TVContentItem( |
| 234 | id: "sp-3", title: "Formula 1", subtitle: "Sunday \u{2022} Racing", |
| 235 | colorHex: "#b71c1c", isFeatured: true), |
| 236 | TVContentItem( |
| 237 | id: "sp-4", title: "Handball", subtitle: "Today \u{2022} Live", |
| 238 | colorHex: "#4a148c", isFeatured: false), |
| 239 | TVContentItem( |
| 240 | id: "sp-5", title: "NHL", subtitle: "Tonight \u{2022} Ice Hockey", |
| 241 | colorHex: "#1b5e20", isFeatured: false), |
| 242 | ], |
| 243 | presenter: self |
| 244 | ) |
| 245 | } |
| 246 | } |