Как сделать анимацию строк в OTP перемещаемой по одной за раз при вставке текста в SwiftUI

#ios #animation #swiftui #textfield #one-time-password

Вопрос:

Я создал поле OTP, которое будет анимировать строки каждый раз, когда вы вводите цифру. Это хорошо работает. Тем не менее, я хочу, чтобы анимация зеленых линий выполнялась по одной(слева направо) при вставке, как при вводе числа.

В настоящее время, когда вы вставляете текст, все строки анимируются одновременно, как показано на видео.

Текущее Поведение

Ожидается, что при вставке строка должна становиться зеленой по одной слева направо.

Пожалуйста, обратите внимание, что моя кнопка «Вставить» — это пользовательская кнопка, которую я создал в качестве обходного пути для какой-либо другой функции.

Я также могу использовать только Xcode 12.3.

Вот мой код:

Некоторые шестнадцатеричные цвета и шрифт являются пользовательскими. Вы можете заменить его при воспроизведении.

 
@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)
    }
}
 

Ответ №1:

похоже, у вас были трудные времена с otp. 🙂

Я обновил OTPViewModel его, добавив две переменные.

OperationQueue С maxConcurrentOperationCount набором как один, чтобы цифры можно было добавлять по otpField одной. (Чтобы исправить анимацию)

Строка userPastedText для didSet наблюдателя свойств, в которой я добавляю по otpField одному.

Я также обновил paste функцию с CXLRPOTPView «вставить» в userPastedText . Это и есть код.

 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)
    }
.
.
.

}

 

Комментарии:

1. хахахаха, ты прав, и я уже должен тебе заплатить. Это потому, что мы только что переехали в SwiftUI, и я гораздо лучше знаком с UIKit, и мне нужно больше учиться. Мое решение похоже на это, но я не выполнил его должным образом, я использовал только очередь отправки и повторил вставленный цикл, но ваш способ более плавный. Спасибо @Alhomaidhi, ты слишком много раз спасал мою задницу здесь.