带有格式化程序的SwiftUI TextField无法正常工作?

时间:2019-06-27 23:28:11

标签: swiftui

我正在尝试更新一个数字字段,所以我在使用带有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,则“文本”视图中的值将立即更新。如果您编辑第二个(数字),则什么也不会发生。 同样,点击“按钮”会显示字符串的更新值,而不是数字。我只在模拟器中尝试过。

7 个答案:

答案 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)
    }
    
}