Я пытаюсь анимировать числовой текст в SwiftUI. Но пока не могу найти желаемого результата! Для более простого понимания, пожалуйста, посмотрите изображение ниже...
Пока вот мой код:
struct AnimatedText: View {
@State var showingText = "234"
var body: some View {
VStack {
HStack {
Spacer()
Text(showingText)
.font(.title)
.transition(.slide)
}.padding(20)
Button("Add Number") {
withAnimation(.spring()) {
showingText += String(Int.random(in: 0..<9))
}
}
}
}
}





Один из способов — разделить каждого персонажа на отдельный Text вид. Тогда переход .push(from: .trailing) достигает желаемого результата:
@State var showingText = "234"
var body: some View {
VStack {
HStack(spacing: 0) {
Spacer()
let charArray = Array(showingText)
ForEach(charArray.indices, id: \.self) { i in
let char = String(charArray[i])
Text(char)
.transition(.push(from: .trailing))
}
}.padding(20)
Button("Add Number") {
withAnimation(.spring()) {
showingText += String(Int.random(in: 0..<9))
}
}
}
}
Этой анимации можно добиться, применив смещение к тексту без анимации, а затем отрицательное смещение к тексту с анимацией. Размер смещения равен ширине текста. Это можно узнать, выполнив всю презентацию в виде наложения и используя GeometryReader для измерения размера текстового отпечатка.
Чтобы сделать его более удобным для повторного использования, вы можете разделить анимированный текст на отдельный View:
struct AnimatedText: View {
let text: String
@State private var overlayOffset = CGFloat.zero
var body: some View {
Text(text)
.hidden()
.overlay {
GeometryReader { proxy in
let w = proxy.size.width
Text(text)
.offset(x: w)
.offset(x: -overlayOffset)
.onAppear { overlayOffset = w }
.onChange(of: w) { oldVal, newVal in
if newVal > oldVal {
withAnimation(.easeOut) {
overlayOffset = newVal
}
}
}
}
}
}
}
Вот как это можно было бы использовать в вашем примере:
VStack {
HStack {
Spacer()
AnimatedText(text: showingText)
.font(.title)
}
.padding(20)
Button("Add Number") {
showingText += String(Int.random(in: 0..<9))
}
}
Для перемещения групп чисел в верхнюю строку можно использовать .matchedGeometryEffect. Вот попытка показать, как это работает, с обновленной версией примера:
@State private var textChunks = [String]()
@State private var bottomText = "0"
@State private var counter = 0
@Namespace private var ns
var body: some View {
VStack(spacing: 20) {
HStack {
Spacer()
Text(".").hidden()
ForEach(Array(textChunks.enumerated()), id: \.offset) { offset, chunk in
Text(chunk)
.matchedGeometryEffect(id: offset, in: ns, isSource: true)
.hidden()
}
}
.animation(.easeInOut, value: textChunks)
HStack {
Spacer()
ZStack(alignment: .trailing) {
ForEach(Array(textChunks.enumerated()), id: \.offset) { offset, chunk in
Text(chunk)
.minimumScaleFactor(0.1)
.matchedGeometryEffect(id: offset, in: ns, isSource: false)
.animation(.easeInOut, value: textChunks)
}
AnimatedText(text: bottomText)
.id(counter)
.transition(.asymmetric(
insertion: .opacity.animation(.easeInOut),
removal: .identity
))
}
.font(.title)
}
HStack(spacing: 20) {
Button("Move up") {
textChunks.append(bottomText)
counter += 1
bottomText = "0"
}
Button("Add Number") {
if bottomText == "0" {
bottomText = String("\(Int.random(in: 1..<9))")
} else {
bottomText += String("\(Int.random(in: 0..<9))")
}
}
Button("Reset") {
withAnimation { textChunks.removeAll() }
counter += 1
bottomText = "0"
}
}
.buttonStyle(.bordered)
}
.padding(20)
}

Большое спасибо за огромные усилия.