如何在 Swift UI 的 TextField 中禁用突出显示的文本

How to disable highlighted text in TextField in Swift UI

我使用文本字段创建了一个 OTP 字段,但我想在双击或长按文本字段时禁用突出显示文本。

我尝试将字体大小调整为 0。这似乎缩小了灰色高光,但并没有完全隐藏它。

我不能使用 textSelection(.disabled) 因为我只能使用 Xcode 12.3 而这个 API 似乎在更高 Xcode 个版本。

当编辑为真时,我也无法将 textField 的框架宽度调整为 0,因为粘贴功能将被禁用。粘贴到文本是必需的要求。

这是我的代码:

import SwiftUI

@available(iOS 13.0, *)
class OTPViewModel: ObservableObject {
    
    var numberOfFields: Int
    
    init(numberOfFields: Int = 6) {
        self.numberOfFields = numberOfFields
    }
    
    @Published var otpField = "" {
        didSet {
            guard otpField.last?.isNumber ?? true else {
                otpField = oldValue
                return
            }
            if otpField.count == numberOfFields {
                hideKeyboard()
            }
        }
    }
    
    @Published var isEditing = 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 OTPView: View {
    @ObservedObject var viewModel = OTPViewModel()
    
    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 {
                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("", text: $viewModel.otpField) { isEditing in
                    viewModel.isEditing = isEditing
                }
                .font(Font.system(size: 90, design: .default))
                .offset(x: 12, y: 10)
                .frame(width: textFieldOriginalWidth, height: textBoxHeight)
                .textContentType(.oneTimeCode)
                .foregroundColor(.clear)
                .background(Color.clear)
                .keyboardType(.decimalPad)
                .accentColor(.clear)
                
            } //: ZSTACK
        } //: VSTACK
    }
    
    @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)
            .accentColor(.clear)
    }
}

在 TextField 的框架中。

.frame(width: textFieldOriginalWidth, height: textBoxHeight)

更改宽度以响应 isEditing 状态。如果 true 使宽度 0 否则使它 textFieldOriginalWidth

像这样:

.frame(width: isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight)

当然这不会禁用它。但它不会突出显示并允许用户过去、复制等...

会得到想要的结果。

更新

将 OTP 设置为“自动填充”或显示在键盘中。

Textfield.textContentType 设置为 .oneTimeCode。 OS 应该处理其余的阅读 Apple docs

您为文本字段所做的。

此操作应粘贴您复制的文本:

Button(action: paste, label: {
    Text("Paste")
})

.
.
.
func paste() {
    let pasteboard = UIPasteboard.general
    guard let pastedString = pasteboard.string else {
        return
    }
    viewModel.otpField = pastedString
}

解决方案是在编辑时将 TextField 的宽度框架设为 0,但这样做会禁用粘贴功能,因此@Alhomaidhi 的解决方案是添加一个粘贴按钮。

我这样做了,当 TextField 双击或长按以模仿 iOS 剪贴板时,它就会出现。

完整代码如下:

import SwiftUI

@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 {
                hideKeyboard()
                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) // SOLUTION THAT PREVENTED TEXT HIGHLIGHT
                .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 otpField = pastedString.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)
            .foregroundColor(Color.black)
    }
}

@available(iOS 13.0.0, *)
struct CXLRPOTPView_Previews: PreviewProvider {
    static var previews: some View {
        CXLRPOTPView(viewModel: OTPViewModel())
            .previewLayout(.sizeThatFits)
    }
}