import Cocoa import SwiftUI struct PinCodeTextField: NSViewRepresentable { typealias NSViewType = PinCodeTextView @Binding var code: String let numberOfDigits: Int func makeNSView(context: Context) -> NSViewType { let view = PinCodeTextView(numberOfDigits: numberOfDigits, itemSpacing: 10) view.codeDidChange = { c in code = c } return view } func updateNSView(_ nsView: NSViewType, context: Context) { nsView.code = (0.. Void)? = nil private let numberOfDigits: Int private let stackView: NSStackView = .init(frame: .zero) private var characterViews: [PinCodeCharacterTextField] = [] // MARK: - Initializers init( numberOfDigits: Int, itemSpacing: CGFloat ) { self.numberOfDigits = numberOfDigits super.init(frame: .zero) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.spacing = itemSpacing stackView.orientation = .horizontal stackView.distribution = .fillEqually stackView.alignment = .centerY addSubview(stackView) NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: self.topAnchor), stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor), stackView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor), stackView.trailingAnchor.constraint(greaterThanOrEqualTo: self.trailingAnchor), stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), ]) self.code = (0.. Bool { if commandSelector == #selector(deleteBackward(_:)) { // If empty, move to previous or first character view if textView.string.isEmpty { if let lastFieldIndexWithCharacter = code.lastIndex(where: { $0 != nil }) { window?.makeFirstResponder(characterViews[lastFieldIndexWithCharacter]) } else { window?.makeFirstResponder(characterViews[0]) } return true } } // Perform default behaviour return false } func controlTextDidChange(_ obj: Notification) { guard let field = obj.object as? NSTextField, isEnabled, let fieldIndex = characterViews.firstIndex(where: { $0 === field }) else { return } let newFieldText = field.stringValue let lastCharacter: Character? if newFieldText.isEmpty { lastCharacter = nil } else { lastCharacter = newFieldText[newFieldText.index(before: newFieldText.endIndex)] } code[fieldIndex] = lastCharacter if lastCharacter != nil { if fieldIndex >= characterViews.count - 1 { resignFirstResponder() } else { window?.makeFirstResponder(characterViews[fieldIndex + 1]) } } else { if let lastFieldIndexWithCharacter = code.lastIndex(where: { $0 != nil }) { window?.makeFirstResponder(characterViews[lastFieldIndexWithCharacter]) } else { window?.makeFirstResponder(characterViews[0]) } } } // MARK: NSResponder override var acceptsFirstResponder: Bool { true } override func becomeFirstResponder() -> Bool { characterViews.first?.becomeFirstResponder() ?? false } } // MARK: - PinCodeCharacterTextField class PinCodeCharacterTextField: NSTextField { var character: Character? = nil { didSet { stringValue = character.map(String.init) ?? "" } } private var lastSize: NSSize? init() { super.init(frame: .zero) wantsLayer = true alignment = .center maximumNumberOfLines = 1 font = .boldSystemFont(ofSize: 48) setContentHuggingPriority(.required, for: .vertical) setContentHuggingPriority(.required, for: .horizontal) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func textDidChange(_ notification: Notification) { super.textDidChange(notification) self.invalidateIntrinsicContentSize() } // This is kinda cheating // Assuming that 0 is the widest and tallest character in 0-9 override var intrinsicContentSize: NSSize { var size = NSAttributedString( string: "0", attributes: [ .font : self.font! ] ) .size() // I guess the cell should probably be doing this sizing in order to take into account everything outside of simply the text's frame, but for some reason I can't find a way to do that which works... size.width += 16 size.height += 8 return size } }