| 1 | // |
| 2 | // TVTabViewControllers.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 | // MARK: - Series Tab |
| 13 | |
| 14 | /// The "Series" tab — shows popular and new series shelves. |
| 15 | @MainActor |
| 16 | final class TVSeriesViewController: JomloCollectionViewController { |
| 17 | |
| 18 | init() { |
| 19 | super.init(sections: []) |
| 20 | } |
| 21 | |
| 22 | override func viewDidLoad() { |
| 23 | super.viewDidLoad() |
| 24 | view.backgroundColor = UIColor(white: 0.05, alpha: 1.0) |
| 25 | registerCells() |
| 26 | loadContent() |
| 27 | } |
| 28 | |
| 29 | private func registerCells() { |
| 30 | collectionView.registerJomloCell(TVPosterCell.self) |
| 31 | collectionView.registerJomloCell(TVSeeAllCell.self) |
| 32 | collectionView.register( |
| 33 | TVSectionHeaderView.self, |
| 34 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, |
| 35 | withReuseIdentifier: TVSectionHeaderView.reuseIdentifier |
| 36 | ) |
| 37 | collectionView.clipsToBounds = false |
| 38 | collectionView.remembersLastFocusedIndexPath = true |
| 39 | } |
| 40 | |
| 41 | private func loadContent() { |
| 42 | setSections( |
| 43 | [ |
| 44 | TVShelfSection( |
| 45 | id: "series-popular", title: "Popular Series", |
| 46 | items: [ |
| 47 | TVContentItem( |
| 48 | id: "sp-1", title: "The Bear", subtitle: "2022\u{2013} \u{2022} Comedy", |
| 49 | colorHex: "#e65100", isFeatured: true), |
| 50 | TVContentItem( |
| 51 | id: "sp-2", title: "Slow Horses", subtitle: "2022\u{2013} \u{2022} Spy", |
| 52 | colorHex: "#1a237e", isFeatured: false), |
| 53 | TVContentItem( |
| 54 | id: "sp-3", title: "Severance", |
| 55 | subtitle: "2022\u{2013} \u{2022} Sci-Fi", colorHex: "#004d40", |
| 56 | isFeatured: false), |
| 57 | TVContentItem( |
| 58 | id: "sp-4", title: "Shogun", subtitle: "2024 \u{2022} Historical", |
| 59 | colorHex: "#b71c1c", isFeatured: true), |
| 60 | TVContentItem( |
| 61 | id: "sp-5", title: "True Detective", subtitle: "2024 \u{2022} Crime", |
| 62 | colorHex: "#311b92", isFeatured: false), |
| 63 | TVContentItem( |
| 64 | id: "sp-6", title: "Fallout", subtitle: "2024 \u{2022} Sci-Fi", |
| 65 | colorHex: "#33691e", isFeatured: false), |
| 66 | ], |
| 67 | |
| 68 | presenter: self), |
| 69 | TVShelfSection( |
| 70 | id: "series-new", title: "New Episodes This Week", |
| 71 | items: [ |
| 72 | TVContentItem( |
| 73 | id: "sn-1", title: "House of\nthe Dragon", |
| 74 | subtitle: "S2 \u{2022} Fantasy", colorHex: "#880e4f", isFeatured: true), |
| 75 | TVContentItem( |
| 76 | id: "sn-2", title: "The Diplomat", subtitle: "S2 \u{2022} Thriller", |
| 77 | colorHex: "#1565c0", isFeatured: false), |
| 78 | TVContentItem( |
| 79 | id: "sn-3", title: "Industry", subtitle: "S3 \u{2022} Drama", |
| 80 | colorHex: "#37474f", isFeatured: false), |
| 81 | TVContentItem( |
| 82 | id: "sn-4", title: "Bad Sisters", subtitle: "S2 \u{2022} Comedy", |
| 83 | colorHex: "#2e7d32", isFeatured: false), |
| 84 | TVContentItem( |
| 85 | id: "sn-5", title: "Reacher", subtitle: "S3 \u{2022} Action", |
| 86 | colorHex: "#4e342e", isFeatured: false), |
| 87 | ], |
| 88 | |
| 89 | presenter: self), |
| 90 | TVShelfSection( |
| 91 | id: "series-scandi", title: "Scandinavian Originals", |
| 92 | items: [ |
| 93 | TVContentItem( |
| 94 | id: "sc-1", title: "The Bridge", subtitle: "Crime \u{2022} Thriller", |
| 95 | colorHex: "#455a64", isFeatured: false), |
| 96 | TVContentItem( |
| 97 | id: "sc-2", title: "Borgen", subtitle: "Political \u{2022} Drama", |
| 98 | colorHex: "#00695c", isFeatured: false), |
| 99 | TVContentItem( |
| 100 | id: "sc-3", title: "Beforeigners", subtitle: "Sci-Fi \u{2022} Crime", |
| 101 | colorHex: "#283593", isFeatured: true), |
| 102 | TVContentItem( |
| 103 | id: "sc-4", title: "The Rain", subtitle: "Sci-Fi \u{2022} Drama", |
| 104 | colorHex: "#1b5e20", isFeatured: false), |
| 105 | TVContentItem( |
| 106 | id: "sc-5", title: "Kamikaze", subtitle: "Drama", colorHex: "#6a1b9a", |
| 107 | isFeatured: false), |
| 108 | ], |
| 109 | |
| 110 | presenter: self), |
| 111 | ], animated: false) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // MARK: - Movies Tab |
| 116 | |
| 117 | /// The "Movies" tab — shows featured films and categories. |
| 118 | @MainActor |
| 119 | final class TVMoviesViewController: JomloCollectionViewController { |
| 120 | |
| 121 | init() { |
| 122 | super.init(sections: []) |
| 123 | } |
| 124 | |
| 125 | override func viewDidLoad() { |
| 126 | super.viewDidLoad() |
| 127 | view.backgroundColor = UIColor(white: 0.05, alpha: 1.0) |
| 128 | registerCells() |
| 129 | loadContent() |
| 130 | } |
| 131 | |
| 132 | private func registerCells() { |
| 133 | collectionView.registerJomloCell(TVPosterCell.self) |
| 134 | collectionView.registerJomloCell(TVSeeAllCell.self) |
| 135 | collectionView.register( |
| 136 | TVSectionHeaderView.self, |
| 137 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, |
| 138 | withReuseIdentifier: TVSectionHeaderView.reuseIdentifier |
| 139 | ) |
| 140 | collectionView.clipsToBounds = false |
| 141 | collectionView.remembersLastFocusedIndexPath = true |
| 142 | } |
| 143 | |
| 144 | private func loadContent() { |
| 145 | setSections( |
| 146 | [ |
| 147 | TVShelfSection( |
| 148 | id: "movies-new", title: "New Releases", |
| 149 | items: [ |
| 150 | TVContentItem( |
| 151 | id: "mn-1", title: "Dune:\nPart Two", subtitle: "2024 \u{2022} Sci-Fi", |
| 152 | colorHex: "#ff8f00", isFeatured: true), |
| 153 | TVContentItem( |
| 154 | id: "mn-2", title: "Civil War", subtitle: "2024 \u{2022} Action", |
| 155 | colorHex: "#455a64", isFeatured: false), |
| 156 | TVContentItem( |
| 157 | id: "mn-3", title: "Challengers", subtitle: "2024 \u{2022} Drama", |
| 158 | colorHex: "#00695c", isFeatured: false), |
| 159 | TVContentItem( |
| 160 | id: "mn-4", title: "Furiosa", subtitle: "2024 \u{2022} Action", |
| 161 | colorHex: "#795548", isFeatured: false), |
| 162 | TVContentItem( |
| 163 | id: "mn-5", title: "Inside Out 2", subtitle: "2024 \u{2022} Animation", |
| 164 | colorHex: "#7b1fa2", isFeatured: true), |
| 165 | ], |
| 166 | |
| 167 | presenter: self), |
| 168 | TVShelfSection( |
| 169 | id: "movies-acclaimed", title: "Critically Acclaimed", |
| 170 | items: [ |
| 171 | TVContentItem( |
| 172 | id: "ma-1", title: "Oppenheimer", subtitle: "2023 \u{2022} Drama", |
| 173 | colorHex: "#ff6f00", isFeatured: true), |
| 174 | TVContentItem( |
| 175 | id: "ma-2", title: "Past Lives", subtitle: "2023 \u{2022} Romance", |
| 176 | colorHex: "#00838f", isFeatured: false), |
| 177 | TVContentItem( |
| 178 | id: "ma-3", title: "Anatomy\nof a Fall", |
| 179 | subtitle: "2023 \u{2022} Thriller", colorHex: "#283593", |
| 180 | isFeatured: false), |
| 181 | TVContentItem( |
| 182 | id: "ma-4", title: "The Holdovers", subtitle: "2023 \u{2022} Comedy", |
| 183 | colorHex: "#2e7d32", isFeatured: false), |
| 184 | TVContentItem( |
| 185 | id: "ma-5", title: "Poor Things", subtitle: "2023 \u{2022} Fantasy", |
| 186 | colorHex: "#ad1457", isFeatured: true), |
| 187 | TVContentItem( |
| 188 | id: "ma-6", title: "Zone of\nInterest", subtitle: "2023 \u{2022} Drama", |
| 189 | colorHex: "#37474f", isFeatured: false), |
| 190 | ], |
| 191 | |
| 192 | presenter: self), |
| 193 | TVShelfSection( |
| 194 | id: "movies-family", title: "Family Movies", |
| 195 | items: [ |
| 196 | TVContentItem( |
| 197 | id: "mf-1", title: "Wish", subtitle: "2023 \u{2022} Animation", |
| 198 | colorHex: "#4527a0", isFeatured: false), |
| 199 | TVContentItem( |
| 200 | id: "mf-2", title: "Migration", subtitle: "2023 \u{2022} Animation", |
| 201 | colorHex: "#00838f", isFeatured: false), |
| 202 | TVContentItem( |
| 203 | id: "mf-3", title: "Wonka", subtitle: "2023 \u{2022} Fantasy", |
| 204 | colorHex: "#6a1b9a", isFeatured: true), |
| 205 | TVContentItem( |
| 206 | id: "mf-4", title: "Elemental", subtitle: "2023 \u{2022} Animation", |
| 207 | colorHex: "#e65100", isFeatured: false), |
| 208 | ], |
| 209 | |
| 210 | presenter: self), |
| 211 | ], animated: false) |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | // MARK: - Sports Tab |
| 216 | |
| 217 | /// The "Sport" tab — shows live and upcoming sports content. |
| 218 | @MainActor |
| 219 | final class TVSportsViewController: JomloCollectionViewController { |
| 220 | |
| 221 | init() { |
| 222 | super.init(sections: []) |
| 223 | } |
| 224 | |
| 225 | override func viewDidLoad() { |
| 226 | super.viewDidLoad() |
| 227 | view.backgroundColor = UIColor(white: 0.05, alpha: 1.0) |
| 228 | registerCells() |
| 229 | loadContent() |
| 230 | } |
| 231 | |
| 232 | private func registerCells() { |
| 233 | collectionView.registerJomloCell(TVPosterCell.self) |
| 234 | collectionView.registerJomloCell(TVSeeAllCell.self) |
| 235 | collectionView.register( |
| 236 | TVSectionHeaderView.self, |
| 237 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, |
| 238 | withReuseIdentifier: TVSectionHeaderView.reuseIdentifier |
| 239 | ) |
| 240 | collectionView.clipsToBounds = false |
| 241 | collectionView.remembersLastFocusedIndexPath = true |
| 242 | } |
| 243 | |
| 244 | private func loadContent() { |
| 245 | setSections( |
| 246 | [ |
| 247 | TVShelfSection( |
| 248 | id: "sports-live", title: "Live Now", |
| 249 | items: [ |
| 250 | TVContentItem( |
| 251 | id: "sl-1", title: "Premier\nLeague", |
| 252 | subtitle: "Live \u{2022} Football", colorHex: "#1a237e", |
| 253 | isFeatured: true), |
| 254 | TVContentItem( |
| 255 | id: "sl-2", title: "Handball\nChampions", |
| 256 | subtitle: "Live \u{2022} Handball", colorHex: "#4a148c", |
| 257 | isFeatured: false), |
| 258 | ], |
| 259 | |
| 260 | presenter: self), |
| 261 | TVShelfSection( |
| 262 | id: "sports-upcoming", title: "Upcoming", |
| 263 | items: [ |
| 264 | TVContentItem( |
| 265 | id: "su-1", title: "Champions\nLeague", |
| 266 | subtitle: "Tomorrow \u{2022} Football", colorHex: "#01579b", |
| 267 | isFeatured: false), |
| 268 | TVContentItem( |
| 269 | id: "su-2", title: "Formula 1", subtitle: "Sunday \u{2022} Racing", |
| 270 | colorHex: "#b71c1c", isFeatured: true), |
| 271 | TVContentItem( |
| 272 | id: "su-3", title: "NHL", subtitle: "Tonight \u{2022} Ice Hockey", |
| 273 | colorHex: "#1b5e20", isFeatured: false), |
| 274 | TVContentItem( |
| 275 | id: "su-4", title: "UFC 310", subtitle: "Saturday \u{2022} MMA", |
| 276 | colorHex: "#880e4f", isFeatured: false), |
| 277 | ], |
| 278 | |
| 279 | presenter: self), |
| 280 | TVShelfSection( |
| 281 | id: "sports-replays", title: "Replays & Highlights", |
| 282 | items: [ |
| 283 | TVContentItem( |
| 284 | id: "sr-1", title: "Match of\nthe Day", subtitle: "Football", |
| 285 | colorHex: "#2e7d32", isFeatured: false), |
| 286 | TVContentItem( |
| 287 | id: "sr-2", title: "F1 Highlights", subtitle: "Racing", |
| 288 | colorHex: "#c62828", isFeatured: false), |
| 289 | TVContentItem( |
| 290 | id: "sr-3", title: "Golf\nMajors", subtitle: "Golf", |
| 291 | colorHex: "#33691e", isFeatured: false), |
| 292 | TVContentItem( |
| 293 | id: "sr-4", title: "Biathlon\nWorld Cup", subtitle: "Winter Sport", |
| 294 | colorHex: "#0d47a1", isFeatured: false), |
| 295 | ], |
| 296 | |
| 297 | presenter: self), |
| 298 | ], animated: false) |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | // MARK: - Kids Tab |
| 303 | |
| 304 | /// The "Kids" tab — shows children's content. |
| 305 | @MainActor |
| 306 | final class TVKidsViewController: JomloCollectionViewController { |
| 307 | |
| 308 | init() { |
| 309 | super.init(sections: []) |
| 310 | } |
| 311 | |
| 312 | override func viewDidLoad() { |
| 313 | super.viewDidLoad() |
| 314 | view.backgroundColor = UIColor(white: 0.08, alpha: 1.0) |
| 315 | registerCells() |
| 316 | loadContent() |
| 317 | } |
| 318 | |
| 319 | private func registerCells() { |
| 320 | collectionView.registerJomloCell(TVPosterCell.self) |
| 321 | collectionView.registerJomloCell(TVSeeAllCell.self) |
| 322 | collectionView.register( |
| 323 | TVSectionHeaderView.self, |
| 324 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, |
| 325 | withReuseIdentifier: TVSectionHeaderView.reuseIdentifier |
| 326 | ) |
| 327 | collectionView.clipsToBounds = false |
| 328 | collectionView.remembersLastFocusedIndexPath = true |
| 329 | } |
| 330 | |
| 331 | private func loadContent() { |
| 332 | setSections( |
| 333 | [ |
| 334 | TVShelfSection( |
| 335 | id: "kids-popular", title: "Popular with Kids", |
| 336 | items: [ |
| 337 | TVContentItem( |
| 338 | id: "kp-1", title: "Peppa Pig", subtitle: "Animation", |
| 339 | colorHex: "#e91e63", isFeatured: true), |
| 340 | TVContentItem( |
| 341 | id: "kp-2", title: "Paw Patrol", subtitle: "Animation", |
| 342 | colorHex: "#1565c0", isFeatured: false), |
| 343 | TVContentItem( |
| 344 | id: "kp-3", title: "Bluey", subtitle: "Animation", colorHex: "#0097a7", |
| 345 | isFeatured: true), |
| 346 | TVContentItem( |
| 347 | id: "kp-4", title: "CoComelon", subtitle: "Animation", |
| 348 | colorHex: "#f57f17", isFeatured: false), |
| 349 | TVContentItem( |
| 350 | id: "kp-5", title: "Lego Friends", subtitle: "Animation", |
| 351 | colorHex: "#7b1fa2", isFeatured: false), |
| 352 | ], |
| 353 | |
| 354 | presenter: self), |
| 355 | TVShelfSection( |
| 356 | id: "kids-movies", title: "Kids Movies", |
| 357 | items: [ |
| 358 | TVContentItem( |
| 359 | id: "km-1", title: "Frozen II", subtitle: "2019 \u{2022} Animation", |
| 360 | colorHex: "#4fc3f7", isFeatured: true), |
| 361 | TVContentItem( |
| 362 | id: "km-2", title: "Moana", subtitle: "2016 \u{2022} Animation", |
| 363 | colorHex: "#00897b", isFeatured: false), |
| 364 | TVContentItem( |
| 365 | id: "km-3", title: "Encanto", subtitle: "2021 \u{2022} Animation", |
| 366 | colorHex: "#ff8f00", isFeatured: false), |
| 367 | TVContentItem( |
| 368 | id: "km-4", title: "Luca", subtitle: "2021 \u{2022} Animation", |
| 369 | colorHex: "#1565c0", isFeatured: false), |
| 370 | ], |
| 371 | |
| 372 | presenter: self), |
| 373 | ], animated: false) |
| 374 | } |
| 375 | } |
| 376 | |
| 377 | // MARK: - Search Tab |
| 378 | |
| 379 | /// The "Search" tab — placeholder for search functionality. |
| 380 | @MainActor |
| 381 | final class TVSearchViewController: UIViewController { |
| 382 | |
| 383 | private let messageLabel: UILabel = { |
| 384 | let label = UILabel() |
| 385 | label.text = "Search" |
| 386 | label.font = .systemFont(ofSize: 42, weight: .medium) |
| 387 | label.textColor = UIColor.white.withAlphaComponent(0.6) |
| 388 | label.textAlignment = .center |
| 389 | return label |
| 390 | }() |
| 391 | |
| 392 | private let subtitleLabel: UILabel = { |
| 393 | let label = UILabel() |
| 394 | label.text = "Type to search for movies, series, and sport" |
| 395 | label.font = .systemFont(ofSize: 28, weight: .regular) |
| 396 | label.textColor = UIColor.white.withAlphaComponent(0.4) |
| 397 | label.textAlignment = .center |
| 398 | return label |
| 399 | }() |
| 400 | |
| 401 | override func viewDidLoad() { |
| 402 | super.viewDidLoad() |
| 403 | view.backgroundColor = UIColor(white: 0.05, alpha: 1.0) |
| 404 | |
| 405 | let stack = UIStackView(arrangedSubviews: [messageLabel, subtitleLabel]) |
| 406 | stack.axis = .vertical |
| 407 | stack.spacing = 16 |
| 408 | stack.alignment = .center |
| 409 | stack.translatesAutoresizingMaskIntoConstraints = false |
| 410 | |
| 411 | view.addSubview(stack) |
| 412 | NSLayoutConstraint.activate([ |
| 413 | stack.centerXAnchor.constraint(equalTo: view.centerXAnchor), |
| 414 | stack.centerYAnchor.constraint(equalTo: view.centerYAnchor), |
| 415 | ]) |
| 416 | } |
| 417 | } |