Search code examples
iosswiftuitextfieldhighlightone-time-password

How to disable highlighted text in TextField in Swift UI


I created an OTP field using textfield but I want to disable highlighting the text when you double tap or longpress on a TextField.

I tried adjusting the font size to 0. This seems to shrink the grey highlight but it did not hide it totally.

I can't use textSelection(.disabled) because I can only use Xcode 12.3 and this API seems to be available in higher Xcode versions.

I also cannot adjust the frame width of textField to 0 when editing is true because the paste functionality would be disable. Paste to text is a needed requirement.

Here is my code:

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


Solution

  • In the frame of the TextField.

    .frame(width: textFieldOriginalWidth, height: textBoxHeight)
    

    Change the width to be reactive to the isEditing state. If true make the width 0 otherwise make it textFieldOriginalWidth

    like this:

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

    Of course this doesn't disable it. But it will not highlight and allow the user to past, copy, etc...

    It will have the desired outcome.

    Update

    To get the OTP to "Autofill" or show up in the keyboard.

    Set the Textfield's .textContentType as .oneTimeCode. The OS should handle the rest read the Apple docs.

    Which you have done for textfield.

    This action should paste your copied text:

    Button(action: paste, label: {
        Text("Paste")
    })
    
    .
    .
    .
    func paste() {
        let pasteboard = UIPasteboard.general
        guard let pastedString = pasteboard.string else {
            return
        }
        viewModel.otpField = pastedString
    }