Swiftui How to Animate Each Character in Textfield

SwiftUI how to animate each character in textfield?

You can just create a "Fake" TextField that appears over the real one. Then show the characters in a ForEach.

It is done with FocusState in iOS 15

@available(iOS 15.0, *)
struct AnimatedInputView: View {
@FocusState private var isFocused: Int?
@State var text: String = ""
//If all the fonts match the cursor is better aligned
@State var font: Font = .system(size: 48, weight: .bold, design: .default)
@State var color: Color = .gray
var body: some View {
HStack(alignment: .center, spacing: 0){
//To maintain size in between the 2 views
Text(text)
.font(font)
.opacity(0)
.overlay(
//This textField will be invisible
TextField("", text: $text)
.font(font)
.foregroundColor(.clear)
.focused($isFocused, equals: 1)
)
.background(
ZStack{
HStack(alignment: .center, spacing: 0, content: {
//You need an array of unique/identifiable characters
let uniqueArray = text.uniqueCharacters()
ForEach(uniqueArray, id: \.id, content: { char in
CharView(char: char.char, isLast: char == uniqueArray.last, font: font)

})
})
}.opacity(1)
.minimumScaleFactor(0.1)

)

.onAppear(perform: {
//Bring focus to the hidden TextField
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
isFocused = 1
})
})
}
.padding()
.border(color)
.font(.title)
//Bring focus to the hidden textfield
.onTapGesture {
isFocused = 1
}
}
}
struct CharView: View{
var char: Character
var isLast: Bool
var font: Font
@State var scale: CGFloat = 0.75
var body: some View{
Text(char.description)
.font(font)
.minimumScaleFactor(0.1)
.scaleEffect(scale)
.onAppear(perform: {
//Animate only if last character
if isLast{
withAnimation(.linear(duration: 0.5)){
scale = 1
}
}else{
scale = 1
}
})
}
}
@available(iOS 15.0, *)
struct AnimatedInputView_Previews: PreviewProvider {
static var previews: some View {
AnimatedInputView()
}
}
//Convert String to Unique characers
extension String{
func uniqueCharacters() -> [UniqueCharacter]{
let array: [Character] = Array(self)
return array.uniqueCharacters()
}
func numberOnly() -> String {
self.trimmingCharacters(in: CharacterSet(charactersIn: "-0123456789.").inverted)
}

}
extension Array where Element == Character {

func uniqueCharacters() -> [UniqueCharacter]{
var array: [UniqueCharacter] = []

for char in self{
array.append(UniqueCharacter(char: char))
}
return array
}

}

//String/Characters can be repeating so yu have to make them a unique value
struct UniqueCharacter: Identifiable, Equatable{
var char: Character
var id: UUID = UUID()
}

Here is a sample version that. only takes numbers like the calculator sample

import SwiftUI

@available(iOS 15.0, *)
struct AnimatedInputView: View {
@FocusState private var isFocused: Int?
@State var text: String = ""
//If all the fonts match the cursor is better aligned
@State var font: Font = .system(size: 48, weight: .bold, design: .default)
@State var color: Color = .gray
var body: some View {
HStack(alignment: .center, spacing: 0){
Text("$").font(font)
//To maintain size in between the 2 views
Text(text)
.font(font)
.opacity(0)
.overlay(
//This textField will be invisible
TextField("", text: $text)
.font(font)
.foregroundColor(.clear)
.focused($isFocused, equals: 1)
.onChange(of: text, perform: { value in
if Double(text) == nil{
//Leaves the negative and decimal period
text = text.numberOnly()
}
//This condition can be improved.
//Checks for 2 occurences of the decimal period
//Possible solution
while text.components(separatedBy: ".").count > 2{
color = .red
text.removeLast()
}

//This condition can be improved.
//Checks for 2 occurences of the negative
//Possible solution
while text.components(separatedBy: "-").count > 2{
color = .red
text.removeLast()
}
color = .gray

})
)
.background(
ZStack{
HStack(alignment: .center, spacing: 0, content: {
//You need an array of unique/identifiable characters
let uniqueArray = text.uniqueCharacters()
ForEach(uniqueArray, id: \.id, content: { char in
CharView(char: char.char, isLast: char == uniqueArray.last, font: font)

})
})
}.opacity(1)
.minimumScaleFactor(0.1)

)

.onAppear(perform: {
//Bring focus to the hidden TextField
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
isFocused = 1
})
})
}
.padding()
.border(color)
.font(.title)
//Bring focus to the hidden textfield
.onTapGesture {
isFocused = 1
}
}
}

Animating Text in SwiftUI

Here is a demo of possible approach (scratchy - you can redesign it to extension, modifier, or separate view)

Tested with Xcode 11.4 / iOS 13.4

demo

