在 SwiftUI 中粘贴文本时,如何使 OTP 中的线条动画一次移动一个
How to make animation of lines in OTP move one at a time when pasting a text in SwiftUI
我创建了一个 OTP 字段,它会在您每次键入数字时为线条设置动画。这很好用。但是,我希望在粘贴时将绿线的动画一次 运行 一条(从左到右),就像键入数字时一样。
目前,当您粘贴文本时,所有行都会同时动画,如视频中所示。
预计应该是粘贴时从左到右一条线变绿
请注意,我的粘贴按钮是我创建的自定义按钮,作为其他一些功能的解决方法。
我也只能用Xcode12.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)
}
}
看来你在 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)
}
.
.
.
}
我创建了一个 OTP 字段,它会在您每次键入数字时为线条设置动画。这很好用。但是,我希望在粘贴时将绿线的动画一次 运行 一条(从左到右),就像键入数字时一样。
目前,当您粘贴文本时,所有行都会同时动画,如视频中所示。
预计应该是粘贴时从左到右一条线变绿
请注意,我的粘贴按钮是我创建的自定义按钮,作为其他一些功能的解决方法。
我也只能用Xcode12.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)
}
}
看来你在 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)
}
.
.
.
}