经过大量的试验和错误,我最终实现了以下实现,以对文本字段中的数字输入进行实时格式化。使用 SwiftUI TextField() 的各种尝试导致了许多异常。下面的方法似乎很可靠,但即使在这里我也很难找到对 NSTextField 进行子类化的正确方法,因为我找不到任何关于如何处理指定初始化程序以使其与 SwiftUI 的帧修饰符兼容的文档。
剩下的一个小异常是,当将光标放在输入的数字中间然后键入非数字字符时,即使文本中没有发生任何变化,光标也会前进。这是宜居的,但我宁愿避免这种情况发生。
有没有更好、更“合适”的方法来实现这一点?
import Foundation
import SwiftUI
struct NumberField : NSViewRepresentable {
typealias NSViewType = NumberText
var defaultText : String
var maxDigits : Int
var numberValue : Binding<Int>
func makeNSView(context: Context) -> NSViewType {
// Create text field
let numberTextField = NumberText()
numberTextField.isEditable = true
// numberTextField.numberBinding = numberValue
numberTextField.configure(text: defaultText, digits: maxDigits, intBinding: numberValue)
return numberTextField
}
func updateNSView(_ nsView: NSViewType, context: Context) {
// nsView.stringValue = "This is my string"
}
}
/// NumberText draws an NSTextField that will accept only digits up to a maximum number specified when calling Configure. Apple implements some nice integration between SwiftUI's frame and padding modifiers and the NSTextField's designated initializer. Rather than having to figure out how to fix/preserve this integration, this class provides a "configure()" function that is effectively it's initializer. As a result, it is MANDATORY that this class's configure() function be called immediately after initializing the class.
class NumberText : NSTextField {
// Code below jumps through a couple of hoops to avoid having to write a custom initializer since that gets in the middle of Apple's configuration of the text field using standard SwiftUI modifiers.
// NOTE THAT A USER OF NumberText MUST CALL CONFIGURE() IMMEDIATELY AFTER CREATING IT
var numberBinding : Binding<Int> = Binding( // This is initialized with a garbage Binding just to avoid having to write an initializer
get: {return -1},
set: {newValue in return}
)
var defaultText = "Default String"
var maxDigits = 9
private var decimalFormatter = NumberFormatter()
func configure(text: String, digits: Int, intBinding: Binding<Int>) { // Configure is used here instead of attempting to override init()
// Configure values
decimalFormatter.numberStyle = .decimal
defaultText = text
self.placeholderString = defaultText
maxDigits = digits
numberBinding = intBinding
// Set up TextField values
self.integerValue = numberBinding.wrappedValue
if self.integerValue == 0 {self.stringValue = ""}
}
override func textDidChange(_ notification: Notification) {
self.stringValue = numberTextFromString(self.stringValue)
if self.stringValue == "0" {self.stringValue = ""}
}
func numberTextFromString(_ inputText: String, maxLength: Int = 9) -> String {
// Create a filtered and trucated version of inputText
let filteredText = inputText.filter { character in
character.isNumber
}
let truncatedText = String(filteredText.suffix(maxLength))
// Make a number from truncated text
let myNumber = Int(truncating: decimalFormatter.number(from: truncatedText) ?? 0 )
// Set binding value
numberBinding.wrappedValue = myNumber
// Create formatted string for return
let returnValue = decimalFormatter.string(from: myNumber as NSNumber) ?? "?"
return returnValue
}