As a user types characters in a textfield, I would like to display some animation on each newly typed character (kinda like how Cash App animates numbers but I'd like to implement it for alphabetical characters as well).
Is it possible to do this in SwiftUI? My intuition is that I might have to bridge to UIKit for more nuanced access to a textfield
's element but not sure how to actually implement that.
CodePudding user response:
You can just create a "Fake" TextField
that sits over the real one. Then show the characters in a ForEach
, you lose the ability to tap and put the cursor in a custom location because the real text field is hidden.
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
}
}
}