mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Add PinCodeTextField
This commit is contained in:
parent
ad267e2b56
commit
fb6ed58b3e
3 changed files with 235 additions and 1 deletions
|
|
@ -10,6 +10,7 @@
|
|||
CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; };
|
||||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
|
||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */; };
|
||||
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2C255A5262003FD669 /* AppleAPI */; };
|
||||
CAA1CB2F255A5262003FD669 /* XcodesKit in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2E255A5262003FD669 /* XcodesKit */; };
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */; };
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = "<group>"; };
|
||||
CA538A0F255A4F3300E64DD7 /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = "<group>"; };
|
||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = "<group>"; };
|
||||
CA8FB5F8256E0F9400469DA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
CA8FB61C256E115700469DA5 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; };
|
||||
CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = XcodesTest.entitlements; sourceTree = "<group>"; };
|
||||
|
|
@ -92,6 +94,7 @@
|
|||
children = (
|
||||
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
|
||||
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
|
||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
|
||||
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
|
||||
CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */,
|
||||
);
|
||||
|
|
@ -272,6 +275,7 @@
|
|||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
|
||||
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
|
||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
|
||||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
|||
222
Xcodes/SignIn/PinCodeTextView.swift
Normal file
222
Xcodes/SignIn/PinCodeTextView.swift
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
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 = "123"
|
||||
var body: some View {
|
||||
PinCodeTextField(code: $code, numberOfDigits: 6)
|
||||
.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 += 8
|
||||
size.height += 8
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
|
@ -6,20 +6,28 @@ struct SignIn2FAView: View {
|
|||
@Binding var isPresented: Bool
|
||||
@State private var code: String = ""
|
||||
let sessionData: AppleSessionData
|
||||
// TODO: dynamic number of digits
|
||||
let numberOfDigits = 6
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Enter the \(6) digit code from one of your trusted devices:")
|
||||
|
||||
HStack {
|
||||
TextField("\(6) digit code", text: $code)
|
||||
Spacer()
|
||||
PinCodeTextField(code: $code, numberOfDigits: numberOfDigits)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
|
||||
HStack {
|
||||
Button("Cancel", action: { isPresented = false })
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Button("Send SMS", action: {})
|
||||
Spacer()
|
||||
Button("Continue", action: { appState.submit2FACode(code, sessionData: sessionData) })
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(code.count != numberOfDigits)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
|
|
|||
Loading…
Reference in a new issue