@@ -10,82 +10,276 @@ import UIKit
1010import NVActivityIndicatorView
1111
1212class StationTableViewCell : UITableViewCell {
13-
13+
14+ // MARK: - UI
15+
16+ private let cardBlurView : UIVisualEffectView = {
17+ let blur = UIBlurEffect ( style: . systemUltraThinMaterialDark)
18+ let view = UIVisualEffectView ( effect: blur)
19+ view. layer. cornerRadius = 14
20+ view. clipsToBounds = true
21+ view. layer. borderWidth = 0.5
22+ view. layer. borderColor = UIColor . white. withAlphaComponent ( 0.12 ) . cgColor
23+ view. translatesAutoresizingMaskIntoConstraints = false
24+ return view
25+ } ( )
26+
27+ private let cardView : UIView = {
28+ let view = UIView ( )
29+ view. backgroundColor = . clear
30+ view. layer. cornerRadius = 14
31+ view. layer. shadowColor = UIColor . black. cgColor
32+ view. layer. shadowOpacity = 0.2
33+ view. layer. shadowOffset = CGSize ( width: 0 , height: 2 )
34+ view. layer. shadowRadius = 8
35+ view. translatesAutoresizingMaskIntoConstraints = false
36+ return view
37+ } ( )
38+
39+ private let pulseRingView : UIView = {
40+ let view = UIView ( )
41+ view. layer. cornerRadius = 12
42+ view. layer. borderWidth = 2.5
43+ view. layer. borderColor = UIColor . white. cgColor
44+ view. layer. shadowColor = UIColor . white. cgColor
45+ view. layer. shadowOpacity = 0.5
46+ view. layer. shadowOffset = . zero
47+ view. layer. shadowRadius = 6
48+ view. alpha = 0
49+ view. translatesAutoresizingMaskIntoConstraints = false
50+ return view
51+ } ( )
52+
1453 let stationImageView : UIImageView = {
1554 let imageView = UIImageView ( )
1655 imageView. contentMode = . scaleAspectFill
1756 imageView. clipsToBounds = true
18- NSLayoutConstraint . activate ( [
19- imageView. heightAnchor. constraint ( equalToConstant: 75 ) ,
20- imageView. widthAnchor. constraint ( equalToConstant: 110 )
21- ] )
57+ imageView. layer. cornerRadius = 12
58+ imageView. translatesAutoresizingMaskIntoConstraints = false
2259 return imageView
2360 } ( )
24-
61+
62+ private let artworkShadowView : UIView = {
63+ let view = UIView ( )
64+ view. layer. shadowColor = UIColor . black. cgColor
65+ view. layer. shadowOpacity = 0.35
66+ view. layer. shadowOffset = CGSize ( width: 0 , height: 3 )
67+ view. layer. shadowRadius = 8
68+ view. layer. cornerRadius = 12
69+ view. translatesAutoresizingMaskIntoConstraints = false
70+ return view
71+ } ( )
72+
2573 let titleLabel : UILabel = {
2674 let label = UILabel ( )
27- label. font = . preferredFont( forTextStyle: . title3 )
28- label. numberOfLines = 2
75+ label. font = . preferredFont( forTextStyle: . headline )
76+ label. numberOfLines = 1
2977 label. translatesAutoresizingMaskIntoConstraints = false
3078 return label
3179 } ( )
32-
80+
81+ private let subtitleStack : UIStackView = {
82+ let stack = UIStackView ( )
83+ stack. axis = . horizontal
84+ stack. spacing = 6
85+ stack. alignment = . center
86+ stack. translatesAutoresizingMaskIntoConstraints = false
87+ return stack
88+ } ( )
89+
3390 let subtitleLabel : UILabel = {
3491 let label = UILabel ( )
35- label. font = . preferredFont( forTextStyle: . footnote)
92+ label. font = . preferredFont( forTextStyle: . subheadline)
93+ label. textColor = . white. withAlphaComponent ( 0.55 )
94+ label. numberOfLines = 1
3695 label. translatesAutoresizingMaskIntoConstraints = false
3796 return label
3897 } ( )
39-
98+
99+ private let equalizerView : NVActivityIndicatorView = {
100+ let view = NVActivityIndicatorView ( frame: . zero, type: . audioEqualizer, color: . white, padding: nil )
101+ view. translatesAutoresizingMaskIntoConstraints = false
102+ NSLayoutConstraint . activate ( [
103+ view. widthAnchor. constraint ( equalToConstant: 16 ) ,
104+ view. heightAnchor. constraint ( equalToConstant: 12 )
105+ ] )
106+ view. alpha = 0
107+ return view
108+ } ( )
109+
110+ private var isAnimatingPulse = false
111+
112+ // MARK: - Init
113+
40114 override init ( style: UITableViewCell . CellStyle , reuseIdentifier: String ? ) {
41115 super. init ( style: style, reuseIdentifier: reuseIdentifier)
42116 setupViews ( )
43117 }
44-
118+
119+ required init ? ( coder: NSCoder ) {
120+ fatalError ( " init(coder:) has not been implemented " )
121+ }
122+
45123 override func prepareForReuse( ) {
46124 super. prepareForReuse ( )
47- titleLabel. text = nil
48- subtitleLabel. text = nil
125+ titleLabel. text = nil
126+ subtitleLabel. text = nil
49127 stationImageView. image = nil
128+ stopPulseAnimation ( )
129+ pulseRingView. alpha = 0
130+ equalizerView. stopAnimating ( )
131+ equalizerView. alpha = 0
50132 }
51-
52- required init ? ( coder: NSCoder ) {
53- fatalError ( " init(coder:) has not been implemented " )
133+
134+ // MARK: - Highlight / Tap Feedback
135+
136+ override func setHighlighted( _ highlighted: Bool , animated: Bool ) {
137+ super. setHighlighted ( highlighted, animated: animated)
138+ let scale : CGFloat = highlighted ? 0.97 : 1.0
139+ UIView . animate ( withDuration: 0.3 , delay: 0 , usingSpringWithDamping: 0.8 , initialSpringVelocity: 0.5 ) {
140+ self . cardView. transform = CGAffineTransform ( scaleX: scale, y: scale)
141+ self . cardBlurView. transform = CGAffineTransform ( scaleX: scale, y: scale)
142+ }
54143 }
55-
144+
145+ // MARK: - Setup
146+
56147 private func setupViews( ) {
57-
58- selectionStyle = . default
59-
60- let vStackView = UIStackView ( arrangedSubviews: [ titleLabel, subtitleLabel] )
61- vStackView. spacing = 8
148+ selectionStyle = . none
149+ backgroundColor = . clear
150+ contentView. backgroundColor = . clear
151+
152+ // Card shadow container + blur
153+ contentView. addSubview ( cardView)
154+ cardView. addSubview ( cardBlurView)
155+
156+ // Artwork container: shadow view + pulse ring + image
157+ let artworkContainer = UIView ( )
158+ artworkContainer. translatesAutoresizingMaskIntoConstraints = false
159+
160+ artworkContainer. addSubview ( artworkShadowView)
161+ artworkContainer. addSubview ( pulseRingView)
162+ artworkContainer. addSubview ( stationImageView)
163+
164+ // Subtitle stack: label + equalizer
165+ subtitleStack. addArrangedSubview ( equalizerView)
166+ subtitleStack. addArrangedSubview ( subtitleLabel)
167+
168+ // Labels stack
169+ let vStackView = UIStackView ( arrangedSubviews: [ titleLabel, subtitleStack] )
170+ vStackView. spacing = 4
62171 vStackView. axis = . vertical
63172 vStackView. translatesAutoresizingMaskIntoConstraints = false
64-
65- let hStackView = UIStackView ( arrangedSubviews: [ stationImageView, vStackView] )
66- hStackView. spacing = 8
173+
174+ // Main horizontal stack
175+ let hStackView = UIStackView ( arrangedSubviews: [ artworkContainer, vStackView] )
176+ hStackView. spacing = 14
67177 hStackView. axis = . horizontal
68178 hStackView. alignment = . center
69179 hStackView. translatesAutoresizingMaskIntoConstraints = false
70-
71- contentView. addSubview ( hStackView)
72-
180+
181+ cardBlurView. contentView. addSubview ( hStackView)
182+
183+ let artworkSize : CGFloat = 70
184+ let pulseInset : CGFloat = - 4
185+
73186 NSLayoutConstraint . activate ( [
74- hStackView. topAnchor. constraint ( equalTo: contentView. layoutMarginsGuide. topAnchor) ,
75- hStackView. trailingAnchor. constraint ( equalTo: contentView. layoutMarginsGuide. trailingAnchor) ,
76- hStackView. bottomAnchor. constraint ( equalTo: contentView. layoutMarginsGuide. bottomAnchor) ,
77- hStackView. leadingAnchor. constraint ( equalTo: contentView. layoutMarginsGuide. leadingAnchor)
187+ // Card inset from contentView
188+ cardView. topAnchor. constraint ( equalTo: contentView. topAnchor, constant: 5 ) ,
189+ cardView. bottomAnchor. constraint ( equalTo: contentView. bottomAnchor, constant: - 5 ) ,
190+ cardView. leadingAnchor. constraint ( equalTo: contentView. leadingAnchor, constant: 16 ) ,
191+ cardView. trailingAnchor. constraint ( equalTo: contentView. trailingAnchor, constant: - 16 ) ,
192+
193+ // Blur fills card
194+ cardBlurView. topAnchor. constraint ( equalTo: cardView. topAnchor) ,
195+ cardBlurView. bottomAnchor. constraint ( equalTo: cardView. bottomAnchor) ,
196+ cardBlurView. leadingAnchor. constraint ( equalTo: cardView. leadingAnchor) ,
197+ cardBlurView. trailingAnchor. constraint ( equalTo: cardView. trailingAnchor) ,
198+
199+ // HStack inside blur content
200+ hStackView. topAnchor. constraint ( equalTo: cardBlurView. contentView. topAnchor, constant: 12 ) ,
201+ hStackView. bottomAnchor. constraint ( equalTo: cardBlurView. contentView. bottomAnchor, constant: - 12 ) ,
202+ hStackView. leadingAnchor. constraint ( equalTo: cardBlurView. contentView. leadingAnchor, constant: 14 ) ,
203+ hStackView. trailingAnchor. constraint ( equalTo: cardBlurView. contentView. trailingAnchor, constant: - 14 ) ,
204+
205+ // Artwork container size
206+ artworkContainer. widthAnchor. constraint ( equalToConstant: artworkSize) ,
207+ artworkContainer. heightAnchor. constraint ( equalToConstant: artworkSize) ,
208+
209+ // Image fills container
210+ stationImageView. topAnchor. constraint ( equalTo: artworkContainer. topAnchor) ,
211+ stationImageView. bottomAnchor. constraint ( equalTo: artworkContainer. bottomAnchor) ,
212+ stationImageView. leadingAnchor. constraint ( equalTo: artworkContainer. leadingAnchor) ,
213+ stationImageView. trailingAnchor. constraint ( equalTo: artworkContainer. trailingAnchor) ,
214+
215+ // Shadow matches image
216+ artworkShadowView. topAnchor. constraint ( equalTo: stationImageView. topAnchor) ,
217+ artworkShadowView. bottomAnchor. constraint ( equalTo: stationImageView. bottomAnchor) ,
218+ artworkShadowView. leadingAnchor. constraint ( equalTo: stationImageView. leadingAnchor) ,
219+ artworkShadowView. trailingAnchor. constraint ( equalTo: stationImageView. trailingAnchor) ,
220+
221+ // Pulse ring slightly larger than artwork
222+ pulseRingView. topAnchor. constraint ( equalTo: stationImageView. topAnchor, constant: pulseInset) ,
223+ pulseRingView. bottomAnchor. constraint ( equalTo: stationImageView. bottomAnchor, constant: - pulseInset) ,
224+ pulseRingView. leadingAnchor. constraint ( equalTo: stationImageView. leadingAnchor, constant: pulseInset) ,
225+ pulseRingView. trailingAnchor. constraint ( equalTo: stationImageView. trailingAnchor, constant: - pulseInset) ,
78226 ] )
79227 }
228+
229+ // MARK: - Now Playing
230+
231+ func setNowPlaying( isPlaying: Bool , isCurrentStation: Bool ) {
232+ guard isCurrentStation else {
233+ stopPulseAnimation ( )
234+ pulseRingView. alpha = 0
235+ equalizerView. stopAnimating ( )
236+ equalizerView. alpha = 0
237+ return
238+ }
239+
240+ if isPlaying {
241+ startPulseAnimation ( )
242+ equalizerView. startAnimating ( )
243+ equalizerView. alpha = 0.7
244+ } else {
245+ stopPulseAnimation ( )
246+ pulseRingView. alpha = 0.25
247+ pulseRingView. transform = . identity
248+ equalizerView. stopAnimating ( )
249+ equalizerView. alpha = 0.4
250+ }
251+ }
252+
253+ // MARK: - Pulse Animation
254+
255+ private func startPulseAnimation( ) {
256+ guard !isAnimatingPulse else { return }
257+ isAnimatingPulse = true
258+
259+ pulseRingView. alpha = 0
260+ pulseRingView. transform = . identity
261+
262+ UIView . animate ( withDuration: 1.5 , delay: 0 , options: [ . repeat , . autoreverse, . curveEaseInOut] ) {
263+ self . pulseRingView. alpha = 0.4
264+ self . pulseRingView. transform = CGAffineTransform ( scaleX: 1.06 , y: 1.06 )
265+ }
266+ }
267+
268+ private func stopPulseAnimation( ) {
269+ guard isAnimatingPulse else { return }
270+ isAnimatingPulse = false
271+ pulseRingView. layer. removeAllAnimations ( )
272+ pulseRingView. transform = . identity
273+ }
80274}
81275
276+ // MARK: - Configuration
277+
82278extension StationTableViewCell {
83279 func configureStationCell( station: RadioStation ) {
84-
85- // Configure the cell...
86280 titleLabel. text = station. name
87281 subtitleLabel. text = station. desc
88-
282+
89283 station. getImage { [ weak self] image in
90284 self ? . stationImageView. image = image
91285 }
0 commit comments