Привязка SwiftUI не запускается при изменении из действия кнопки

#swift #swiftui

#swift #swiftui

Вопрос:

Я создал пользовательское текстовое поле со следующими дополнительными функциями:

  1. может быть безопасным / небезопасным
  2. имеет кнопку показать / скрыть пароль (если защищен)
  3. имеет кнопку с открытым текстом
  4. имеет текстовую метку ошибки внизу.
  5. заполнитель анимируется над текстовым полем при вводе текста (аналогично текстовым полям Android material)
  6. имеет закрытие 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 amp;amp; 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 amp;amp; 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 amp;amp; 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 amp;amp; 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)
            }
        }
    }
}
 

Комментарии:

1. Отлично работает с Xcode 12.1 / iOS 14.1 с простой репликацией и привязкой к свойству состояния строки.

2. @Asperi вы прокомментировали изменение в действии кнопки? Предполагается, что он запускает onChange внутри блока набора привязок. Мне пришлось добавить его в действие кнопки, потому что оно не запускало мою привязку, когда я устанавливал текст на «»

3. вы прокомментировали изменение в действии кнопки? — да, text = "" используется только.

4. @Asperi это действительно странно… у меня это не работает…

Ответ №1:

Изменение text привязки не влияет binding на привязку, которая передается в текстовое поле. Вы должны правильно знать взаимосвязь между двумя привязками. set: блок запускается только тогда, когда binding текстовое поле было изменено.

Если ваша минимальная целевая iOS равна 14, вы также можете использовать onChange модификатор, а не ваш обработчик, для наблюдения за любым изменением text привязки, чтобы вызвать вашу onChange привязку при изменении привязки. И вам не нужно создавать binding привязку для текстового поля. просто передайте text привязку напрямую.

Комментарии:

1. к сожалению, моя минимальная цель — 13. Есть ли другой способ вызвать что-либо при изменении значения @Binding? Кроме установленного блока кода?