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 } } }
textfieldbut not necessarily every each character in it?