Я хочу сделать экран с кодом входа. Он состоит из 4 отдельных UITextField
элементов, каждый из которых принимает один символ. Я реализовал систему, в которой каждый раз, когда одно из изменений UITextField
проверяет, все ли значения заполнены, и обновляют ли они логическую привязку, чтобы сообщить родительскому объекту, что код верен.
Чтобы сделать это, я обертываю переменные @State
в пользовательскую привязку, которая выполняет обратный вызов в установщике, например:
@State private var chars:[String] = ["","","",""]
...
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
, и эти привязки передаются компонентам. Каждый раз, когда мое текстовое значение изменяется validateCode
вызывается. Это прекрасно работает.
Однако теперь я хочу добавить дополнительное поведение: если пользователь вводит 4 символа, а код неправильный, я хочу переместить первого респондента обратно в первое текстовое поле и очистить его содержимое. Первая часть респондента работает нормально (я также управляю этим с помощью переменных @State
, но я не использую для них связывающую обертку), однако я не могу изменить текст внутри своего кода. Я думаю, это потому, что мои компоненты используют эту обернутую привязку, а не переменную, содержащую текст.
Вот как выглядит мой validateCode
:
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.chars = ["","","",""]
}
}
hasFocus
делает свое дело правильно и курсор перемещается на первый UITextField
. Однако текст остается в текстовых полях. Я попытался создать эти привязки в init
, чтобы я мог также использовать их в своей функции validateCode
, но это дает всевозможные ошибки компиляции, потому что я использую self
внутри геттера и сеттера.
Есть идеи, как это решить? Должен ли я работать с Observables? Я только начинаю со SwiftUI, так что, возможно, мне не хватает некоторых инструментов, которые я могу использовать для этого.
Для полноты, вот код всего файла:
import SwiftUI
struct CWCodeView: View {
var value:String
@Binding var isValid:Bool
@State private var chars:[String] = ["","","",""]
@State private var hasFocus = [true,false,false,false]
@State private var nothingHasFocus:Bool = false
init(value:String,isValid:Binding<Bool>) {
self.value = value
self._isValid = isValid
}
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.nothingHasFocus = false
self.chars = ["","","",""]
}
}
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
return GeometryReader { geometry in
ScrollView (.vertical){
VStack{
HStack {
CWNumberField(letter: bindings[0],hasFocus: self.$hasFocus[0], previousHasFocus: self.$nothingHasFocus, nextHasFocus: self.$hasFocus[1])
CWNumberField(letter: bindings[1],hasFocus: self.$hasFocus[1], previousHasFocus: self.$hasFocus[0], nextHasFocus: self.$hasFocus[2])
CWNumberField(letter: bindings[2],hasFocus: self.$hasFocus[2], previousHasFocus: self.$hasFocus[1], nextHasFocus: self.$hasFocus[3])
CWNumberField(letter: bindings[3],hasFocus: self.$hasFocus[3], previousHasFocus: self.$hasFocus[2], nextHasFocus: self.$nothingHasFocus)
}
}
.frame(width: geometry.size.width)
.frame(height: geometry.size.height)
.modifier(AdaptsToSoftwareKeyboard())
}
}
}
}
struct CWCodeView_Previews: PreviewProvider {
static var previews: some View {
CWCodeView(value: "1000", isValid: .constant(false))
}
}
struct CWNumberField : View {
@Binding var letter:String
@Binding var hasFocus:Bool
@Binding var previousHasFocus:Bool
@Binding var nextHasFocus:Bool
var body: some View {
CWSingleCharacterTextField(character:$letter,hasFocus: $hasFocus, previousHasFocus: $previousHasFocus, nextHasFocus: $nextHasFocus)
.frame(width: 46,height:56)
.keyboardType(.numberPad)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.init("codeBorder"), lineWidth: 1)
)
}
}
struct CWSingleCharacterTextField : UIViewRepresentable {
@Binding var character: String
@Binding var hasFocus:Bool
@Binding var previousHasFocus:Bool
@Binding var nextHasFocus:Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField.init()
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}
Спасибо!