main
swift 147 lines 4.83 KB
Raw
1 //
2 // TVSeeAllViewController.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 /// A tvOS view controller that simulates loading content from a network request,
13 /// then displays the results in a grid optimized for the big screen.
14 ///
15 /// Inspired by Viaplay's `LinkLoadingViewController` — shows a loading state
16 /// while "fetching" page data, then transitions to a focusable content grid.
17 @MainActor
18 final class TVSeeAllViewController: JomloCollectionViewController {
19
20 private let sectionTitle: String
21 private let contentItems: [TVContentItem]
22
23 private lazy var loadingView: UIView = {
24 let container = UIView()
25 container.translatesAutoresizingMaskIntoConstraints = false
26
27 let spinner = UIActivityIndicatorView(style: .large)
28 spinner.color = .white
29 spinner.startAnimating()
30 spinner.translatesAutoresizingMaskIntoConstraints = false
31
32 let label = UILabel()
33 label.text = "Loading..."
34 label.font = .systemFont(ofSize: 30, weight: .medium)
35 label.textColor = UIColor.white.withAlphaComponent(0.5)
36 label.translatesAutoresizingMaskIntoConstraints = false
37
38 container.addSubview(spinner)
39 container.addSubview(label)
40
41 NSLayoutConstraint.activate([
42 spinner.centerXAnchor.constraint(equalTo: container.centerXAnchor),
43 spinner.centerYAnchor.constraint(equalTo: container.centerYAnchor, constant: -20),
44 label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
45 label.topAnchor.constraint(equalTo: spinner.bottomAnchor, constant: 20),
46 ])
47
48 return container
49 }()
50
51 init(title: String, items: [TVContentItem]) {
52 self.sectionTitle = title
53 self.contentItems = items
54 super.init(sections: [])
55 }
56
57 override func viewDidLoad() {
58 super.viewDidLoad()
59 self.title = sectionTitle
60 view.backgroundColor = UIColor(white: 0.05, alpha: 1.0)
61
62 collectionView.registerJomloCell(TVPosterCell.self)
63 collectionView.clipsToBounds = false
64 collectionView.remembersLastFocusedIndexPath = true
65
66 showLoading()
67 simulateNetworkRequest()
68 }
69
70 private func showLoading() {
71 collectionView.isHidden = true
72 view.addSubview(loadingView)
73 NSLayoutConstraint.activate([
74 loadingView.topAnchor.constraint(equalTo: view.topAnchor),
75 loadingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
76 loadingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
77 loadingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
78 ])
79 }
80
81 private func simulateNetworkRequest() {
82 Task { @MainActor in
83 try? await Task.sleep(for: .seconds(1.5))
84 showContent()
85 }
86 }
87
88 private func showContent() {
89 loadingView.removeFromSuperview()
90 collectionView.isHidden = false
91
92 let gridSection = TVSeeAllGridSection(items: contentItems)
93 setSections([gridSection], animated: true)
94 }
95 }
96
97 // MARK: - Grid Section for tvOS "See All" page
98
99 @MainActor
100 private final class TVSeeAllGridSection: JomloSection {
101
102 let identifier = JomloSectionID("tv-see-all-grid")
103 var onReloadNeeded: (@MainActor @Sendable () -> Void)?
104
105 private let gridItems: [TVContentItem]
106
107 init(items: [TVContentItem]) {
108 self.gridItems = items
109 }
110
111 func items() -> [any JomloItem] {
112 gridItems
113 }
114
115 func layoutSection(
116 environment: NSCollectionLayoutEnvironment,
117 sectionIndex: Int
118 ) -> NSCollectionLayoutSection {
119 // 4 columns on TV for a nice grid
120 let itemSize = NSCollectionLayoutSize(
121 widthDimension: .fractionalWidth(0.25),
122 heightDimension: .absolute(420)
123 )
124 let item = NSCollectionLayoutItem(layoutSize: itemSize)
125 item.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
126
127 let groupSize = NSCollectionLayoutSize(
128 widthDimension: .fractionalWidth(1.0),
129 heightDimension: .absolute(420)
130 )
131 let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
132
133 let section = NSCollectionLayoutSection(group: group)
134 section.contentInsets = NSDirectionalEdgeInsets(
135 top: 40, leading: 60, bottom: 40, trailing: 60)
136 return section
137 }
138
139 func cell(
140 for item: any JomloItem,
141 in collectionView: UICollectionView,
142 at indexPath: IndexPath
143 ) -> UICollectionViewCell {
144 guard let contentItem = item as? TVContentItem else { return UICollectionViewCell() }
145 return collectionView.dequeueJomloCell(TVPosterCell.self, for: indexPath, item: contentItem)
146 }
147 }