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

Time:09-17

I created an OTP page that changes the color of underlines if the text field with the corresponding line is selected. The color changes properly but I want to add animation that the color of the line will change slowly like there's an animation. I have attached an example of what is expected and the current behavior.

I tried to add ease in animation but there's a problem when dismissing the number and text. They both don't go down or transition at the same time. In the recording the lines went down a little behind when I tapped done.

PS. I can only use Xcode 12.3 version.

import SwiftUI

@available(iOS 13.0, *)
class OTPViewModel: ObservableObject {
    
    @Published var otpField = "" {
        didSet {
            isNextTypedArr = Array(repeating: false, count: 6)
            guard otpField.count <= 6,
                  otpField.last?.isNumber ?? true else {
                otpField = oldValue
                return
            }
            if otpField.count < 6 {
                isNextTypedArr[otpField.count] = true
            }
        }
    }
    var otp1: String {
        guard otpField.count >= 1 else {
            return ""
        }
        return String(Array(otpField)[0])
    }
    var otp2: String {
        guard otpField.count >= 2 else {
            return ""
        }
        return String(Array(otpField)[1])
    }
    var otp3: String {
        guard otpField.count >= 3 else {
            return ""
        }
        return String(Array(otpField)[2])
    }
    var otp4: String {
        guard otpField.count >= 4 else {
            return ""
        }
        return String(Array(otpField)[3])
    }
    
    var otp5: String {
        guard otpField.count >= 5 else {
            return ""
        }
        return String(Array(otpField)[4])
    }
    
    var otp6: String {
        guard otpField.count >= 6 else {
            return ""
        }
        return String(Array(otpField)[5])
    }
    
    @Published var isNextTypedArr = Array(repeating: false, count: 6)
    
    @Published var borderColor: Color = .black
    
    @Published var isEditing = false {
        didSet {
            isNextTypedArr = Array(repeating: false, count: 6)
            if isEditing && otpField.count < 6 {
                isNextTypedArr[otpField.count] = true
            }
        }
    }
}

@available(iOS 13.0, *)
struct CXLRPOTPView: View {
    
    @ObservedObject var viewModel = OTPViewModel()
    
    let textBoxWidth = UIScreen.main.bounds.width / 8
    let textBoxHeight = UIScreen.main.bounds.width / 8
    let spaceBetweenLines: CGFloat = 16
    let paddingOfBox: CGFloat = 1
    var textFieldOriginalWidth: CGFloat {
        (textBoxWidth*6) (spaceBetweenLines*3) ((paddingOfBox*2)*3)
    }
    
    var body: some View {
        VStack {
            ZStack {
                HStack (spacing: spaceBetweenLines){
                    otpText(text: viewModel.otp1, isNextTyped: $viewModel.isNextTypedArr[0])
                    otpText(text: viewModel.otp2, isNextTyped: $viewModel.isNextTypedArr[1])
                    otpText(text: viewModel.otp3, isNextTyped: $viewModel.isNextTypedArr[2])
                    otpText(text: viewModel.otp4, isNextTyped: $viewModel.isNextTypedArr[3])
                    otpText(text: viewModel.otp5, isNextTyped: $viewModel.isNextTypedArr[4])
                    otpText(text: viewModel.otp6, isNextTyped: $viewModel.isNextTypedArr[5])
                }
                
                TextField("", text: $viewModel.otpField) { isEditing in
                    viewModel.isEditing = isEditing
                }
                .frame(width: viewModel.isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight)
                .textContentType(.oneTimeCode)
                .foregroundColor(.clear)
                .accentColor(.clear)
                .background(Color.clear)
                .keyboardType(.numberPad)
            }
        }
    }
    
    @available(iOS 13.0, *)
    private func otpText(text: String, isNextTyped: Binding<Bool>) -> some View {
        return Text(text)
            .font(Font.custom("GTWalsheim-Regular", size: 34))
            .frame(width: textBoxWidth, height: textBoxHeight)
            .background(VStack{
                Spacer()
                RoundedRectangle(cornerRadius: 1)
                    .frame(height: 2)
                    .foregroundColor(isNextTyped.wrappedValue ? Color(hex: "#367878") : Color(hex: "#BCBEC0"))
                    .animation(.easeIn) //this made the text bounced.
            })
            .padding(paddingOfBox)
    }
}

CodePudding user response:

The issue is that you aren't properly offsetting the color, therefore it defaults to a color fade. The green underlining is a separate component that shifts based on where the pointer is.

CodePudding user response:

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
        )
    }
}
  • Related