mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
The previous implementation used 6 separate text fields for the SMS verification code, which broke macOS's built-in SMS autofill functionality. When the system detects an SMS code and offers to autofill it, it expects a single text field to receive the entire code. Changes: - Replaced 6 individual PinCodeCharacterTextField instances with a single NSTextField - Added .oneTimeCode content type to enable SMS autofill on macOS 11+ - Simplified the code structure while maintaining the same functionality - Added input validation to ensure only digits are accepted Now when users receive an SMS verification code, macOS can properly autofill the entire code with a single click, rather than filling only the first digit and deleting the message. Closes #788
156 lines
4.6 KiB
Swift
156 lines
4.6 KiB
Swift
import Cocoa
|
|
import SwiftUI
|
|
|
|
struct PinCodeTextField: NSViewRepresentable {
|
|
typealias NSViewType = PinCodeTextView
|
|
|
|
@Binding var code: String
|
|
let numberOfDigits: Int
|
|
let complete: (String) -> Void
|
|
|
|
func makeNSView(context: Context) -> NSViewType {
|
|
let view = PinCodeTextView(numberOfDigits: numberOfDigits)
|
|
view.codeDidChange = { c in code = c }
|
|
view.codeDidComplete = { complete($0) }
|
|
return view
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSViewType, context: Context) {
|
|
if nsView.code != code {
|
|
nsView.code = code
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PinCodeTextField_Previews: PreviewProvider {
|
|
struct PreviewContainer: View {
|
|
@State private var code = ""
|
|
var body: some View {
|
|
PinCodeTextField(code: $code, numberOfDigits: 6) {
|
|
print("Input is complete \($0)")
|
|
}.padding()
|
|
}
|
|
}
|
|
|
|
static var previews: some View {
|
|
Group {
|
|
PreviewContainer()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - PinCodeTextView
|
|
|
|
/// A single text field that accepts a verification code.
|
|
/// Uses a single NSTextField to enable macOS SMS autofill functionality.
|
|
class PinCodeTextView: NSControl, NSTextFieldDelegate {
|
|
var code: String = "" {
|
|
didSet {
|
|
guard code != oldValue else { return }
|
|
|
|
// Ensure code only contains digits and is within length limit
|
|
let filteredCode = String(code.filter { $0.isNumber }.prefix(numberOfDigits))
|
|
if filteredCode != code {
|
|
code = filteredCode
|
|
return
|
|
}
|
|
|
|
if textField.stringValue != code {
|
|
textField.stringValue = code
|
|
}
|
|
|
|
codeDidChange?(code)
|
|
|
|
if code.count == numberOfDigits {
|
|
codeDidComplete?(code)
|
|
}
|
|
}
|
|
}
|
|
var codeDidChange: ((String) -> Void)? = nil
|
|
var codeDidComplete: ((String) -> Void)? = nil
|
|
|
|
private let numberOfDigits: Int
|
|
private let textField: NSTextField
|
|
|
|
// MARK: - Initializers
|
|
|
|
init(numberOfDigits: Int) {
|
|
self.numberOfDigits = numberOfDigits
|
|
self.textField = NSTextField(frame: .zero)
|
|
|
|
super.init(frame: .zero)
|
|
|
|
setupTextField()
|
|
setupLayout()
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func setupTextField() {
|
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
|
textField.delegate = self
|
|
textField.alignment = .center
|
|
textField.font = .monospacedDigitSystemFont(ofSize: 32, weight: .medium)
|
|
textField.placeholderString = String(repeating: "•", count: numberOfDigits)
|
|
|
|
// Enable one-time code autofill
|
|
if #available(macOS 11.0, *) {
|
|
textField.contentType = .oneTimeCode
|
|
}
|
|
|
|
// Configure for numeric input
|
|
textField.allowsEditingTextAttributes = false
|
|
|
|
// Add letter spacing for better readability
|
|
let formatter = NumberFormatter()
|
|
formatter.allowsFloats = false
|
|
formatter.minimum = 0
|
|
formatter.maximum = NSNumber(value: Int(String(repeating: "9", count: numberOfDigits))!)
|
|
|
|
addSubview(textField)
|
|
}
|
|
|
|
private func setupLayout() {
|
|
NSLayoutConstraint.activate([
|
|
textField.topAnchor.constraint(equalTo: topAnchor),
|
|
textField.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
textField.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
textField.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
textField.widthAnchor.constraint(greaterThanOrEqualToConstant: CGFloat(numberOfDigits * 30 + 40)),
|
|
textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 50)
|
|
])
|
|
}
|
|
|
|
// MARK: NSTextFieldDelegate
|
|
|
|
func controlTextDidChange(_ obj: Notification) {
|
|
guard
|
|
let field = obj.object as? NSTextField,
|
|
field === textField,
|
|
isEnabled
|
|
else { return }
|
|
|
|
let newText = field.stringValue
|
|
|
|
// Filter to only digits
|
|
let filteredText = String(newText.filter { $0.isNumber }.prefix(numberOfDigits))
|
|
|
|
if filteredText != newText {
|
|
field.stringValue = filteredText
|
|
}
|
|
|
|
code = filteredText
|
|
}
|
|
|
|
// MARK: NSResponder
|
|
|
|
override var acceptsFirstResponder: Bool {
|
|
true
|
|
}
|
|
|
|
override func becomeFirstResponder() -> Bool {
|
|
textField.becomeFirstResponder()
|
|
}
|
|
}
|