我正在尝试更新一个数字字段,所以我在使用带有formatter:参数集的TextField。它可以很好地将数字格式化为输入字段,但在编辑时不会更新绑定值。在不指定格式化程序的情况下,TextField可以正常工作(在String上)。这是错误还是我错过了什么?
更新:从Xcode 11 beta 3开始,它可以正常工作。现在,如果您编辑数字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")
}
}
}
}
如果键入第一个(字符串)TextField,则“文本”视图中的值将立即更新。如果您编辑第二个(数字),则什么也不会发生。 同样,点击“按钮”会显示字符串的更新值,而不是数字。我只在模拟器中尝试过。
答案 0 :(得分:11)
您可以使用Binding将Double <->字符串转换为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)")
}
}
}
答案 1 :(得分:6)
似乎在使用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)")
}
}
}
答案 2 :(得分:4)
方案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完成工作为止。
答案 3 :(得分:1)
我知道这有一些可以接受的答案,但是在输入值(至少是双精度)时,上面的答案似乎会产生错误的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
}
}
答案 4 :(得分:0)
受上述公认的代理人答案的启发,这是一个可以使用的具有大量代码的结构。我真的希望苹果公司可以添加一个选项来切换行为。
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)
}
}
}
答案 5 :(得分:0)
为了保持整洁和轻巧,我在视图模型中使用了一个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())
}
}
}
}
}
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
}
}
}
答案 6 :(得分: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)
}
}