Initial commit
This commit is contained in:
167
mlbTVOS/Views/Components/ScoresTickerView.swift
Normal file
167
mlbTVOS/Views/Components/ScoresTickerView.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ScoresTickerView: View {
|
||||
var gamesOverride: [Game]? = nil
|
||||
|
||||
@Environment(GamesViewModel.self) private var viewModel
|
||||
|
||||
private var entries: [Game] {
|
||||
gamesOverride ?? viewModel.games
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !entries.isEmpty {
|
||||
TickerMarqueeView(text: tickerText)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 22)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.fill(.black.opacity(0.72))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.strokeBorder(.white.opacity(0.08), lineWidth: 1)
|
||||
}
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
private var tickerText: String {
|
||||
entries.map(tickerSummary(for:)).joined(separator: " | ")
|
||||
}
|
||||
|
||||
private func tickerSummary(for game: Game) -> String {
|
||||
let matchup = "\(game.awayTeam.code) \(game.awayTeam.score.map(String.init) ?? "-") - \(game.homeTeam.code) \(game.homeTeam.score.map(String.init) ?? "-")"
|
||||
|
||||
if game.isLive, let linescore = game.linescore {
|
||||
let inning = linescore.currentInningOrdinal ?? game.currentInningDisplay ?? "LIVE"
|
||||
let state = (linescore.inningState ?? linescore.inningHalf ?? "Live").uppercased()
|
||||
let outs = linescore.outs ?? 0
|
||||
return "\(matchup) \(state) \(inning.uppercased()) • \(outs) OUT\(outs == 1 ? "" : "S")"
|
||||
}
|
||||
|
||||
if game.isFinal {
|
||||
return "\(matchup) FINAL"
|
||||
}
|
||||
|
||||
if let startTime = game.startTime {
|
||||
return "\(matchup) \(startTime.uppercased())"
|
||||
}
|
||||
|
||||
return "\(matchup) \(game.status.label.uppercased())"
|
||||
}
|
||||
}
|
||||
|
||||
private struct TickerMarqueeView: UIViewRepresentable {
|
||||
let text: String
|
||||
|
||||
func makeUIView(context: Context) -> TickerMarqueeContainerView {
|
||||
let view = TickerMarqueeContainerView()
|
||||
view.setText(text)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: TickerMarqueeContainerView, context: Context) {
|
||||
uiView.setText(text)
|
||||
}
|
||||
}
|
||||
|
||||
private final class TickerMarqueeContainerView: UIView {
|
||||
private let trackView = UIView()
|
||||
private let primaryLabel = UILabel()
|
||||
private let secondaryLabel = UILabel()
|
||||
private let spacing: CGFloat = 48
|
||||
private let pointsPerSecond: CGFloat = 64
|
||||
|
||||
private var currentText = ""
|
||||
private var previousBoundsWidth: CGFloat = 0
|
||||
private var previousContentWidth: CGFloat = 0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
clipsToBounds = true
|
||||
isUserInteractionEnabled = false
|
||||
|
||||
let baseFont = UIFont.systemFont(ofSize: 15, weight: .bold)
|
||||
let roundedFont = UIFont(descriptor: baseFont.fontDescriptor.withDesign(.rounded) ?? baseFont.fontDescriptor, size: 15)
|
||||
|
||||
[primaryLabel, secondaryLabel].forEach { label in
|
||||
label.font = roundedFont
|
||||
label.textColor = UIColor.white.withAlphaComponent(0.92)
|
||||
label.numberOfLines = 1
|
||||
label.lineBreakMode = .byClipping
|
||||
trackView.addSubview(label)
|
||||
}
|
||||
|
||||
addSubview(trackView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setText(_ text: String) {
|
||||
guard currentText != text else { return }
|
||||
currentText = text
|
||||
primaryLabel.text = text
|
||||
secondaryLabel.text = text
|
||||
previousContentWidth = 0
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
guard bounds.width > 0, bounds.height > 0 else { return }
|
||||
|
||||
let contentWidth = ceil(primaryLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: bounds.height)).width)
|
||||
let contentHeight = bounds.height
|
||||
|
||||
primaryLabel.frame = CGRect(x: 0, y: 0, width: contentWidth, height: contentHeight)
|
||||
secondaryLabel.frame = CGRect(x: contentWidth + spacing, y: 0, width: contentWidth, height: contentHeight)
|
||||
|
||||
let cycleWidth = contentWidth + spacing
|
||||
|
||||
if contentWidth <= bounds.width {
|
||||
trackView.layer.removeAllAnimations()
|
||||
secondaryLabel.isHidden = true
|
||||
trackView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: contentHeight)
|
||||
primaryLabel.frame = CGRect(x: 0, y: 0, width: bounds.width, height: contentHeight)
|
||||
primaryLabel.textAlignment = .left
|
||||
previousBoundsWidth = bounds.width
|
||||
previousContentWidth = contentWidth
|
||||
return
|
||||
}
|
||||
|
||||
primaryLabel.textAlignment = .left
|
||||
secondaryLabel.isHidden = false
|
||||
trackView.frame = CGRect(x: 0, y: 0, width: contentWidth * 2 + spacing, height: contentHeight)
|
||||
|
||||
let shouldRestart = abs(previousBoundsWidth - bounds.width) > 0.5
|
||||
|| abs(previousContentWidth - contentWidth) > 0.5
|
||||
|| trackView.layer.animation(forKey: "tickerMarquee") == nil
|
||||
|
||||
previousBoundsWidth = bounds.width
|
||||
previousContentWidth = contentWidth
|
||||
|
||||
guard shouldRestart else { return }
|
||||
|
||||
trackView.layer.removeAllAnimations()
|
||||
trackView.transform = .identity
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "transform.translation.x")
|
||||
animation.fromValue = 0
|
||||
animation.toValue = -cycleWidth
|
||||
animation.duration = Double(cycleWidth / pointsPerSecond)
|
||||
animation.repeatCount = .infinity
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animation.isRemovedOnCompletion = false
|
||||
|
||||
trackView.layer.add(animation, forKey: "tickerMarquee")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user