struct ContentView: View {
@State var shortString = true
var body: some View {
VStack {
if shortString {
Text("This is short.").font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
if !shortString {
Text("This is considerably longer.").font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}

Button(action: {self.shortString.toggle()}) {
Text("Toggle").padding()
}
}
}
}

Any suggestions for shrinking an animated gif's dimensions?

I use this way:
- decrease zoom of Preview to 75% (or resize window of Simulator)
- use QuickTimePlayer region-based Screen Recording
- use https://ezgif.com/video-to-gif for converting to GIF

Animating Text in Swift UI

There is a pure way of animating text in SwiftUI. Here's an implementation of your progress indicator using the AnimatableModifier protocol in SwiftUI:

Sample Image

I've written an extensive article documenting the use of AnimatableModifier (and its bugs). It includes the progress indicator too. You can read it here: https://swiftui-lab.com/swiftui-animations-part3/

struct ContentView: View {
@State private var percent: CGFloat = 0

var body: some View {
VStack {
Spacer()
Color.clear.overlay(Indicator(pct: self.percent))

Spacer()
HStack(spacing: 10) {
MyButton(label: "0%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0 } }

MyButton(label: "27%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0.27 } }

MyButton(label: "100%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 1.0 } }
}
}.navigationBarTitle("Example 10")
}
}

struct Indicator: View {
var pct: CGFloat

var body: some View {
return Circle()
.fill(LinearGradient(gradient: Gradient(colors: [.blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing))
.frame(width: 150, height: 150)
.modifier(PercentageIndicator(pct: self.pct))
}
}

struct PercentageIndicator: AnimatableModifier {
var pct: CGFloat = 0

var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}

func body(content: Content) -> some View {
content
.overlay(ArcShape(pct: pct).foregroundColor(.red))
.overlay(LabelView(pct: pct))
}

struct ArcShape: Shape {
let pct: CGFloat

func path(in rect: CGRect) -> Path {

var p = Path()

p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: .degrees(0),
endAngle: .degrees(360.0 * Double(pct)), clockwise: false)

return p.strokedPath(.init(lineWidth: 10, dash: [6, 3], dashPhase: 10))
}
}

struct LabelView: View {
let pct: CGFloat

var body: some View {
Text("\(Int(pct * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
}

Animating the change of colors when text field is changed using Swift UI

The problem is that you aren't actually offsetting the color, so it just does a color fade by default. I made the green underline a separate part, that offsets depending on where the cursor currently is.

To solve the animation problem where it animates even when doing something unrelated - use .animation(_:value:) instead - providing a value where the animation only happens when that value changes.

Change the otpText method to:

@available(iOS 13.0, *)
private func otpText(text: String, isEditing: Bool, beforeCursor: Bool, afterCursor: Bool) -> some View {
return Text(text)
.font(Font.custom("GTWalsheim-Regular", size: 34))
.frame(width: textBoxWidth, height: textBoxHeight)
.background(VStack{
Spacer()

ZStack {
Capsule()
.frame(width: textBoxWidth, height: 2)
.foregroundColor(Color(hex: "#BCBEC0"))

Capsule()
.frame(width: textBoxWidth, height: 2)
.foregroundColor(Color(hex: "#367878"))
.offset(x: (beforeCursor ? textBoxWidth : 0) + (afterCursor ? -textBoxWidth : 0))
.animation(.easeInOut, value: [beforeCursor, afterCursor])
.opacity(isEditing ? 1 : 0)
}
.clipped()
})
.padding(paddingOfBox)
}

And where you use this function (albeit, quite messy and you could clean up your code with a loop, and an array instead of otp1, otp2, etc.):

HStack (spacing: spaceBetweenLines){
otpText(text: viewModel.otp1, isEditing: viewModel.isEditing, beforeCursor: 0 < viewModel.otpField.count, afterCursor: false)
otpText(text: viewModel.otp2, isEditing: viewModel.isEditing, beforeCursor: 1 < viewModel.otpField.count, afterCursor: viewModel.otpField.count < 1)
otpText(text: viewModel.otp3, isEditing: viewModel.isEditing, beforeCursor: 2 < viewModel.otpField.count, afterCursor: viewModel.otpField.count < 2)
otpText(text: viewModel.otp4, isEditing: viewModel.isEditing, beforeCursor: 3 < viewModel.otpField.count, afterCursor: viewModel.otpField.count < 3)
otpText(text: viewModel.otp5, isEditing: viewModel.isEditing, beforeCursor: 4 < viewModel.otpField.count, afterCursor: viewModel.otpField.count < 4)
otpText(text: viewModel.otp6, isEditing: viewModel.isEditing, beforeCursor: false, afterCursor: viewModel.otpField.count < 5)
}

Result:

Result


To clean up your code, remove all the otp1, otp2, etc and replace with below:

func otp(digit: Int) -> String {
guard otpField.count >= digit else {
return ""
}
return String(Array(otpField)[digit - 1])
}

Then you can simplify the view code:

HStack (spacing: spaceBetweenLines){
ForEach(1 ... 6, id: \.self) { digit in
otpText(
text: viewModel.otp(digit: digit),
isEditing: viewModel.isEditing,
beforeCursor: digit - 1 < viewModel.otpField.count,
afterCursor: viewModel.otpField.count < digit - 1
)
}
}

How to animate/transition text value change in SwiftUI

So it turns out this is really easy

Text(textValue)
.font(.largeTitle)
.frame(width: 200, height: 200)
.transition(.opacity)
.id("MyTitleComponent" + textValue)

Note the additional id at the end. SwiftUI uses this to decide if it's dealing with the same view or not when doing a redraw. If the id is different then it assumes the previous view was removed and this one has been added. Because it's adding a new view it applies the specified transition as expected.

NB: It's quite possible that this id should be unique for the entire view tree so you probably want to take care to namespace it accordingly (hence the MyTitleComponent prefix in the example).

Animating a SwiftUI state change while existing animation is running causes Form to be in an inconsistent state

Just tested this recently and discovered this issue no longer occurs (at least as of iOS 15.3, but probably all of iOS 15).

How to detect live changes on TextField in SwiftUI?

You can create a binding with a custom closure, like this:

struct ContentView: View {
@State var location: String = ""

var body: some View {
let binding = Binding<String>(get: {
self.location
}, set: {
self.location = $0
// do whatever you want here
})

return VStack {
Text("Current location: \(location)")
TextField("Search Location", text: binding)
}

}
}


Related Topics



Leave a reply



Submit