Search code examples
iosswiftuitextfieldcopy-pasteone-time-password

How to paste text on textfield from different app in Swift UI


I created an OTP field for 6 digits and I'd like to enable paste function, example I want to copy a text from imessage to my app's textfield. I thought this was automatic in textfield but when I double tap in the textfield the dialog asking paste a text does not appear. My clipboard was not empty as I tried it in other text field that is not SwiftUI and it works.

My textfield was created to a lot of workaround views due to the limitation of the Xcode that I am using. I can only use Xcode 12.3.

PS

If someone can also help me why the Done button is missing from my decimal pad keyboard.

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.count <= numberOfFields,
                  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])
    }
    
    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 {
        Background {
            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: 50, design: .default))
                    .offset(y: 10)
                    .frame(width: viewModel.isEditing ? 0 : textFieldOriginalWidth, height: textBoxHeight)
                    .textContentType(.oneTimeCode)
                    .foregroundColor(.clear)
                    .background(Color.clear)
                    .keyboardType(.decimalPad)
                } //: ZSTACK
            } //: VSTACK
        } //: Background
        .onTapGesture {
            viewModel.hideKeyboard()
        }

    }
    
    struct Background<Content: View>: View {
        private var content: Content

        init(@ViewBuilder content: @escaping () -> Content) {
            self.content = content()
        }

        var body: some View {
            Color.clear
            .contentShape(Rectangle())
            .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
            .overlay(content)
        }
    }
    
    @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)
    }
}


Solution

  • The paste functionality is there but due to my textfield's frame. it was way out of bounds and cannot be seen in screen. I adjusted the frame and the paste now appears. I also modified the accent color to hide the cursor.

      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: 50, design: .default))
                    .offset(y: 10)
                    .frame(width: textFieldOriginalWidth, height: textBoxHeight) // What I changed
                    .textContentType(.oneTimeCode)
                    .foregroundColor(.clear)
                    .background(Color.clear)
                    .keyboardType(.decimalPad)
                    .accentColor(.clear)
                } //: ZSTACK
            } //: VSTACK