【问题标题】:SwiftUI TextField with formatter not working?带有格式化程序的 SwiftUI TextField 不起作用?
【发布时间】:2019-11-09 23:20:33
【问题描述】:

我正在尝试更新数字字段,因此我使用了带有 formatter: 参数集的 TextField。它将数字格式化为输入字段就好了,但在编辑时不会更新绑定值。没有指定格式化程序的 TextField 可以正常工作(在字符串上)。这是一个错误还是我错过了什么?

更新:从 Xcode 11 beta 3 开始,它可以正常工作。现在,如果您编辑数字文本字段,则在您按回车后会更新绑定值。每次按键后仍会更新 String TextField。我猜他们不想在每次按键时将要格式化的值发送到格式化程序,或者可能有/将会有一个修饰符让 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")
            }
        }
    }
}

如果您键入第一个(字符串)文本字段,文本视图中的值会立即更新。如果您编辑第二个(数字),则不会发生任何事情。 同样,点击按钮会显示字符串的更新值,但不会显示数字。我只在模拟器中尝试过。

【问题讨论】:

  • 这里一样,尝试将其设置为.textContentType(.emailAddress)(在我的情况下,我需要电子邮件键盘)但也没有任何更改,我想这是一个错误
  • FTR,在 beta6 中,带有格式化程序的 TextFields 似乎工作得更好。
  • @marcprux 你能举个例子吗,因为在我的情况下不能使用 Xcode 11 Beta 6。她我的代码:TextField("", value: $grindSize, formatter: NumberFormatter()) 我需要按回车键来更新值。
  • 如何在键盘上获得 Return?我正在使用 .decimalPad 键盘,它缺少 Return、Done 等。因此,TextField 永远不会更新值:。相反,如果我在模拟器中使用硬件键盘(那里有一个返回键)它工作正常
  • 非常有用的实现可以在这里找到:github.com/edw/swiftui-numberfield

标签: swiftui


【解决方案1】:

您可以使用Binding将DoubleString转换为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: $0) {
                    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: $0) {
                    self.someNumber = value.doubleValue
                }
            }
        )
    }

    var body: some View {
        VStack {
            TextField("Number", text: someNumberProxy)

            Text("number: \(someNumber)")
        }
      }
}

【讨论】:

  • 我只是希望苹果的开发人员能意识到这太笨拙了!文本字段应该适用于 String 和 Double ,而我们不必创建变通方法来处理它们不够充分的远见。
  • 应该指出,这种模式在 XCode 中被标记为从 body 修改状态,这似乎违反了框架模式 - 表示您无法访问 @987654325 之外的 @State 值@ 要么 - 所以我真的不明白你是如何实现这些非平凡的模式
  • 我不明白你在这种情况下一切都是在法律范围内进行的
  • 我发现的一个问题是它会在你想要之前自动格式化整个数字。假设您在该字段中有5,那么您想将其更改为2.56。第二次输入2,它会给你2.00。然后你删除0并输入5,它会给你2.50
  • @Ranoiaetep 是的,我遇到了同样的问题,我尝试更改精度但无法正常工作。你设法解决了这个问题吗?
【解决方案2】:

似乎在使用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)")
    }
  }
}

【讨论】:

    【解决方案3】:

    方案 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 完成工作之前完成。

    【讨论】:

      【解决方案4】:

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

      警告 虽然我已经做了很长时间的 iOS 开发人员,但我对 SwiftUI 还是很陌生。因此,这与专家建议相去甚远。我喜欢 反馈我的方法,但要友善。到目前为止,这一直在解决 我的新项目很好。但是,我怀疑这是否与 Apple 的格式化程序一样有效。

      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({ $0 == "" || Int($0) != 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
          }
      }
      

      【讨论】:

        【解决方案5】:

        为了保持简洁和轻量级,我在视图模型中使用 getter/setter 进行类型转换,并保留文本类型 TextField。

        又快又脏(ish),但它可以工作并且感觉不像我在战斗 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())
                        }
                    }
                }
            }
        }
        

        视图模型

        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
                }
            }
        }
        

        【讨论】:

          【解决方案6】:

          受上述公认的代理答案的启发,这里有一个包含大量代码的现成结构。我真的希望 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 = $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)
                  }
              }
          }
          

          【讨论】:

            【解决方案7】:
            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)
                }
                
            }
            

            【讨论】:

              【解决方案8】:

              目前带有值初始化器的 iOS 14 TextField 不会更新状态。

              我找到了解决此错误的方法,可以使用 NSNumber、Double ... 和 NumberFormatter。 这是一个接受 NSNumber 和 NumberFormatter 的新品牌 TextField

              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 = $0
                                  .replacingOccurrences(of: formatter.groupingSeparator, with: "")
                              value.wrappedValue = formatter.number(from: string) ?? .init(value: Float.zero)
                          })
                      )
                  }
              }
              

              或者你可以在绑定的get和set方法中实现你自己的逻辑

              TextField("placeholder", text: .init(
                              get: {
                                  decimalFormatter.string(from: number) ?? ""
                              },
                              set: {
                                  let string = $0
                                      .replacingOccurrences(of: decimalFormatter.groupingSeparator, with: "")
              
                                  _number.wrappedValue = decimalFormatter.number(from: string)
                                          ?? .init(value: Double.zero)
                              }
                          ))
              

              【讨论】:

                【解决方案9】:

                Swift 5.5 和 iOS 15 具有新的格式化 API。

                我正在寻找一个干净的货币格式化程序并偶然发现了这个文档。

                在此处查看文档: ParseableFormatStyle

                这仍然不会在您键入时更新 TextField 绑定值。但是,您不再需要按回车键来触发格式化。您可以简单地退出 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()
                    }
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-01-07
                  • 2019-06-10
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-12-31
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多