首页 > 解决方案 > SwiftUI 中的 UITextField 导致“在视图更新期间修改状态,这将导致未定义的行为。”

问题描述

我正在尝试在 SwiftUI 中创建一个系统,让我可以控制哪个文本字段是第一响应者,这样我就可以轻松地创建一个响应者链。

我创建了一个UIViewRepresentable获取Binding布尔类型的环境值的 a,并且我正在根据这些函数textFieldDidBeginEditingtextFieldDidEndEditing.

struct TextField: UIViewRepresentable {
 
    @Binding var text: String
    @Environment(\.isSecureField) var isSecureField: Bool
    @Environment(\.isFirstResponder) var isFirstResponder: Binding<Bool>?
    @Environment(\.placeholder) var placeholder: String?
    @Environment(\.keyboardType) var keyboardType: UIKeyboardType
    @Environment(\.returnKeyType) var returnKeyType: UIReturnKeyType
    @Environment(\.textContentType) var contentType: UITextContentType?
    @Environment(\.autoCapitalization) var  autoCapitalization: Bool
    @Environment(\.autoCorrection) var  autoCorrection: Bool
    @Environment(\.font) var font: Font?
    
    let onCommit: (() -> Bool)?
    
    init(
        text: Binding<String>,
        onCommit:(() -> Bool)? = nil) {

        self._text = text
        self.onCommit = onCommit
    
    }
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        syncValues(context: context, textField: textField)
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        syncValues(context: context, textField: uiView)
 
        if let isFirstReposnderValue = isFirstResponder?.wrappedValue {
            switch isFirstReposnderValue {
            case true:
                uiView.becomeFirstResponder()
            case false:
                uiView.resignFirstResponder()
            }
        }
    }
    
    private func syncValues(context: Context, textField: UITextField) {
        textField.text = text
        textField.isSecureTextEntry = isSecureField
        textField.textContentType = contentType
        textField.keyboardType = keyboardType
        textField.placeholder = placeholder
        textField.font = UIFont.preferredFont(from: font)
        textField.autocapitalizationType = autoCapitalization ? .sentences : .none
        textField.returnKeyType = returnKeyType
        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textField.autocorrectionType = autoCorrection ? .yes : .no
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator( parent: self  )
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        
        private let parent: TextField
        
        init(parent: TextField) {
            self.parent = parent
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            return parent.onCommit?() ?? true
        }
        
        func textFieldDidChangeSelection(_ textField: UITextField) {
            parent.$text.wrappedValue = textField.text ?? ""
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            parent.isFirstResponder?.wrappedValue =  true

        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            parent.isFirstResponder?.wrappedValue =  false
        }

    }
}

此外,我为具有布尔类型的 Binding 创建了一个新的构造函数,因此我可以使用 Hashable(如枚举),因此我可以有两个以上的状态,并且没有真假。

extension Binding where Value == Bool {
    
    init<V: Hashable>(tag: V, selection: Binding<V?>) {
        self.init { () -> Value in
            selection.wrappedValue == tag
        } set: { (newValue) in
            selection.wrappedValue = newValue ?  tag : nil
        }
    }
}

这是我如何使用它的示例:

class TestLoginViewModel:ObservableObject {
    
    @Published var email: String = ""
    @Published var password: String = ""
}

struct TestLoginView: View {
    
    enum Field {
        case email
        case password
    }
    
    @StateObject private var viewModel: TestLoginViewModel
    @State private var activeField: Field?
    
    init(viewModel: TestLoginViewModel = .init()) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }
  
    
    var body: some View {
        VStack(spacing: 8) {
            
            TextField(text: $viewModel.email, onCommit: { () -> Bool in
                activeField = .password
                return true
            })
            .placeholder(L10n.email)
            .isFirstReponder(.init(tag: .email, selection: $activeField))
            .returnKeyType(.next)
            .keyboard(.emailAddress)
            
            TextField(text: $viewModel.password, onCommit: { () -> Bool in
                activeField = nil
                return true
            })
            .placeholder(L10n.password)
            .isSecureField(true)
            .isFirstReponder(.init(tag: .password, selection: $activeField))
            .returnKeyType(.send)
            .keyboard(.emailAddress)
        }
    }
}

但我得到:

在视图更新期间修改状态,这将导致未定义的行为。

在此处输入图像描述

标签: iosswiftswiftuiuitextfield

解决方案


推荐阅读