main
swift 124 lines 4.14 KB
Raw
1 //
2 // TVResumeCell.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" cell for TV with large thumbnail, progress bar,
13 /// and remaining-time badge. Focus causes the cell to lift with shadow.
14 final class TVResumeCell: UICollectionViewCell, JomloSelfConfiguringCell {
15
16 private let thumbnailView: UIView = {
17 let view = UIView()
18 view.layer.cornerRadius = 14
19 view.clipsToBounds = true
20 return view
21 }()
22
23 private let titleLabel: UILabel = {
24 let label = UILabel()
25 label.font = .systemFont(ofSize: 24, weight: .semibold)
26 label.textColor = .white
27 return label
28 }()
29
30 private let episodeLabel: UILabel = {
31 let label = UILabel()
32 label.font = .systemFont(ofSize: 20, weight: .regular)
33 label.textColor = UIColor.white.withAlphaComponent(0.7)
34 return label
35 }()
36
37 private let remainingLabel: UILabel = {
38 let label = UILabel()
39 label.font = .systemFont(ofSize: 18, weight: .medium)
40 label.textColor = UIColor.white.withAlphaComponent(0.5)
41 return label
42 }()
43
44 private let progressView: UIProgressView = {
45 let pv = UIProgressView(progressViewStyle: .default)
46 pv.trackTintColor = UIColor.white.withAlphaComponent(0.2)
47 pv.progressTintColor = .systemRed
48 pv.layer.cornerRadius = 3
49 pv.clipsToBounds = true
50 return pv
51 }()
52
53 override init(frame: CGRect) {
54 super.init(frame: frame)
55 setupViews()
56 }
57
58 required init?(coder: NSCoder) {
59 fatalError("init(coder:) not supported")
60 }
61
62 func configure(with item: TVResumeItem) {
63 titleLabel.text = item.title
64 episodeLabel.text = item.episodeInfo
65 remainingLabel.text = item.remainingTime
66 progressView.progress = item.progress
67 thumbnailView.backgroundColor = UIColor(hex: item.colorHex) ?? .darkGray
68 }
69
70 // MARK: - Focus Engine
71
72 override var canBecomeFocused: Bool { true }
73
74 override func didUpdateFocus(
75 in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator
76 ) {
77 coordinator.addCoordinatedAnimations(
78 {
79 if self.isFocused {
80 self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
81 self.layer.shadowColor = UIColor.black.cgColor
82 self.layer.shadowOpacity = 0.5
83 self.layer.shadowRadius = 15
84 self.layer.shadowOffset = CGSize(width: 0, height: 12)
85 } else {
86 self.transform = .identity
87 self.layer.shadowOpacity = 0
88 }
89 }, completion: nil)
90 }
91
92 private func setupViews() {
93 clipsToBounds = false
94 contentView.clipsToBounds = false
95
96 let textStack = UIStackView(arrangedSubviews: [titleLabel, episodeLabel, remainingLabel])
97 textStack.axis = .vertical
98 textStack.spacing = 6
99
100 let mainStack = UIStackView(arrangedSubviews: [thumbnailView, progressView, textStack])
101 mainStack.axis = .vertical
102 mainStack.spacing = 12
103 mainStack.translatesAutoresizingMaskIntoConstraints = false
104
105 contentView.addSubview(mainStack)
106 NSLayoutConstraint.activate([
107 mainStack.topAnchor.constraint(equalTo: contentView.topAnchor),
108 mainStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
109 mainStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
110 mainStack.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor),
111 thumbnailView.heightAnchor.constraint(
112 equalTo: thumbnailView.widthAnchor, multiplier: 0.56),
113 progressView.heightAnchor.constraint(equalToConstant: 6),
114 ])
115
116 isAccessibilityElement = true
117 accessibilityTraits = .button
118 }
119
120 override var accessibilityLabel: String? {
121 get { "\(titleLabel.text ?? ""). \(episodeLabel.text ?? ""). \(remainingLabel.text ?? "")" }
122 set {}
123 }
124 }