Как использовать ObservableObject с UIViewRepresentable - PullRequest
1 голос
/ 25 сентября 2019

Я создаю SwiftUI приложение с использованием MVVM.

Мне нужны некоторые дополнительные поведения для текстового поля, поэтому я обертываю UITextField в UIViewRepresentable представлении.

Если я использую простое @State в представлении, которое содержит мои текстовые поля, для привязки текста, настраиваемое текстовое поле будет работать так, как ожидается;но так как я хочу сохранить все тексты моих текстовых полей в модели представления, я использую @ObservedObject;при использовании этого привязка текстового поля не работает: похоже, он всегда сбрасывается в исходное состояние (пустой текст) и не публикует никаких значений (и представление не обновляется). Это странное поведение происходит только для UIViewRepresentable представлений.

Мой основной вид содержит форму и выглядит следующим образом:

struct LoginSceneView: View {

    @ObservedObject private var viewModel: LoginViewModel = LoginViewModel()

    var body: some View {
        ScrollView(showsIndicators: false) {
            VStack(spacing: 22) {
                UIKitTextField(text: $viewModel.email, isFirstResponder: $viewModel.isFirstResponder)
                SecureField("Password", text: $viewModel.password)
                Button(action: {}) {
                    Text("LOGIN")
                }
                .disabled(!viewModel.isButtonEnabled)       
            }
            .padding(.vertical, 40)
        }
    }

}

Модель представления такова:

class LoginViewModel: ObservableObject {

    @Published var email = ""
    @Published var password = ""
    @Published var isFirstResponder = false

    var isButtonEnabled: Bool { !email.isEmpty && !password.isEmpty }

}

И, наконец, это мое текстовое поле:

struct UIKitTextField: UIViewRepresentable {

    // MARK: - Coordinator

    class Coordinator: NSObject, UITextFieldDelegate {

        private let textField: UIKitTextField

        fileprivate init(_ textField: UIKitTextField) {
            self.textField = textField

            super.init()
        }

        @objc fileprivate func editingChanged(_ sender: UITextField) {
            let text = sender.text ?? ""
            textField.text = text
            textField.onEditingChanged(text)
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            self.textField.onEditingBegin()
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            self.textField.onEditingEnd()
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            self.textField.onReturnKeyPressed()
        }

    }

    // MARK: - Properties

    @Binding private var text: String
    private let onEditingChanged: (String) -> Void
    private let onEditingBegin: () -> Void
    private let onEditingEnd: () -> Void
    private let onReturnKeyPressed: () -> Bool

    // MARK: - Initializers

    init(text: Binding<String>,
         onEditingChanged: @escaping (String) -> Void = { _ in },
         onEditingBegin: @escaping  () -> Void = {},
         onEditingEnd: @escaping  () -> Void = {},
         onReturnKeyPressed: @escaping  () -> Bool = { true }) {
        _text = text
        self.onEditingChanged = onEditingChanged
        self.onEditingBegin = onEditingBegin
        self.onEditingEnd = onEditingEnd
        self.onReturnKeyPressed = onReturnKeyPressed
    }

    // MARK: - UIViewRepresentable methods

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
        textField.addTarget(context.coordinator, action: #selector(Coordinator.editingChanged(_:)), for: .editingChanged)
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }

}

Ответы [ 2 ]

0 голосов
/ 27 сентября 2019

Я отредактировал ваш код, посмотрите, если это то, что вы ищете.

class LoginViewModel: ObservableObject {

    @Published var email = ""
    @Published var password = ""

    var isButtonEnabled: Bool { !email.isEmpty && !password.isEmpty }

}

struct LoginSceneView: View {

    @ObservedObject private var viewModel: LoginViewModel = LoginViewModel()

    var body: some View {


        ScrollView(showsIndicators: false) {
            VStack(spacing: 22) {
                UIKitTextField(text: $viewModel.email)
                SecureField("Password", text: $viewModel.password)
                Button(action: {
                    print("email is \(self.viewModel.email)")
                    print("password is \(self.viewModel.password)")
                    UIApplication.shared.endEditing()
                }) {
                    Text("LOGIN")
                }
                .disabled(!viewModel.isButtonEnabled)
            }
            .padding(.vertical, 40)
        }
    }

}

struct UIKitTextField: UIViewRepresentable {

    // MARK: - Coordinator

    class Coordinator: NSObject, UITextFieldDelegate {

        let textField: UIKitTextField

        fileprivate init(_ textField: UIKitTextField) {
            self.textField = textField

            super.init()
        }

        @objc fileprivate func editingChanged(_ sender: UITextField) {
            let text = sender.text ?? ""
            textField.text = text
            textField.onEditingChanged(text)
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            self.textField.onEditingBegin()
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            self.textField.onEditingEnd()
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            self.textField.onReturnKeyPressed()
        }

    }

    // MARK: - Properties

    @Binding private var text: String
    private let onEditingChanged: (String) -> Void
    private let onEditingBegin: () -> Void
    private let onEditingEnd: () -> Void
    private let onReturnKeyPressed: () -> Bool


    // MARK: - Initializers

    init(text: Binding<String>,
         onEditingChanged: @escaping (String) -> Void = { _ in },
         onEditingBegin: @escaping  () -> Void = {},
         onEditingEnd: @escaping  () -> Void = {},
         onReturnKeyPressed: @escaping  () -> Bool = { true }) {
        _text = text
        self.onEditingChanged = onEditingChanged
        self.onEditingBegin = onEditingBegin
        self.onEditingEnd = onEditingEnd
        self.onReturnKeyPressed = onReturnKeyPressed

    }

    // MARK: - UIViewRepresentable methods

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
        textField.addTarget(context.coordinator, action: #selector(Coordinator.editingChanged(_:)), for: .editingChanged)
        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }

}

Я настоятельно рекомендую не делать ваш UIViewRepresentable сложным для таких функций, как использование isFirstResponder, чтобы делать то, что возможно с альтернативойпути, если это необходимо.Я предполагаю, что вы хотите использовать это как параметр, чтобы отклонить клавиатуру.Есть несколько альтернатив, таких как:

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}
0 голосов
/ 26 сентября 2019

Ваша проблема может заключаться в том, что UIViewRepresentable создается каждый раз, когда изменяется какая-либо обязательная переменная.Поставьте код отладки для проверки.

struct UIKitTextField: UIViewRepresentable {
    init() {
         print("UIViewRepresentable init()")
    }
    ...
}
...