#swift #swiftui
#swift #swiftui
Вопрос:
Я создал пользовательское текстовое поле со следующими дополнительными функциями:
- может быть безопасным / небезопасным
- имеет кнопку показать / скрыть пароль (если защищен)
- имеет кнопку с открытым текстом
- имеет текстовую метку ошибки внизу.
- заполнитель анимируется над текстовым полем при вводе текста (аналогично текстовым полям Android material)
- имеет закрытие 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? Кроме установленного блока кода?