import SwiftUI struct MarqueeText: View { let text: String let font: Font let foregroundColor: Color private let speed: CGFloat = 10 // пикселей в секунду @State private var textWidth: CGFloat = 0 @State private var containerWidth: CGFloat = 0 @State private var offset: CGFloat = 0 @State private var animationStarted = false var body: some View { GeometryReader { geo in Text(text) .font(font) .foregroundColor(foregroundColor) .lineLimit(1) .fixedSize() // важно, чтобы не обрезался .background(WidthGetter()) .offset(x: offset) .onAppear { containerWidth = geo.size.width if textWidth > containerWidth, !animationStarted { animationStarted = true let distance = textWidth - containerWidth let duration = Double(distance / speed) withAnimation(.linear(duration: duration).repeatForever(autoreverses: true)) { offset = -distance } } } .clipped() } .frame(height: lineHeight(for: font)) .onPreferenceChange(WidthKey.self) { width in textWidth = width } } private func lineHeight(for font: Font) -> CGFloat { switch font { case .largeTitle: return 34 case .title: return 28 case .title2: return 22 case .title3: return 20 case .headline: return 17 case .body: return 17 case .callout: return 16 case .subheadline: return 15 case .caption: return 13 case .caption2: return 12 default: return 17 } } } // MARK: — Helpers для измерения ширины текста private struct WidthKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } private struct WidthGetter: View { var body: some View { GeometryReader { geo in Color.clear.preference(key: WidthKey.self, value: geo.size.width) } } }