带格式化程序的 SwiftUI TextField 不工作?
SwiftUI TextField with formatter not working?
我正在尝试更新数字字段,因此我使用了带有格式化程序:参数集的 TextField。它将数字格式化为输入字段就好了,但在编辑时不会更新绑定值。 TextField 在没有指定格式化程序的情况下工作正常(在字符串上)。这是一个错误还是我遗漏了什么?
更新:从 Xcode 11 beta 3 开始,它可以正常工作。现在,如果您编辑数字 TextField,绑定值会在您点击 return 后更新。每次按键后,String TextField 仍会更新。我猜他们不想在每次按键时将要格式化的值发送给格式化程序,或者可能 is/will 是 TextField 的修饰符来告诉它这样做。
请注意 API 略有变化;旧的 TextField init() 已弃用,新的 titleKey String 字段已添加为第一个参数,该参数在该字段中显示为占位符文本。
struct TestView : View {
@State var someText = "Change me!"
@State var someNumber = 123.0
var body: some View {
Form {
// Xcode 11 beta 2
// TextField($someText)
// TextField($someNumber, formatter: NumberFormatter())
// Xcode 11 beta 3
TextField("Text", text: $someText)
TextField("Number", value: $someNumber, formatter: NumberFormatter())
Spacer()
// if you change the first TextField value, the change shows up here
// if you change the second (the number),
// it does not *until you hit return*
Text("text: \(self.someText), number: \(self.someNumber)")
// the button does the same, but logs to the console
Button(action: { print("text: \(self.someText), number: \(self.someNumber)")}) {
Text("Log Values")
}
}
}
}
如果您键入第一个(字符串)TextField,文本视图中的值会立即更新。如果您编辑第二个(数字),什么也不会发生。
同样,点击 Button 会显示 String 的更新值,但不会显示数字。我只在模拟器中试过这个。
似乎在使用 value:
作为输入时,SwiftUI 不会为用户点击的任何键重新加载视图。而且,正如您提到的,它会在用户退出或提交该字段时重新加载视图。
另一方面,每当按下一个键时,SwiftUI 使用 text:
作为输入(立即)重新加载视图。我没有别的想法。
就我而言,我为 someNumber2
做了如下操作:
struct ContentView: View {
@State var someNumber = 123.0
@State var someNumber2 = "123"
var formattedNumber : NSNumber {
let formatter = NumberFormatter()
guard let number = formatter.number(from: someNumber2) else {
print("not valid to be converted")
return 0
}
return number
}
var body: some View {
VStack {
TextField("Number", value: $someNumber, formatter: NumberFormatter())
TextField("Number2", text: $someNumber2)
Text("number: \(self.someNumber)")
Text("number: \(self.formattedNumber)")
}
}
}
您可以使用 Binding 将 Double<-->String 转换为 TextField
struct TestView: View {
@State var someNumber = 123.0
var body: some View {
let someNumberProxy = Binding<String>(
get: { String(format: "%.02f", Double(self.someNumber)) },
set: {
if let value = NumberFormatter().number(from: [=10=]) {
self.someNumber = value.doubleValue
}
}
)
return VStack {
TextField("Number", text: someNumberProxy)
Text("number: \(someNumber)")
}
}
}
您可以使用计算属性 的方式来解决这个问题。 (感谢@iComputerfreak)
struct TestView: View {
@State var someNumber = 123.0
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%.02f", Double(self.someNumber)) },
set: {
if let value = NumberFormatter().number(from: [=11=]) {
self.someNumber = value.doubleValue
}
}
)
}
var body: some View {
VStack {
TextField("Number", text: someNumberProxy)
Text("number: \(someNumber)")
}
}
}
我知道这有一些可接受的答案,但上面的答案在输入值时似乎有错误的用户体验结果(至少对于双精度值)。所以我决定编写自己的解决方案。它很大程度上受到这里答案的启发,所以我会先尝试这里的其他例子,然后再尝试这个例子,因为它有更多的代码。
WARNING Although I have been an iOS developer for a long time, I'm fairly new to SwiftUI. So this is far from expert advice. I would love
feedback on my approach but be nice. So far this has been working out
well on my new project. However, I doubt this is as efficient as Apple's formatters.
protocol NewFormatter {
associatedtype Value: Equatable
/// The logic that converts your value to a string presented by the `TextField`. You should omit any values
/// - Parameter object: The value you are converting to a string.
func toString(object: Value) -> String
/// Once the change is allowed and the input is final, this will convert
/// - Parameter string: The full text currently on the TextField.
func toObject(string: String) -> Value
/// Specify if the value contains a final result. If it does not, nothing will be changed yet.
/// - Parameter string: The full text currently on the TextField.
func isFinal(string: String) -> Bool
/// Specify **all** allowed inputs, **including** intermediate text that cannot be converted to your object **yet** but are necessary in the input process for a final result. It will allow this input without changing your value until a final correct value can be determined.
/// For example, `1.` is not a valid `Double`, but it does lead to `1.5`, which is. Therefore the `DoubleFormatter` would return true on `1.`.
/// Returning false will reset the input to the previous allowed value.
/// - Parameter string: The full text currently on the TextField.
func allowChange(to string: String) -> Bool
}
struct NewTextField<T: NewFormatter>: View {
let title: String
@Binding var value: T.Value
let formatter: T
@State private var previous: T.Value
@State private var previousGoodString: String? = nil
init(_ title: String, value: Binding<T.Value>, formatter: T) {
self.title = title
self._value = value
self._previous = State(initialValue: value.wrappedValue)
self.formatter = formatter
}
var body: some View {
let changedValue = Binding<String>(
get: {
if let previousGoodString = self.previousGoodString {
let previousValue = self.formatter.toObject(string: previousGoodString)
if previousValue == self.value {
return previousGoodString
}
}
let string = self.formatter.toString(object: self.value)
return string
},
set: { newString in
if self.formatter.isFinal(string: newString) {
let newValue = self.formatter.toObject(string: newString)
self.previousGoodString = newString
self.previous = newValue
self.value = newValue
} else if !self.formatter.allowChange(to: newString) {
self.value = self.previous
}
}
)
return TextField(title, text: changedValue)
}
}
然后您可以为 Double
创建自定义格式化程序,如下所示:
/// An object that converts a double to a valid TextField value.
struct DoubleFormatter: NewFormatter {
let numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.allowsFloats = true
numberFormatter.numberStyle = .decimal
numberFormatter.maximumFractionDigits = 15
return numberFormatter
}()
/// The logic that converts your value to a string used by the TextField.
func toString(object: Double) -> String {
return numberFormatter.string(from: NSNumber(value: object)) ?? ""
}
/// The logic that converts the string to your value.
func toObject(string: String) -> Double {
return numberFormatter.number(from: string)?.doubleValue ?? 0
}
/// Specify if the value contains a final result. If it does not, nothing will be changed yet.
func isFinal(string: String) -> Bool {
return numberFormatter.number(from: string) != nil
}
/// Specify **all** allowed values, **including** intermediate text that cannot be converted to your object **yet** but are necessary in the input process for a final result.
/// For example, `1.` is not a valid `Double`, but it does lead to `1.5`, which is. It will allow this input without changing your value until a final correct value can be determined.
/// Returning false will reset the input the the previous allowed value. For example, when using the `DoubleFormatter` the input `0.1j` would result in false which would reset the value back to `0.1`.
func allowChange(to string: String) -> Bool {
let components = string.components(separatedBy: ".")
if components.count <= 2 {
// We allow an Integer or an empty value.
return components.allSatisfy({ [=11=] == "" || Int([=11=]) != nil })
} else {
// If the count is > 2, we have more than one decimal
return false
}
}
}
你可以像这样使用这个新组件:
NewTextField(
"Value",
value: $bodyData.doubleData.value,
formatter: DoubleFormatter()
)
以下是我能想到的其他一些用法:
/// Just a simple passthrough formatter to use on a NewTextField
struct PassthroughFormatter: NewFormatter {
func toString(object: String) -> String {
return object
}
func toObject(string: String) -> String {
return string
}
func isFinal(string: String) -> Bool {
return true
}
func allowChange(to string: String) -> Bool {
return true
}
}
/// A formatter that converts empty strings to nil values
struct EmptyStringFormatter: NewFormatter {
func toString(object: String?) -> String {
return object ?? ""
}
func toObject(string: String) -> String? {
if !string.isEmpty {
return string
} else {
return nil
}
}
func isFinal(string: String) -> Bool {
return true
}
func allowChange(to string: String) -> Bool {
return true
}
}
方案B。由于使用value:
和NumberFormatter
不起作用,我们可以使用自定义的TextField
。我已将 TextField
包裹在 struct
中,以便您可以尽可能透明地使用它。
我对Swift和SwiftUI都是非常新手,所以毫无疑问是一个更优雅的解决方案。
struct IntField: View {
@Binding var int: Int
@State private var intString: String = ""
var body: some View {
return TextField("", text: $intString)
.onReceive(Just(intString)) { value in
if let i = Int(value) { int = i }
else { intString = "\(int)" }
}
.onAppear(perform: {
intString = "\(int)"
})
}
}
并且在 ContentView 中:
struct ContentView: View {
@State var testInt: Int = 0
var body: some View {
return HStack {
Text("Number:")
IntField(int: $testInt);
Text("Value: \(testInt)")
}
}
}
基本上,我们使用 TextField("…", text: …)
,它的行为符合预期,并使用代理文本字段。
与使用value:
和NumberFormatter
的版本不同,.onReceive
方法会立即响应,我们用它来设置绑定的真实整数值。当我们这样做的时候,我们检查文本是否真的产生了一个整数。
.onAppear
方法用于从整数填充字符串
您可以对 FloatField
执行相同的操作。
这可能会在 Apple 完成之前完成这项工作。
受上面接受的代理答案的启发,这里有一个具有相当数量代码的现成的结构。我真的希望 Apple 可以添加一个选项来切换行为。
struct TextFieldRow<T>: View {
var value: Binding<T>
var title: String
var subtitle: String?
var valueProxy: Binding<String> {
switch T.self {
case is String.Type:
return Binding<String>(
get: { self.value.wrappedValue as! String },
set: { self.value.wrappedValue = [=10=] as! T } )
case is String?.Type:
return Binding<String>(
get: { (self.value.wrappedValue as? String).bound },
set: { self.value.wrappedValue = [=10=] as! T })
case is Double.Type:
return Binding<String>( get: { String(self.value.wrappedValue as! Double) },
set: {
let doubleFormatter = NumberFormatter()
doubleFormatter.numberStyle = .decimal
doubleFormatter.maximumFractionDigits = 3
if let doubleValue = doubleFormatter.number(from: [=10=])?.doubleValue {
self.value.wrappedValue = doubleValue as! T
}
}
)
default:
fatalError("not supported")
}
}
var body: some View {
return HStack {
VStack(alignment: .leading) {
Text(title)
if let subtitle = subtitle, subtitle.isEmpty == false {
Text(subtitle)
.font(.caption)
.foregroundColor(Color(UIColor.secondaryLabel))
}
}
Spacer()
TextField(title, text: valueProxy)
.multilineTextAlignment(.trailing)
}
}
}
为了保持简洁和轻便,我在视图模型中使用 getter/setter 结束了转换类型,并保留了文本类型 TextField。
快速而肮脏(大概),但它有效并且我不觉得我在战斗 SwiftUI。
查看正文
struct UserDetails: View {
@ObservedObject var userViewModel: UserViewModel
init(user: PedalUserViewModel) {
userViewModel = user
}
var body: some View {
VStack {
Form {
Section(header: Text("Personal Information")) {
TextField("Age", text: $userViewModel.userAge)
.keyboardType(.numberPad)
.modifier(DoneButton())
}
}
}
}
}
ViewModel
class UserViewModel: ObservableObject {
@ObservedObject var currentUser: User
var anyCancellable: AnyCancellable?
init(currentUser: User) {
self.currentUser = currentUser
self.anyCancellable = self.currentUser.objectWillChange.sink{ [weak self] (_) in
self?.objectWillChange.send()
}
}
var userAge: String {
get {
String(currentUser.userAge)
}
set {
currentUser.userAge = Int(newValue) ?? 0
}
}
}
import Foundation
import SwiftUI
struct FormattedTextField<T: Equatable>: View {
let placeholder: LocalizedStringKey
@Binding var value: T
let formatter: Formatter
var valueChanged: ((T) -> Void)? = nil
var editingChanged: ((Bool) -> Void)? = nil
var onCommit: (() -> Void)? = nil
@State private var isUpdated = false
var proxy: Binding<String> {
Binding<String>(
get: {
formatter.string(for: value) ?? ""
},
set: {
var obj: AnyObject? = nil
formatter.getObjectValue(&obj, for: [=10=], errorDescription: nil)
if let newValue = obj as? T {
let notifyUpdate = newValue == value
value = newValue
valueChanged?(value)
if notifyUpdate {
isUpdated.toggle()
}
}
}
)
}
var body: some View {
TextField(
placeholder,
text: proxy,
onEditingChanged: { isEditing in
editingChanged?(isEditing)
},
onCommit: {
onCommit?()
}
)
.tag(isUpdated ? 0 : 1)
}
}
目前 iOS 14 个具有值初始化程序的 TextField 未更新状态。
I found a workaround for this bug and can be used NSNumber, Double ... and a NumberFormatter.
This a new brand TextField that accept NSNumber and NumberFormatter
extension TextField {
public init(_ prompt: LocalizedStringKey, value: Binding<NSNumber>, formatter: NumberFormatter) where Text == Label {
self.init(
prompt,
text: .init(get: {
formatter.string(for: value.wrappedValue) ?? String()
}, set: {
let string = [=10=]
.replacingOccurrences(of: formatter.groupingSeparator, with: "")
value.wrappedValue = formatter.number(from: string) ?? .init(value: Float.zero)
})
)
}
}
Or you can implement you own Logic inside the binding get and set methods
TextField("placeholder", text: .init(
get: {
decimalFormatter.string(from: number) ?? ""
},
set: {
let string = [=11=]
.replacingOccurrences(of: decimalFormatter.groupingSeparator, with: "")
_number.wrappedValue = decimalFormatter.number(from: string)
?? .init(value: Double.zero)
}
))
Swift 5.5 和 iOS 15 具有新的格式化 API。
我正在寻找一个干净的货币格式化程序并偶然发现了这个文档。
请参阅此处的文档:
ParseableFormatStyle
这仍然不会在您键入时更新 TextField 绑定值。但是,您不再需要按 return 来触发格式化。您可以简单地退出 TextField。当您单击返回 TextField 以编辑原始值时,它的行为也符合预期。
这是一个工作示例:
import SwiftUI
struct FormatTest: View {
@State var myNumber: Double?
@State var myDate: Date.FormatStyle.FormatInput?
var body: some View {
Form {
TextField("", value: $myNumber, format: .currency(code: "USD"), prompt: Text("Enter a number:"))
TextField("", value: $myDate, format: .dateTime.month(.twoDigits).day(.twoDigits).year(), prompt: Text("MM/DD/YY"))
Text(myDate?.formatted(.dateTime.weekday(.wide)) ?? "")
}
}
}
struct FormatTest_Previews: PreviewProvider {
static var previews: some View {
FormatTest()
}
}
我正在尝试更新数字字段,因此我使用了带有格式化程序:参数集的 TextField。它将数字格式化为输入字段就好了,但在编辑时不会更新绑定值。 TextField 在没有指定格式化程序的情况下工作正常(在字符串上)。这是一个错误还是我遗漏了什么?
更新:从 Xcode 11 beta 3 开始,它可以正常工作。现在,如果您编辑数字 TextField,绑定值会在您点击 return 后更新。每次按键后,String TextField 仍会更新。我猜他们不想在每次按键时将要格式化的值发送给格式化程序,或者可能 is/will 是 TextField 的修饰符来告诉它这样做。
请注意 API 略有变化;旧的 TextField init() 已弃用,新的 titleKey String 字段已添加为第一个参数,该参数在该字段中显示为占位符文本。
struct TestView : View {
@State var someText = "Change me!"
@State var someNumber = 123.0
var body: some View {
Form {
// Xcode 11 beta 2
// TextField($someText)
// TextField($someNumber, formatter: NumberFormatter())
// Xcode 11 beta 3
TextField("Text", text: $someText)
TextField("Number", value: $someNumber, formatter: NumberFormatter())
Spacer()
// if you change the first TextField value, the change shows up here
// if you change the second (the number),
// it does not *until you hit return*
Text("text: \(self.someText), number: \(self.someNumber)")
// the button does the same, but logs to the console
Button(action: { print("text: \(self.someText), number: \(self.someNumber)")}) {
Text("Log Values")
}
}
}
}
如果您键入第一个(字符串)TextField,文本视图中的值会立即更新。如果您编辑第二个(数字),什么也不会发生。 同样,点击 Button 会显示 String 的更新值,但不会显示数字。我只在模拟器中试过这个。
似乎在使用 value:
作为输入时,SwiftUI 不会为用户点击的任何键重新加载视图。而且,正如您提到的,它会在用户退出或提交该字段时重新加载视图。
另一方面,每当按下一个键时,SwiftUI 使用 text:
作为输入(立即)重新加载视图。我没有别的想法。
就我而言,我为 someNumber2
做了如下操作:
struct ContentView: View {
@State var someNumber = 123.0
@State var someNumber2 = "123"
var formattedNumber : NSNumber {
let formatter = NumberFormatter()
guard let number = formatter.number(from: someNumber2) else {
print("not valid to be converted")
return 0
}
return number
}
var body: some View {
VStack {
TextField("Number", value: $someNumber, formatter: NumberFormatter())
TextField("Number2", text: $someNumber2)
Text("number: \(self.someNumber)")
Text("number: \(self.formattedNumber)")
}
}
}
您可以使用 Binding 将 Double<-->String 转换为 TextField
struct TestView: View {
@State var someNumber = 123.0
var body: some View {
let someNumberProxy = Binding<String>(
get: { String(format: "%.02f", Double(self.someNumber)) },
set: {
if let value = NumberFormatter().number(from: [=10=]) {
self.someNumber = value.doubleValue
}
}
)
return VStack {
TextField("Number", text: someNumberProxy)
Text("number: \(someNumber)")
}
}
}
您可以使用计算属性 的方式来解决这个问题。 (感谢@iComputerfreak)
struct TestView: View {
@State var someNumber = 123.0
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%.02f", Double(self.someNumber)) },
set: {
if let value = NumberFormatter().number(from: [=11=]) {
self.someNumber = value.doubleValue
}
}
)
}
var body: some View {
VStack {
TextField("Number", text: someNumberProxy)
Text("number: \(someNumber)")
}
}
}
我知道这有一些可接受的答案,但上面的答案在输入值时似乎有错误的用户体验结果(至少对于双精度值)。所以我决定编写自己的解决方案。它很大程度上受到这里答案的启发,所以我会先尝试这里的其他例子,然后再尝试这个例子,因为它有更多的代码。
WARNING Although I have been an iOS developer for a long time, I'm fairly new to SwiftUI. So this is far from expert advice. I would love feedback on my approach but be nice. So far this has been working out well on my new project. However, I doubt this is as efficient as Apple's formatters.
protocol NewFormatter {
associatedtype Value: Equatable
/// The logic that converts your value to a string presented by the `TextField`. You should omit any values
/// - Parameter object: The value you are converting to a string.
func toString(object: Value) -> String
/// Once the change is allowed and the input is final, this will convert
/// - Parameter string: The full text currently on the TextField.
func toObject(string: String) -> Value
/// Specify if the value contains a final result. If it does not, nothing will be changed yet.
/// - Parameter string: The full text currently on the TextField.
func isFinal(string: String) -> Bool
/// Specify **all** allowed inputs, **including** intermediate text that cannot be converted to your object **yet** but are necessary in the input process for a final result. It will allow this input without changing your value until a final correct value can be determined.
/// For example, `1.` is not a valid `Double`, but it does lead to `1.5`, which is. Therefore the `DoubleFormatter` would return true on `1.`.
/// Returning false will reset the input to the previous allowed value.
/// - Parameter string: The full text currently on the TextField.
func allowChange(to string: String) -> Bool
}
struct NewTextField<T: NewFormatter>: View {
let title: String
@Binding var value: T.Value
let formatter: T
@State private var previous: T.Value
@State private var previousGoodString: String? = nil
init(_ title: String, value: Binding<T.Value>, formatter: T) {
self.title = title
self._value = value
self._previous = State(initialValue: value.wrappedValue)
self.formatter = formatter
}
var body: some View {
let changedValue = Binding<String>(
get: {
if let previousGoodString = self.previousGoodString {
let previousValue = self.formatter.toObject(string: previousGoodString)
if previousValue == self.value {
return previousGoodString
}
}
let string = self.formatter.toString(object: self.value)
return string
},
set: { newString in
if self.formatter.isFinal(string: newString) {
let newValue = self.formatter.toObject(string: newString)
self.previousGoodString = newString
self.previous = newValue
self.value = newValue
} else if !self.formatter.allowChange(to: newString) {
self.value = self.previous
}
}
)
return TextField(title, text: changedValue)
}
}
然后您可以为 Double
创建自定义格式化程序,如下所示:
/// An object that converts a double to a valid TextField value.
struct DoubleFormatter: NewFormatter {
let numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.allowsFloats = true
numberFormatter.numberStyle = .decimal
numberFormatter.maximumFractionDigits = 15
return numberFormatter
}()
/// The logic that converts your value to a string used by the TextField.
func toString(object: Double) -> String {
return numberFormatter.string(from: NSNumber(value: object)) ?? ""
}
/// The logic that converts the string to your value.
func toObject(string: String) -> Double {
return numberFormatter.number(from: string)?.doubleValue ?? 0
}
/// Specify if the value contains a final result. If it does not, nothing will be changed yet.
func isFinal(string: String) -> Bool {
return numberFormatter.number(from: string) != nil
}
/// Specify **all** allowed values, **including** intermediate text that cannot be converted to your object **yet** but are necessary in the input process for a final result.
/// For example, `1.` is not a valid `Double`, but it does lead to `1.5`, which is. It will allow this input without changing your value until a final correct value can be determined.
/// Returning false will reset the input the the previous allowed value. For example, when using the `DoubleFormatter` the input `0.1j` would result in false which would reset the value back to `0.1`.
func allowChange(to string: String) -> Bool {
let components = string.components(separatedBy: ".")
if components.count <= 2 {
// We allow an Integer or an empty value.
return components.allSatisfy({ [=11=] == "" || Int([=11=]) != nil })
} else {
// If the count is > 2, we have more than one decimal
return false
}
}
}
你可以像这样使用这个新组件:
NewTextField(
"Value",
value: $bodyData.doubleData.value,
formatter: DoubleFormatter()
)
以下是我能想到的其他一些用法:
/// Just a simple passthrough formatter to use on a NewTextField
struct PassthroughFormatter: NewFormatter {
func toString(object: String) -> String {
return object
}
func toObject(string: String) -> String {
return string
}
func isFinal(string: String) -> Bool {
return true
}
func allowChange(to string: String) -> Bool {
return true
}
}
/// A formatter that converts empty strings to nil values
struct EmptyStringFormatter: NewFormatter {
func toString(object: String?) -> String {
return object ?? ""
}
func toObject(string: String) -> String? {
if !string.isEmpty {
return string
} else {
return nil
}
}
func isFinal(string: String) -> Bool {
return true
}
func allowChange(to string: String) -> Bool {
return true
}
}
方案B。由于使用value:
和NumberFormatter
不起作用,我们可以使用自定义的TextField
。我已将 TextField
包裹在 struct
中,以便您可以尽可能透明地使用它。
我对Swift和SwiftUI都是非常新手,所以毫无疑问是一个更优雅的解决方案。
struct IntField: View {
@Binding var int: Int
@State private var intString: String = ""
var body: some View {
return TextField("", text: $intString)
.onReceive(Just(intString)) { value in
if let i = Int(value) { int = i }
else { intString = "\(int)" }
}
.onAppear(perform: {
intString = "\(int)"
})
}
}
并且在 ContentView 中:
struct ContentView: View {
@State var testInt: Int = 0
var body: some View {
return HStack {
Text("Number:")
IntField(int: $testInt);
Text("Value: \(testInt)")
}
}
}
基本上,我们使用 TextField("…", text: …)
,它的行为符合预期,并使用代理文本字段。
与使用value:
和NumberFormatter
的版本不同,.onReceive
方法会立即响应,我们用它来设置绑定的真实整数值。当我们这样做的时候,我们检查文本是否真的产生了一个整数。
.onAppear
方法用于从整数填充字符串
您可以对 FloatField
执行相同的操作。
这可能会在 Apple 完成之前完成这项工作。
受上面接受的代理答案的启发,这里有一个具有相当数量代码的现成的结构。我真的希望 Apple 可以添加一个选项来切换行为。
struct TextFieldRow<T>: View {
var value: Binding<T>
var title: String
var subtitle: String?
var valueProxy: Binding<String> {
switch T.self {
case is String.Type:
return Binding<String>(
get: { self.value.wrappedValue as! String },
set: { self.value.wrappedValue = [=10=] as! T } )
case is String?.Type:
return Binding<String>(
get: { (self.value.wrappedValue as? String).bound },
set: { self.value.wrappedValue = [=10=] as! T })
case is Double.Type:
return Binding<String>( get: { String(self.value.wrappedValue as! Double) },
set: {
let doubleFormatter = NumberFormatter()
doubleFormatter.numberStyle = .decimal
doubleFormatter.maximumFractionDigits = 3
if let doubleValue = doubleFormatter.number(from: [=10=])?.doubleValue {
self.value.wrappedValue = doubleValue as! T
}
}
)
default:
fatalError("not supported")
}
}
var body: some View {
return HStack {
VStack(alignment: .leading) {
Text(title)
if let subtitle = subtitle, subtitle.isEmpty == false {
Text(subtitle)
.font(.caption)
.foregroundColor(Color(UIColor.secondaryLabel))
}
}
Spacer()
TextField(title, text: valueProxy)
.multilineTextAlignment(.trailing)
}
}
}
为了保持简洁和轻便,我在视图模型中使用 getter/setter 结束了转换类型,并保留了文本类型 TextField。
快速而肮脏(大概),但它有效并且我不觉得我在战斗 SwiftUI。
查看正文
struct UserDetails: View {
@ObservedObject var userViewModel: UserViewModel
init(user: PedalUserViewModel) {
userViewModel = user
}
var body: some View {
VStack {
Form {
Section(header: Text("Personal Information")) {
TextField("Age", text: $userViewModel.userAge)
.keyboardType(.numberPad)
.modifier(DoneButton())
}
}
}
}
}
ViewModel
class UserViewModel: ObservableObject {
@ObservedObject var currentUser: User
var anyCancellable: AnyCancellable?
init(currentUser: User) {
self.currentUser = currentUser
self.anyCancellable = self.currentUser.objectWillChange.sink{ [weak self] (_) in
self?.objectWillChange.send()
}
}
var userAge: String {
get {
String(currentUser.userAge)
}
set {
currentUser.userAge = Int(newValue) ?? 0
}
}
}
import Foundation
import SwiftUI
struct FormattedTextField<T: Equatable>: View {
let placeholder: LocalizedStringKey
@Binding var value: T
let formatter: Formatter
var valueChanged: ((T) -> Void)? = nil
var editingChanged: ((Bool) -> Void)? = nil
var onCommit: (() -> Void)? = nil
@State private var isUpdated = false
var proxy: Binding<String> {
Binding<String>(
get: {
formatter.string(for: value) ?? ""
},
set: {
var obj: AnyObject? = nil
formatter.getObjectValue(&obj, for: [=10=], errorDescription: nil)
if let newValue = obj as? T {
let notifyUpdate = newValue == value
value = newValue
valueChanged?(value)
if notifyUpdate {
isUpdated.toggle()
}
}
}
)
}
var body: some View {
TextField(
placeholder,
text: proxy,
onEditingChanged: { isEditing in
editingChanged?(isEditing)
},
onCommit: {
onCommit?()
}
)
.tag(isUpdated ? 0 : 1)
}
}
目前 iOS 14 个具有值初始化程序的 TextField 未更新状态。
I found a workaround for this bug and can be used NSNumber, Double ... and a NumberFormatter. This a new brand TextField that accept NSNumber and NumberFormatter
extension TextField {
public init(_ prompt: LocalizedStringKey, value: Binding<NSNumber>, formatter: NumberFormatter) where Text == Label {
self.init(
prompt,
text: .init(get: {
formatter.string(for: value.wrappedValue) ?? String()
}, set: {
let string = [=10=]
.replacingOccurrences(of: formatter.groupingSeparator, with: "")
value.wrappedValue = formatter.number(from: string) ?? .init(value: Float.zero)
})
)
}
}
Or you can implement you own Logic inside the binding get and set methods
TextField("placeholder", text: .init(
get: {
decimalFormatter.string(from: number) ?? ""
},
set: {
let string = [=11=]
.replacingOccurrences(of: decimalFormatter.groupingSeparator, with: "")
_number.wrappedValue = decimalFormatter.number(from: string)
?? .init(value: Double.zero)
}
))
Swift 5.5 和 iOS 15 具有新的格式化 API。
我正在寻找一个干净的货币格式化程序并偶然发现了这个文档。
请参阅此处的文档: ParseableFormatStyle
这仍然不会在您键入时更新 TextField 绑定值。但是,您不再需要按 return 来触发格式化。您可以简单地退出 TextField。当您单击返回 TextField 以编辑原始值时,它的行为也符合预期。
这是一个工作示例:
import SwiftUI
struct FormatTest: View {
@State var myNumber: Double?
@State var myDate: Date.FormatStyle.FormatInput?
var body: some View {
Form {
TextField("", value: $myNumber, format: .currency(code: "USD"), prompt: Text("Enter a number:"))
TextField("", value: $myDate, format: .dateTime.month(.twoDigits).day(.twoDigits).year(), prompt: Text("MM/DD/YY"))
Text(myDate?.formatted(.dateTime.weekday(.wide)) ?? "")
}
}
}
struct FormatTest_Previews: PreviewProvider {
static var previews: some View {
FormatTest()
}
}