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

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

  1. 可以是安全的/不安全的
  2. 有一个显示/隐藏密码按钮(如果安全)
  3. 有一个明文按钮
  4. 下面有一个错误文本标签。
  5. 输入文本时,占位符在文本字段上方动画(类似于 android 材料文本字段)
  6. 有一个 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 真的很奇怪......它对我不起作用......

标签: swift swiftui


【解决方案1】:

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

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

【讨论】:

  • 不幸的是,我的最低目标是 13 当@Binding 值发生变化时,还有其他方法可以触发某些事情吗?除了设置的代码块?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-01
  • 2021-04-02
  • 2021-10-26
  • 1970-01-01
相关资源
最近更新 更多