Почему мои представления не отображаются в SwiftUI, даже если издатель выдает значение

#swiftui #combine #publisher

Вопрос:

У меня есть текстовое поле имени пользователя в SwiftUI. Я пытаюсь проверить ввод с помощью издателей.

Вот мой код:

 View

struct UserView: View {
    @StateObject private var userViewModel = UserViewModel()

    init(){
        UITextField.appearance().semanticContentAttribute = .forceRightToLeft
        UITextField.appearance().keyboardAppearance = .dark
    
    }

    var body: some View {
        SecureField("", text: $userViewModel.passwordText)
        Text(userViewModel.passwordError).foregroundColor(.red)
            .frame(width: 264, alignment: .trailing)
    }
}
 

Модель Представления

 ViewModel

final class UserViewModel: ObservableObject {

    private var cancellables = Set<AnyCancellable>()
    @Published var userText: String = ""
    @Published var userTextError = ""

    private var usernamevalidation: AnyPublisher<(username:String, isValid: Bool), Never> {
        return $userText
            .dropFirst()
            .map{(username:$0, isValid: !$0.isEmpty)}
            .eraseToAnyPublisher()
    }

    private var usernamevalidated: AnyPublisher<Bool,Never> {
        return usernamevalidation
            .filter{$0.isValid}
            .map{$0.username.isValidUserName()}
            .eraseToAnyPublisher()
    }


    init(){
        usernamevalidation.receive(on: RunLoop.main)
            .map{$0.isValid ? "": "Emptyusername "}
            .assign(to: .userTextError, on: self)
            .store(in: amp;cancellables)
        usernamevalidated.receive(on: RunLoop.main)
            .map{$0 ? "" : "wrong username "}
            .assign(to: .userTextError, on: self)
            .store(in: amp;cancellables)
    }
}
 

Расширение

  extension String {

     func isValidUserName() -> Bool {
         let usernameRegex = "^[a-zA-Z0-9_-]*$"
         let usernamepred = NSPredicate(format:"SELF MATCHES %@", usernameRegex)
         return usernamepred.evaluate(with: self)
   }
}
 

В usernamevalidated в блоке init() в ViewModel я присваиваю ошибку свойству userTextError, которое должно быть отражено в textview. Это должно произойти, если введен специальный символ, такой как @ или % .. и т. Д. Что происходит, так это то, что иногда ошибка отображается красным цветом, а другие нет, даже если я пытаюсь напечатать значение строки после оператора карты, я вижу строку при печати нормально. Просто ошибка иногда отражается в представлении, а иногда нет. Я что-то упускаю или делаю что-то в корне неправильное

Ответ №1:

Проблема в том, что оба usernamevalidation и usernamevalidated являются вычисляемыми свойствами. Сохранение их решит проблему, но вы также можете упростить модель представления , наблюдая за изменениями userText , проверяя их и назначая userTextError таким образом:

 final class UserViewModel: ObservableObject {
    
    @Published var userText: String = ""
    @Published private(set) var userTextError = ""
    
    init() {
        $userText
            .dropFirst()
            .map { username in
                guard !username.isEmpty else {
                    return "Username is empty"
                }
                guard username.isValidUserName() else {
                    return "Username is invalid"
                }
                return ""
            }
            .receive(on: RunLoop.main)
            .assign(to: amp;$userTextError)
    } 
}
 

Также стоит упомянуть, что замена .assign(to: .userTextError, on: self) на .assign(to: amp;$userTextError) избавляет от утечки памяти и означает, что вам больше не нужно ее хранить cancellables .

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

1. это прекрасно работает, но почему при закрытии издателей это не работает

2. Издатель UserText не может быть частным, поскольку представление должно как читать, так и записывать в него. UserTextError может быть закрытым(заданным), потому что представление должно иметь возможность его читать, но не должно записывать в него.

Ответ №2:

Вместо вычислимых используйте сохраненные свойства для ваших издателей (чтобы они оставались в живых), например

 private lazy var usernamevalidation: AnyPublisher<(username:String, isValid: Bool), Never> = {
    return $userText
        .dropFirst()
        .map{(username:$0, isValid: !$0.isEmpty)}
        .eraseToAnyPublisher()
}()
 

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

1. все еще не работает, особенно пользователь, проверенный издателем, иногда он показывает ошибку, иногда нет

2. Я обнаружил свою проблему после того, как первым делом в каждом издателе мне нужно было получить их в главном потоке. Добавление следующего .receive(on: DispatchQueue.main) решило мою проблему

Ответ №3:

Предыдущий ответ (см. изменения, если хотите) был неверным, потому что тогда Emptyusername ошибка была бы перезаписана, а это не то, что нам нужно (моя ошибка).

Оказывается, проблема в обновлении пользовательского интерфейса iOS 14! Исправление, которое мне приходилось использовать раньше TextField , выглядит немного необычно, но выполняет свою работу.

Просто добавьте это в тело представления:

 var body: some View {
    let _ = userViewModel.userTextError

    /* Rest of view as before */
}
 

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

1. Проблема все еще сохраняется, хотя то, что вы говорите, имеет смысл. Проблема в том, что иногда отображается ошибка, а иногда нет. Я не знаю, является ли это ошибкой в xcode или чем-то еще, но это больно

2. @Ахмед После этого изменения это сработало для меня. Протестировано с помощью Xcode 13b2 ‘бета-версия 13.0 (13A5155e)’

3. позвольте мне проверить мою версию xcode. Можете ли вы попробовать несколько раз, потому что иногда это работает, а иногда нет. например, попробуйте 6-10 раз, и вы можете подтвердить это со мной

4. @Ахмед Ага! Шахта работает на симуляторе и реальном устройстве на iOS 15, но не на симуляторе на iOS 14.

5. @Ахмед, я буду честен, я не уверен, почему это по-другому, но я оставлю вас для расследования. Похоже, это проблема с версией iOS, это все, что я пока знаю