| 1 | // |
| 2 | // TVResumeSection.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 "Continue Watching" shelf with wide landscape cards showing progress. |
| 13 | @MainActor |
| 14 | final class TVResumeSection: JomloSection { |
| 15 | |
| 16 | let identifier = JomloSectionID("tv-resume") |
| 17 | var onReloadNeeded: (@MainActor @Sendable () -> Void)? |
| 18 | |
| 19 | private let resumeItems: [TVResumeItem] |
| 20 | |
| 21 | init(items: [TVResumeItem]) { |
| 22 | self.resumeItems = items |
| 23 | } |
| 24 | |
| 25 | func items() -> [any JomloItem] { |
| 26 | resumeItems |
| 27 | } |
| 28 | |
| 29 | func layoutSection( |
| 30 | environment: NSCollectionLayoutEnvironment, |
| 31 | sectionIndex: Int |
| 32 | ) -> NSCollectionLayoutSection { |
| 33 | // Wide landscape cards — 16:9 thumbnails |
| 34 | let itemSize = NSCollectionLayoutSize( |
| 35 | widthDimension: .fractionalWidth(1.0), |
| 36 | heightDimension: .fractionalHeight(1.0) |
| 37 | ) |
| 38 | let item = NSCollectionLayoutItem(layoutSize: itemSize) |
| 39 | |
| 40 | let groupSize = NSCollectionLayoutSize( |
| 41 | widthDimension: .absolute(380), |
| 42 | heightDimension: .absolute(320) |
| 43 | ) |
| 44 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) |
| 45 | |
| 46 | let section = NSCollectionLayoutSection(group: group) |
| 47 | section.orthogonalScrollingBehavior = .continuous |
| 48 | section.interGroupSpacing = 40 |
| 49 | section.contentInsets = NSDirectionalEdgeInsets( |
| 50 | top: 10, leading: 80, bottom: 50, trailing: 80) |
| 51 | |
| 52 | let headerSize = NSCollectionLayoutSize( |
| 53 | widthDimension: .fractionalWidth(1.0), |
| 54 | heightDimension: .absolute(60) |
| 55 | ) |
| 56 | let header = NSCollectionLayoutBoundarySupplementaryItem( |
| 57 | layoutSize: headerSize, |
| 58 | elementKind: UICollectionView.elementKindSectionHeader, |
| 59 | alignment: .top |
| 60 | ) |
| 61 | section.boundarySupplementaryItems = [header] |
| 62 | |
| 63 | return section |
| 64 | } |
| 65 | |
| 66 | func cell( |
| 67 | for item: any JomloItem, |
| 68 | in collectionView: UICollectionView, |
| 69 | at indexPath: IndexPath |
| 70 | ) -> UICollectionViewCell { |
| 71 | guard let resumeItem = item as? TVResumeItem else { return UICollectionViewCell() } |
| 72 | return collectionView.dequeueJomloCell(TVResumeCell.self, for: indexPath, item: resumeItem) |
| 73 | } |
| 74 | |
| 75 | func supplementaryView( |
| 76 | ofKind kind: String, |
| 77 | in collectionView: UICollectionView, |
| 78 | at indexPath: IndexPath |
| 79 | ) -> UICollectionReusableView? { |
| 80 | guard kind == UICollectionView.elementKindSectionHeader else { return nil } |
| 81 | guard |
| 82 | let header = collectionView.dequeueReusableSupplementaryView( |
| 83 | ofKind: kind, |
| 84 | withReuseIdentifier: TVSectionHeaderView.reuseIdentifier, |
| 85 | for: indexPath |
| 86 | ) as? TVSectionHeaderView |
| 87 | else { return nil } |
| 88 | header.configure(title: "Continue Watching") |
| 89 | return header |
| 90 | } |
| 91 | } |