#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, это все, что я пока знаю