当从按钮操作中更改时,SwiftUI绑定不会触发

当从按钮操作中更改时,SwiftUI绑定不会触发,swift,swiftui,Swift,Swiftui,我构建了一个具有以下附加功能的自定义文本字段: 可能是安全的/不安全的 具有显示/隐藏密码按钮(如果安全) 有一个明文按钮 下面有一个错误文本标签 输入文本时,占位符将在文本字段上方设置动画(类似于android材质文本字段) 有一个onChange闭包,您可以在其中运行验证 按钮操作将我的文本绑定设置为“” 当我在文本字段中输入文本时,绑定会正确触发 但当我点击按钮清除文本时,绑定不会触发 这有什么原因吗 这是我的结构。有关问题发生的位置,请参见注释 public struct CustomT

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

  • 可能是安全的/不安全的
  • 具有显示/隐藏密码按钮(如果安全)
  • 有一个明文按钮
  • 下面有一个错误文本标签
  • 输入文本时,占位符将在文本字段上方设置动画(类似于android材质文本字段)
  • 有一个onChange闭包,您可以在其中运行验证
  • 按钮操作将我的文本绑定设置为“”

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

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

    这有什么原因吗

    这是我的结构。有关问题发生的位置,请参见注释

    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)
                }
            }
        }
    }
    
    公共结构CustomTextField:视图{ //标记:-属性包装器 @环境(\.isEnabled)私有变量isEnabled:Bool @绑定私有变量文本:字符串 @绑定私有变量errorText:字符串 @状态私有变量secureTextHidden:Bool=true //标记:-结构属性 私有变量占位符:字符串 私有变量安全:Bool 私有变量hasLabel:Bool=false 私有变量hasErrorLabel:Bool=false 私有变量onChange:((字符串)->Void)? //标记:-计算属性 私有颜色:颜色{ 我找到了其他人{ 返回,格雷 } 如果!errorText.isEmpty{ 返回,红色 }否则,如果!text.isEmpty{ 返回,蓝色 }否则{ 返回,格雷 } } 私有变量textColor:Color{ 我找到了其他人{ 返回颜色。灰色。不透明度(0.5) } 返回,黑色 } 私有变量textField:一些视图{ 让绑定=绑定{ 自我文本 }设置:{ //当文本更改时会正确触发,但在“我的按钮”操作中更改文本时不会正确触发。 self.text=$0 一次更改?($0) } 如果isSecure&&secureTextHidden{ 返回SecureField(占位符,文本:绑定) .eraseToAnyView() }否则{ 返回文本字段(占位符,文本:绑定) .eraseToAnyView() } } 私有变量hasText:Bool{!text.isEmpty} 私有变量hasError:Bool{!errorText.isEmpty} //MARK:-Init ///初始化新的CustomTextField ///-参数: ///-占位符:文本字段占位符 ///-IsSecurity:如果为true,textfield的行为将类似于密码字段 ///-hasLabel:输入文本时将占位符显示为标签 ///-hasErrorLabel:下方可见的错误标签 ///-onChange:每次击键时运行的代码(可选) 公共初始化( 占位符:字符串, 案文:有约束力, errorText:Binding=.constant(“”), IsSecurity:Bool=false, hasLabel:Bool=false, hasErrorLabel:Bool=false, onChange:((字符串)->Void)?=nil ) { self.placeholder=占位符 _文本=文本 _errorText=errorText self.isSecure=isSecure self.hasLabel=hasLabel self.hasErrorLabel=hasErrorLabel self.onChange=onChange } //马克:身体 公共机构:一些看法{ VStack(对齐:。前导,间距:。文本边距){ 如果有标签{ 文本(“\(占位符)”) .foregroundColor(文本颜色) .抵消( x:hasText?0:16, y:hasText?0:30 ) .不透明度(hasText?1:0) .animation(.easeOut(持续时间:0.3)) }否则{ EmptyView() } HStack(对齐:。中心,间距:8){ ZStack(对齐:对齐(水平:尾随,垂直:中心)){ HStack(对齐:中心,间距:16){ 文本字段 HStack(对齐:中心,间距:16){ 如果已启用hasText&&I{ 钮扣{ text=“” //我必须手动触发onChange,因为上面设置的文本不会触发绑定块。 onChange?(文本) }标签:{ 图像(系统名称:“xmark.circle.fill”) } .buttonStyle(无边框buttonStyle()) .foregroundColor(.黑色) } 如果是安全的,则启用(&I){ 钮扣{ secureTextHidden.toggle() }标签:{ 图像(系统名称:secureTextHidden?“eye.fill”:“eye.slash.fill”) } .buttonStyle(无边框buttonStyle()) .前底色(颜色.灰色.不透明度(0.5)) } } } } .padding(.margin) .frame(高度:.textFieldHeight,对齐:.center) .背景( ZStack{ 圆角转角(拐角半径:4) .fill(.白色) 圆角转角(拐角半径:4) .strokeBorder(边框颜色,线宽:2) } ) } 如果已启用hasErrorLabel&&I{ 文本(错误文本) .lineLimit(2) .font(.caption) F