main
swift 112 lines 3.68 KB
Raw
1 //
2 // TVPosterCell.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 poster cell optimized for TV — large artwork area with focus-driven scaling
13 /// and shadow. Uses the tvOS focus engine for smooth parallax-like interactions.
14 final class TVPosterCell: UICollectionViewCell, JomloSelfConfiguringCell {
15
16 private let posterView: UIView = {
17 let view = UIView()
18 view.layer.cornerRadius = 12
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: .medium)
26 label.textColor = .white
27 label.textAlignment = .center
28 label.numberOfLines = 2
29 return label
30 }()
31
32 private let subtitleLabel: UILabel = {
33 let label = UILabel()
34 label.font = .systemFont(ofSize: 18, weight: .regular)
35 label.textColor = UIColor.white.withAlphaComponent(0.7)
36 label.textAlignment = .center
37 return label
38 }()
39
40 override init(frame: CGRect) {
41 super.init(frame: frame)
42 setupViews()
43 }
44
45 required init?(coder: NSCoder) {
46 fatalError("init(coder:) not supported")
47 }
48
49 func configure(with item: TVContentItem) {
50 titleLabel.text = item.title
51 subtitleLabel.text = item.subtitle
52 posterView.backgroundColor = UIColor(hex: item.colorHex) ?? .darkGray
53 }
54
55 // MARK: - Focus Engine
56
57 override var canBecomeFocused: Bool { true }
58
59 override func didUpdateFocus(
60 in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator
61 ) {
62 coordinator.addCoordinatedAnimations(
63 {
64 if self.isFocused {
65 self.transform = CGAffineTransform(scaleX: 1.08, y: 1.08)
66 self.layer.shadowColor = UIColor.black.cgColor
67 self.layer.shadowOpacity = 0.5
68 self.layer.shadowRadius = 15
69 self.layer.shadowOffset = CGSize(width: 0, height: 15)
70 self.titleLabel.alpha = 1
71 self.subtitleLabel.alpha = 1
72 } else {
73 self.transform = .identity
74 self.layer.shadowOpacity = 0
75 self.titleLabel.alpha = 0.8
76 self.subtitleLabel.alpha = 0.5
77 }
78 }, completion: nil)
79 }
80
81 private func setupViews() {
82 clipsToBounds = false
83 contentView.clipsToBounds = false
84
85 let stack = UIStackView(arrangedSubviews: [posterView, titleLabel, subtitleLabel])
86 stack.axis = .vertical
87 stack.spacing = 12
88 stack.alignment = .center
89 stack.translatesAutoresizingMaskIntoConstraints = false
90
91 contentView.addSubview(stack)
92 NSLayoutConstraint.activate([
93 stack.topAnchor.constraint(equalTo: contentView.topAnchor),
94 stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
95 stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
96 stack.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor),
97 posterView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
98 posterView.heightAnchor.constraint(equalTo: posterView.widthAnchor, multiplier: 1.5),
99 ])
100
101 titleLabel.alpha = 0.8
102 subtitleLabel.alpha = 0.5
103
104 isAccessibilityElement = true
105 accessibilityTraits = .button
106 }
107
108 override var accessibilityLabel: String? {
109 get { "\(titleLabel.text ?? ""). \(subtitleLabel.text ?? "")" }
110 set {}
111 }
112 }