1

I created an OTP field which will animate the lines each time you type a digit. This works well. However, I want the animation of green lines to run one at time(from left to right) when pasting just like when typing a number.

Currently when you paste a text, all lines are animating simulaneously as seen in the video.

Current Behavior

The expected should be that the line should turn green one at a time from left to right when pasting.

Please note that my paste button is a custom button I created as a workaround on some other feature.

I can also use only Xcode 12.3.

Here is my code:

Some hex colors and font are custom. You may replace it when replicating.


@available(iOS 13.0, *)
class OTPViewModel: ObservableObject {
    
    var numberOfFields: Int
    
    init(numberOfFields: Int = 6) {
        self.numberOfFields = numberOfFields
    }
    
    @Published var otpField = "" {
        didSet {
            showPasteButton = false
            guard otpField.last?.isNumber ?? true else {
                otpField = oldValue
                return
            }
            if otpField.count == numberOfFields {
                showPasteButton = false
            }
        }
    }
    
    @Published var isEditing = false {
        didSet {
            if !isEditing { showPasteButton = isEditing }
        }
    }
    
    @Published var showPasteButton = false
    
    func otp(digit: Int) -> String {
        guard otpField.count >= digit else {
            return ""
        }
        return String(Array(otpField)[digit - 1])
    }
    
    private func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

@available(iOS 13.0, *)
struct CXLRPOTPView: View {
    @ObservedObject var viewModel = OTPViewModel()
    @Environment(\.colorScheme) var colorScheme
    
    private let textBoxWidth: CGFloat = 41
    private let textBoxHeight = UIScreen.main.bounds.width / 8
    private let spaceBetweenLines: CGFloat = 16
    private let paddingOfBox: CGFloat = 1
    private var textFieldOriginalWidth: CGFloat {
        (textBoxWidth + CGFloat(18)) * CGFloat(viewModel.numberOfFields)
    }
    var body: some View {
        VStack {
            ZStack {
                // DOUBLE TAP AND LONG PRESS LISTENER
                Text("123456")
                    .onTapGesture(count: 2) {
                        viewModel.showPasteButton = true
                    }
                    .frame(width: textFieldOriginalWidth, height: textBoxHeight)
                    .background(Color.clear)
                    .font(Font.system(size: 90, design: .default))
                    .foregroundColor(Color.clear)
                    .onLongPressGesture(minimumDuration: 0.5) {
                        self.viewModel.showPasteButton = true
                    }
                
                // OTP TEXT
                HStack (spacing: spaceBetweenLines) {
                    ForEach(1 ... viewModel.numberOfFields, 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
                        )
                    }
                } //: HSTACK
                
                // TEXTFIELD FOR EDITING
                TextField("", text: $viewModel.otpField) { isEditing in
                    viewModel.isEditing = isEditing
                }
                .font(Font.system(size: 90, design: .default))
                .offset(x: 12, y: 10)
                .frame(width: viewModel.isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight)
                .textContentType(.oneTimeCode)
                .foregroundColor(.clear)
                .background(Color.clear)
                .keyboardType(.numberPad)
                .accentColor(.clear)
                
                // PASTE BUTTON
                Button(action: pasteText, label: {
                    Text("Paste")
                })
                .padding(.top, 9)
                .padding(.bottom, 9)
                .padding(.trailing, 16)
                .padding(.leading, 16)
                .font(Font.system(size: 14, design: .default))
                .accentColor(Color(.white))
                .background(Color(colorScheme == .light ? UIColor.black : UIColor.systemGray6))
                .cornerRadius(7.0)
                .overlay(
                    RoundedRectangle(cornerRadius: 7).stroke(Color(.black), lineWidth: 2)
                )
                .opacity(viewModel.showPasteButton ? 1 : 0)
                .offset(x: viewModel.numberOfFields == 6 ? -150 : -100, y: -40)
            } //: ZSTACK
        } //: VSTACK
    }
    
    func pasteText() {
        let pasteboard = UIPasteboard.general
        guard let pastedString = pasteboard.string else {
            return
        }
        let decimalInputOnly = pastedString
            .components(separatedBy:CharacterSet.decimalDigits.inverted)
            .joined()
        let otpField = decimalInputOnly.prefix(viewModel.numberOfFields)
        viewModel.otpField = String(otpField)
    }
    
    @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()
                    .frame(height: 65)
                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)
                } //: ZSTACK
                .clipped()
            })
            .padding(paddingOfBox)
    }
}

@available(iOS 13.0.0, *)
struct CXLRPOTPView_Previews: PreviewProvider {
    static var previews: some View {
        CXLRPOTPView(viewModel: OTPViewModel())
            .previewLayout(.sizeThatFits)
    }
}
Steve Vinoski
  • 19,847
  • 3
  • 31
  • 46
Dreiohc
  • 317
  • 2
  • 12

1 Answers1

1

seems like you've been having a tough time with the otp. :)

I have updated the OTPViewModel by adding two variables.

An OperationQueue with maxConcurrentOperationCount set as one to allow the digits to be added to the otpField one by one. (To fix the animation)

A string userPastedText to have didSet property observer where I add to the otpField one by one.

I also updated the paste func from the CXLRPOTPView to "paste" in the userPastedText. This is the code.

class OTPViewModel: ObservableObject {

    let operationQueue: OperationQueue = {
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1
        return operationQueue
    }()

.
.
.

    var userPastedText = "" {
        didSet {
            for char in userPastedText {
                operationQueue.addOperation {
                    Thread.sleep(forTimeInterval: 0.2)
                    DispatchQueue.main.async {
                        self.otpField += String(char)
                    }
                }
                
            }
        }
    }

.
.
.

}
struct CXLRPOTPView: View {
.
.
.
    func pasteText() {
        let pasteboard = UIPasteboard.general
        guard let pastedString = pasteboard.string else {
            return
        }
        let decimalInputOnly = pastedString
            .components(separatedBy:CharacterSet.decimalDigits.inverted)
            .joined()
        let otpField = decimalInputOnly.prefix(viewModel.numberOfFields)
        viewModel.userPastedText = String(otpField)
    }
.
.
.

}

Alhomaidhi
  • 517
  • 4
  • 12
  • hahahaha you are right and I should pay you by now. It's because we just moved to SwiftUI and I am much more familiar with UIKIt and I need to study more. My solution is like this but I haven't executed it properly I used only dispatch queue and iterated on pasted loop but yours is way smoother. Thanks @Alhomaidhi you saved my ass here too many times. – Dreiohc Sep 08 '21 at 09:07