mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
222 lines
6.9 KiB
Swift
222 lines
6.9 KiB
Swift
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..<numberOfDigits).map { index in
|
|
if index < code.count {
|
|
let codeIndex = code.index(code.startIndex, offsetBy: index)
|
|
return code[codeIndex]
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PinCodeTextField_Previews: PreviewProvider {
|
|
struct PreviewContainer: View {
|
|
@State private var code = "1234567890"
|
|
var body: some View {
|
|
PinCodeTextField(code: $code, numberOfDigits: 11)
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
static var previews: some View {
|
|
Group {
|
|
PreviewContainer()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - PinCodeTextView
|
|
|
|
class PinCodeTextView: NSControl, NSTextFieldDelegate {
|
|
var code: [Character?] = [] {
|
|
didSet {
|
|
guard code != oldValue else { return }
|
|
|
|
if let handler = codeDidChange {
|
|
handler(String(code.compactMap { $0 }))
|
|
}
|
|
updateText()
|
|
}
|
|
}
|
|
var codeDidChange: ((String) -> 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..<numberOfDigits).map { _ in nil }
|
|
self.characterViews = (0..<numberOfDigits).map { _ in
|
|
let view = PinCodeCharacterTextField()
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.delegate = self
|
|
return view
|
|
}
|
|
characterViews.forEach {
|
|
stackView.addArrangedSubview($0)
|
|
stackView.heightAnchor.constraint(equalTo: $0.heightAnchor).isActive = true
|
|
}
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func updateText() {
|
|
characterViews.enumerated().forEach { (index, item) in
|
|
if (0..<code.count).contains(index) {
|
|
let _index = code.index(code.startIndex, offsetBy: index)
|
|
item.character = code[_index]
|
|
} else {
|
|
item.character = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: NSTextFieldDelegate
|
|
|
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> 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
|
|
}
|
|
}
|