格式化程序不工作的SwiftUI文本字段?

格式化程序不工作的SwiftUI文本字段?,swiftui,Swiftui,我试图更新一个数字字段,所以我使用了一个带有formatter:parameter集的TextField。它将数字格式化到输入字段中,但在编辑时不会更新绑定值。在没有指定格式化程序的情况下,TextField可以正常工作(在字符串上)。这是一个错误还是我遗漏了什么 更新:从Xcode 11 beta 3开始,它可以正常工作。现在,如果编辑数字文本字段,则在单击return后,绑定值将更新。每次按键后,字符串文本字段仍会更新。我猜他们不想在每次按键时都将要格式化的值发送到格式化程序,或者可能会有一

我试图更新一个数字字段,所以我使用了一个带有formatter:parameter集的TextField。它将数字格式化到输入字段中,但在编辑时不会更新绑定值。在没有指定格式化程序的情况下,TextField可以正常工作(在字符串上)。这是一个错误还是我遗漏了什么

更新:从Xcode 11 beta 3开始,它可以正常工作。现在,如果编辑数字文本字段,则在单击return后,绑定值将更新。每次按键后,字符串文本字段仍会更新。我猜他们不想在每次按键时都将要格式化的值发送到格式化程序,或者可能会有一个TextField的修饰符来告诉它这样做

请注意,API略有变化;旧的TextField init()已被弃用,并添加了一个新的titleKey字符串字段作为第一个参数,它在字段中显示为占位符文本

struct TestView:View{
@State var someText=“更改我!”
@状态变量someNumber=123.0
var body:一些观点{
形式{
//Xcode 11 beta 2
//TextField($someText)
//TextField($someNumber,格式化程序:NumberFormatter())
//Xcode 11 beta 3
TextField(“Text”,Text:$someText)
TextField(“Number”,值:$someNumber,格式化程序:NumberFormatter())
垫片()
//如果更改第一个TextField值,更改将显示在此处
//如果更改第二个(数字),
//直到你点击回车键,它才开始*
文本(“文本:\(self.someText),数字:\(self.someNumber)”)
//按钮也会执行相同的操作,但会登录到控制台
按钮(操作:{print(“text:\(self.someText),number:\(self.someNumber)”)}){
文本(“日志值”)
}
}
}
}
如果键入第一个(字符串)文本字段,文本视图中的值将立即更新。如果编辑第二个(数字),则不会发生任何事情。
类似地,点击按钮会显示字符串的更新值,但不会显示数字。我只在模拟器中尝试过这一点。

似乎在使用
值:
作为输入时,SwiftUI不会为用户点击的任何键重新加载视图。而且,正如您所提到的,当用户退出字段或提交它时,它会重新加载视图

另一方面,只要按下一个键,SwiftUI就会使用
文本:
作为输入(立即)重新加载视图。我没有想到别的

在我的例子中,我为
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)")
    }
  }
}

您可以使用绑定转换TextField的DoubleString

struct TestView:View{
@状态变量someNumber=123.0
var body:一些观点{
让someNumberProxy=绑定(
获取:{String(格式:%.02f),Double(self.someNumber))},
设置:{
如果let value=NumberFormatter().number(from:$0){
self.someNumber=value.doubleValue
}
}
)
返回VStack{
文本字段(“数字”,文本:someNumberProxy)
文本(“编号:\(someNumber)”)
}
}
}
您可以使用计算属性方法来解决此问题。(谢谢@icomputerbreak)

struct TestView:View{
@状态变量someNumber=123.0
var someNumberProxy:绑定{
装订(
获取:{String(格式:%.02f),Double(self.someNumber))},
设置:{
如果let value=NumberFormatter().number(from:$0){
self.someNumber=value.doubleValue
}
}
)
}
var body:一些观点{
VStack{
文本字段(“数字”,文本:someNumberProxy)
文本(“编号:\(someNumber)”)
}
}
}

我知道这有一些公认的答案,但上面的答案在输入值时似乎有着油滑的UX结果(至少对于双精度)。所以我决定写我自己的解决方案。它很大程度上受到了这里答案的启发,所以我会先尝试其他示例,然后再尝试这个示例,因为它有更多的代码

警告虽然我已经是一名iOS开发人员很长时间了,但我对SwiftUI还是相当陌生的。因此,这远远不是专家的建议。我很乐意 对我的方法的反馈,但要友善。到目前为止,这是可行的 好的,我的新项目。然而,我怀疑这是否和苹果的格式化程序一样有效

要执行此操作,您可以如下方式使用此新组件:

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
包装在
struct
中,以便您可以尽可能透明地使用它

我对Swift和SwiftUI都很陌生,所以毫无疑问有一个更优雅的解决方案

struct IntField:视图{
@绑定变量int:int
@国家私有变量intString:String=“”
var body:一些观点{
返回文本字段(“,text:$intString)
.onReceive(Just(intString)){中的值
如果设i=Int(value){Int=i}
else{intString=“\(int)”}
}
.onAppear(表演:{
intString=“\(int)”
})
}
}
在ContentView中:

struct ContentView:View{
@状态变量testInt:Int=0
var body:一些观点{
回钉{
文本(“编号:”)
IntField(int:$testInt);
文本(“值:\(测试)”)
}
}
}
基本上,我们使用
TextField(“…”,text:…)
,它的行为符合需要,并使用代理文本字段

与使用
value:
NumberFormatter
的版本不同,
.onReceive
方法会立即响应,我们使用它来设置实际的整数值,它是绑定的。当我们这么做的时候,我们
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
    }
}
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 = $0 as! T } )
        case is String?.Type:
            return Binding<String>(
                get: { (self.value.wrappedValue as? String).bound },
                set: { self.value.wrappedValue = $0 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: $0)?.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)
        }
    }
}
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())
                }
            }
        }
    }
}
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: $0, 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)
    }
    
}