feature/v1
swift 134 lines 4.56 KB
Raw
1 //
2 // TVHeroCell.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 large hero cell for the top-shelf-style featured content area.
13 /// Designed for 1920x1080 TV screens with generous padding and large typography.
14 final class TVHeroCell: UICollectionViewCell, JomloSelfConfiguringCell {
15
16 private let gradientLayer = CAGradientLayer()
17
18 private let titleLabel: UILabel = {
19 let label = UILabel()
20 label.font = .systemFont(ofSize: 56, weight: .bold)
21 label.textColor = .white
22 label.numberOfLines = 2
23 return label
24 }()
25
26 private let taglineLabel: UILabel = {
27 let label = UILabel()
28 label.font = .systemFont(ofSize: 28, weight: .medium)
29 label.textColor = UIColor.white.withAlphaComponent(0.85)
30 label.numberOfLines = 1
31 return label
32 }()
33
34 private let metadataLabel: UILabel = {
35 let label = UILabel()
36 label.font = .systemFont(ofSize: 22, weight: .regular)
37 label.textColor = UIColor.white.withAlphaComponent(0.6)
38 return label
39 }()
40
41 private let focusOverlay: UIView = {
42 let view = UIView()
43 view.backgroundColor = UIColor.white.withAlphaComponent(0.08)
44 view.layer.cornerRadius = 24
45 view.alpha = 0
46 return view
47 }()
48
49 override init(frame: CGRect) {
50 super.init(frame: frame)
51 setupViews()
52 }
53
54 required init?(coder: NSCoder) {
55 fatalError("init(coder:) not supported")
56 }
57
58 override func layoutSubviews() {
59 super.layoutSubviews()
60 gradientLayer.frame = contentView.bounds
61 }
62
63 func configure(with item: TVHeroItem) {
64 titleLabel.text = item.title
65 taglineLabel.text = item.tagline
66 metadataLabel.text = item.metadata
67
68 let topColor = UIColor(hex: item.gradientTop) ?? .darkGray
69 let bottomColor = UIColor(hex: item.gradientBottom) ?? .black
70 gradientLayer.colors = [topColor.cgColor, bottomColor.cgColor]
71 }
72
73 // MARK: - Focus Engine
74
75 override var canBecomeFocused: Bool { true }
76
77 override func didUpdateFocus(
78 in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator
79 ) {
80 coordinator.addCoordinatedAnimations(
81 {
82 if self.isFocused {
83 self.transform = CGAffineTransform(scaleX: 1.03, y: 1.03)
84 self.focusOverlay.alpha = 1
85 self.layer.shadowColor = UIColor.white.cgColor
86 self.layer.shadowOpacity = 0.3
87 self.layer.shadowRadius = 20
88 self.layer.shadowOffset = CGSize(width: 0, height: 10)
89 } else {
90 self.transform = .identity
91 self.focusOverlay.alpha = 0
92 self.layer.shadowOpacity = 0
93 }
94 }, completion: nil)
95 }
96
97 private func setupViews() {
98 contentView.layer.cornerRadius = 24
99 contentView.clipsToBounds = true
100 clipsToBounds = false
101 contentView.layer.insertSublayer(gradientLayer, at: 0)
102 gradientLayer.startPoint = CGPoint(x: 0, y: 0)
103 gradientLayer.endPoint = CGPoint(x: 1, y: 1)
104
105 focusOverlay.translatesAutoresizingMaskIntoConstraints = false
106 contentView.addSubview(focusOverlay)
107
108 let textStack = UIStackView(arrangedSubviews: [titleLabel, taglineLabel, metadataLabel])
109 textStack.axis = .vertical
110 textStack.spacing = 12
111 textStack.alignment = .leading
112 textStack.translatesAutoresizingMaskIntoConstraints = false
113 contentView.addSubview(textStack)
114
115 NSLayoutConstraint.activate([
116 focusOverlay.topAnchor.constraint(equalTo: contentView.topAnchor),
117 focusOverlay.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
118 focusOverlay.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
119 focusOverlay.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
120
121 textStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 60),
122 textStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -60),
123 textStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -50),
124 ])
125
126 isAccessibilityElement = true
127 accessibilityTraits = .button
128 }
129
130 override var accessibilityLabel: String? {
131 get { "\(titleLabel.text ?? ""). \(taglineLabel.text ?? "")" }
132 set {}
133 }
134 }