从按钮操作中更改时未触发 SwiftUI 绑定

Posted

技术标签:

【中文标题】从按钮操作中更改时未触发 SwiftUI 绑定【英文标题】:SwiftUI Binding not triggering when changed from within a button action 【发布时间】:2021-01-05 00:56:11 【问题描述】:

我构建了一个具有以下附加功能的自定义文本字段:

    可以是安全的/不安全的 有一个显示/隐藏密码按钮(如果安全) 有一个明文按钮 下面有一个错误文本标签。 输入文本时,占位符在文本字段上方动画(类似于 android 材料文本字段) 有一个 onChange 闭包,您可以在其中运行验证

按钮操作将我的文本绑定设置为“”

当我在文本字段中输入文本时,绑定会正确触发。

但是当我点击按钮清除文本时,绑定不会触发。

这是有原因的吗?

这是我的结构。请参阅 cmets 了解问题所在。

public struct CustomTextField: View 
    // MARK: - Property Wrappers

    @Environment(\.isEnabled) private var isEnabled: Bool
    @Binding private var text: String
    @Binding private var errorText: String
    @State private var secureTextHidden: Bool = true

    // MARK: - Struct Properties

    private var placeholder: String
    private var isSecure: Bool
    private var hasLabel: Bool = false
    private var hasErrorLabel: Bool = false
    private var onChange: ((String) -> Void)?

    // MARK: - Computed Properties

    private var borderColor: Color 
        guard isEnabled else 
            return .gray
        

        if !errorText.isEmpty 
            return .red
         else if !text.isEmpty 
            return .blue
         else 
            return .gray
        
    

    private var textColor: Color 
        guard isEnabled else 
            return Color.gray.opacity(0.5)
        

        return .black
    

    private var textField: some View 
        let binding = Binding<String> 
            self.text
         set: 
            // This is triggered correctly when text changes, but not when text is changed within my button action.
            self.text = $0
            onChange?($0)
        

        if isSecure && secureTextHidden 
            return SecureField(placeholder, text: binding)
                .eraseToAnyView()
         else 
            return TextField(placeholder, text: binding)
                .eraseToAnyView()
        
    

    private var hasText: Bool  !text.isEmpty 
    private var hasError: Bool  !errorText.isEmpty 

    // MARK: - Init

    /// Initializes a new CustomTextField
    /// - Parameters:
    ///   - placeholder: the textfield placeholder
    ///   - isSecure: if true, textfield will behave like a password field
    ///   - hasLabel: Show placeholder as a label when text is entered
    ///   - hasErrorLabel: Visible Error Label underneath
    ///   - onChange: code that will run on each keystroke (optional)
    public init(
        placeholder: String,
        text: Binding<String>,
        errorText: Binding<String> = .constant(""),
        isSecure: Bool = false,
        hasLabel: Bool = false,
        hasErrorLabel: Bool = false,
        onChange: ((String) -> Void)? = nil
    ) 
        self.placeholder = placeholder
        _text = text
        _errorText = errorText
        self.isSecure = isSecure
        self.hasLabel = hasLabel
        self.hasErrorLabel = hasErrorLabel
        self.onChange = onChange
    

    // MARK: - Body

    public var body: some View 
        VStack(alignment: .leading, spacing: .textMargin) 
            if hasLabel 
                Text("\(placeholder)")
                    .foregroundColor(textColor)
                    .offset(
                        x: hasText ? 0 : 16,
                        y: hasText ? 0 : 30
                    )
                    .opacity(hasText ? 1 : 0)
                    .animation(.easeOut(duration: 0.3))
             else 
                EmptyView()
            

            HStack(alignment: .center, spacing: 8) 
                ZStack(alignment: Alignment(horizontal: .trailing, vertical: .center)) 
                    HStack(alignment: .center, spacing: 16) 
                        textField

                        HStack(alignment: .center, spacing: 16) 
                            if hasText && isEnabled 
                                Button 
                                    text = ""
                                    // I had to trigger onChange manually as setting my text above is not triggering my binding block.
                                    onChange?(text)
                                 label: 
                                    Image(systemName: "xmark.circle.fill")
                                
                                .buttonStyle(BorderlessButtonStyle())
                                .foregroundColor(.black)
                            

                            if isSecure && isEnabled 
                                Button 
                                    secureTextHidden.toggle()
                                 label: 
                                    Image(systemName: secureTextHidden ? "eye.fill" : "eye.slash.fill")
                                
                                .buttonStyle(BorderlessButtonStyle())
                                .foregroundColor(Color.gray.opacity(0.5))
                            
                        
                    
                
                .padding(.margin)
                .frame(height: .textFieldHeight, alignment: .center)
                .background(
                    ZStack 
                        RoundedRectangle(cornerRadius: 4)
                            .fill(.white)
                        RoundedRectangle(cornerRadius: 4)
                            .strokeBorder(borderColor, lineWidth: 2)
                    
                )
            

            if hasErrorLabel && isEnabled 
                Text(errorText)
                    .lineLimit(2)
                    .font(.caption)
                    .foregroundColor(.red)
                    .offset(y: hasError ? 0 : -.textFieldHeight)
                    .opacity(hasError ? 1 : 0)
                    .animation(.easeOut(duration: 0.3))
             else 
                EmptyView()
            
        
        .foregroundColor(textColor)
        .onAppear 
            if hasText 
                onChange?(text)
            
        
    

【问题讨论】:

与 Xcode 12.1 / ios 14.1 配合使用,只需简单的复制和绑定到字符串状态属性。 @Asperi 您在按钮操作中注释掉了 onChange 吗?它应该触发绑定集块内的 onChange 。我必须将它添加到按钮操作中,因为当我将文本设置为“”时它不会触发我的绑定 您是否在按钮操作中注释掉了 onChange? - 是的,只使用了 text = "" @Asperi 真的很奇怪......它对我不起作用...... 【参考方案1】:

更改text 绑定不会影响传递给文本字段的binding 绑定。您应该正确了解这两个绑定之间的关系。 set: 块仅在 binding 已被文本字段更改时触发。

如果您的最低目标 iOS 是 14,您还可以使用 onChange 修饰符而不是您的处理程序来观察 text 绑定的任何变化,以便在绑定发生变化时调用您的 onChange。而且,您不必为文本字段进行binding 绑定。直接通过text绑定即可。

【讨论】:

不幸的是,我的最低目标是 13 当@Binding 值发生变化时,还有其他方法可以触发某些事情吗?除了设置的代码块?

以上是关于从按钮操作中更改时未触发 SwiftUI 绑定的主要内容,如果未能解决你的问题,请参考以下文章

根据 SwiftUI 中的切换值更改文本

SwiftUI:“@Published”属性更改时未刷新“带有组的动态列表”

第一次选择pickerView时未触发条件

将 SwiftUI 按钮绑定到 AnySubscriber,例如 RxCocoa 的按钮点击

在主干中绑定模型时未触发 keyup 事件

SwiftUI 在按钮上触发操作而无需用户单击按钮