wristbands
wristbands

Reputation: 1389

TextField sometimes fails to paste OTP code from keyboard suggestion

I've noticed that the one-time-password from keyboard suggestions sometimes fails to paste into my text field in my SwiftUI iOS app. Most of the time it works, but sometimes the first time I click the suggestion it doesn't paste. Then I try to click the suggestion a second time, and luckily that second time always seems to work.

I think it might be an issue with iOS 17, since I've only experienced the issue on devices running versions of iOS 17. I have never experienced it on iOS 15 or iOS 16.

Has anyone else experienced this issue? Anyone have any solutions?

Here's the code for my verification code field, if it's of use:

struct VerificationCodeField: View {
// Adapted from: https://medium.com/flawless-app-stories/swiftui-passcode-field-for-otp-and-pin-entry-b61ba663dc31

  let maxDigits = 6
  let showCodeShowingToggle = false

  @Binding var code: String
  @State var showCode = true
  @State var isDisabled = false

  var onFinishedEntry:
    (_ code: String, _ onVerifiedCode: @escaping (_ didSucceed: Bool) -> Void) -> Void

  var body: some View {
    VStack(spacing: 10) {
      ZStack {
        codeBoxes
        codeBackgroundField
      }

      if showCodeShowingToggle {
        codeShowingToggle
      }
    }
  }

  private var codeBoxes: some View {
    HStack {
      Spacer()
      ForEach(0..<maxDigits, id: \.self) { index in
        Image(systemName: getImageName(at: index))
          .resizable()
          .font(.body.weight(.thin))
          .aspectRatio(1, contentMode: .fit)
          .foregroundColor(.reelspaceLightGray)
        Spacer()
      }
    }
  }

  private var codeBackgroundField: some View {
    TextField("", text: $code, onCommit: submitCode)
      .font(.system(size: 35))
      .accentColor(.clear) // Prevent cursor from appearing in iOS 15
      .tint(.clear) // Prevent cursor from appearing in iOS 16+
      .foregroundColor(.clear)
      .textContentType(.oneTimeCode)
      .keyboardType(.numberPad)
      .disabled(isDisabled)
      .onChange(of: code) { _ in submitCode() }
  }

  private var codeShowingToggle: some View {
    HStack {
      Spacer()
      if !code.isEmpty {
        codeShowingButton
      }
    }
    .frame(height: 20)
    .padding([.trailing])
  }

  private var codeShowingButton: some View {
    Button(action: {
      showCode.toggle()
    }, label: {
      showCode ?
        Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
        Image(systemName: "eye.fill").foregroundColor(.primary)
    })
  }

  private func submitCode() {
    if code.count == maxDigits {
      UIApplication.shared.closeKeyboard()
      isDisabled = true

      onFinishedEntry(code) { verifiedCode in
        if !verifiedCode {
          code = ""
        }
        isDisabled = false
      }
    }

    // This code is never reached under  normal circumstances. If the user pastes a text with count
    // higher than the max digits, we remove the extra leading characters and make a recursive call.
    if code.count > maxDigits {
      code = String(code.suffix(maxDigits))
      submitCode()
    }
  }

  private func getImageName(at index: Int) -> String {
    if index >= code.count {
      return "square"
    }

    if showCode {
      return code.digits[index].numberString + ".square"
    }

    return "square.fill"
  }
}

private extension String {
  var digits: [Int] {
    // Map characters to digits, defaulting to 0 if character is not a digit.
    map { Int(String($0)) ?? 0 }
  }
}

private extension Int {
  var numberString: String {
    guard self < 10 else { return "0" }

    return String(self)
  }
}

Upvotes: 1

Views: 298

Answers (0)

Related Questions