main
swift 120 lines 4.23 KB
Raw
1 //
2 // TVShelfSection.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 horizontal shelf of poster cards sized for TV viewing distance,
13 /// with a "See All" card appended as the final item in the shelf.
14 ///
15 /// The last item renders as `TVSeeAllCell` — a dark card with a prompt
16 /// and pink pill button, matching the Viaplay tvOS design.
17 @MainActor
18 final class TVShelfSection: JomloSection {
19
20 let identifier: JomloSectionID
21 var onReloadNeeded: (@MainActor @Sendable () -> Void)?
22
23 private let title: String
24 private let contentItems: [TVContentItem]
25 weak var presenter: UIViewController?
26
27 init(id: String, title: String, items: [TVContentItem], presenter: UIViewController? = nil) {
28 self.identifier = JomloSectionID(id)
29 self.title = title
30 self.contentItems = items
31 self.presenter = presenter
32 }
33
34 // MARK: - JomloSection
35
36 /// Returns content items followed by a single `TVSeeAllItem` sentinel.
37 func items() -> [any JomloItem] {
38 let seeAll = TVSeeAllItem(id: "\(identifier.rawValue)-see-all", sectionTitle: title)
39 return contentItems + [seeAll]
40 }
41
42 func layoutSection(
43 environment: NSCollectionLayoutEnvironment,
44 sectionIndex: Int
45 ) -> NSCollectionLayoutSection {
46 let itemSize = NSCollectionLayoutSize(
47 widthDimension: .fractionalWidth(1.0),
48 heightDimension: .fractionalHeight(1.0)
49 )
50 let item = NSCollectionLayoutItem(layoutSize: itemSize)
51
52 let groupSize = NSCollectionLayoutSize(
53 widthDimension: .absolute(240),
54 heightDimension: .absolute(420)
55 )
56 let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
57
58 let section = NSCollectionLayoutSection(group: group)
59 section.orthogonalScrollingBehavior = .continuous
60 section.interGroupSpacing = 40
61 section.contentInsets = NSDirectionalEdgeInsets(
62 top: 10, leading: 80, bottom: 50, trailing: 80)
63
64 let headerSize = NSCollectionLayoutSize(
65 widthDimension: .fractionalWidth(1.0),
66 heightDimension: .absolute(60)
67 )
68 let header = NSCollectionLayoutBoundarySupplementaryItem(
69 layoutSize: headerSize,
70 elementKind: UICollectionView.elementKindSectionHeader,
71 alignment: .top
72 )
73 section.boundarySupplementaryItems = [header]
74
75 return section
76 }
77
78 func cell(
79 for item: any JomloItem,
80 in collectionView: UICollectionView,
81 at indexPath: IndexPath
82 ) -> UICollectionViewCell {
83 if let seeAllItem = item as? TVSeeAllItem {
84 return collectionView.dequeueJomloCell(
85 TVSeeAllCell.self, for: indexPath, item: seeAllItem)
86 }
87 guard let contentItem = item as? TVContentItem else { return UICollectionViewCell() }
88 return collectionView.dequeueJomloCell(TVPosterCell.self, for: indexPath, item: contentItem)
89 }
90
91 func supplementaryView(
92 ofKind kind: String,
93 in collectionView: UICollectionView,
94 at indexPath: IndexPath
95 ) -> UICollectionReusableView? {
96 guard kind == UICollectionView.elementKindSectionHeader else { return nil }
97 guard
98 let header = collectionView.dequeueReusableSupplementaryView(
99 ofKind: kind,
100 withReuseIdentifier: TVSectionHeaderView.reuseIdentifier,
101 for: indexPath
102 ) as? TVSectionHeaderView
103 else { return nil }
104 // Title only — "See All" is now the last shelf card, not the header
105 header.configure(title: title)
106 return header
107 }
108
109 func didSelectItem(_ item: any JomloItem, at indexPath: IndexPath) {
110 if let seeAllItem = item as? TVSeeAllItem {
111 let seeAllVC = TVSeeAllViewController(
112 title: seeAllItem.sectionTitle, items: contentItems)
113 presenter?.navigationController?.pushViewController(seeAllVC, animated: true)
114 return
115 }
116 if let contentItem = item as? TVContentItem {
117 print("[tvOS] Selected: \(contentItem.title)")
118 }
119 }
120 }