Validasi data dalam aplikasi iOS

Saya pikir kita masing-masing telah menghadapi tugas memvalidasi data dalam aplikasi. Misalnya, saat mendaftarkan pengguna, Anda perlu memastikan bahwa email memiliki format yang benar, dan kata sandi memenuhi persyaratan keamanan, dan sebagainya. Anda dapat memberikan banyak contoh, tetapi semuanya bermuara pada satu tugas - validasi data sebelum mengirimkan formulir.


Saat mengerjakan proyek berikutnya, saya berpikir untuk membuat solusi universal, daripada menulis metode validasi untuk setiap layar dengan formulir secara terpisah. Swift 5.1 memperkenalkan anotasi @propertyWrapper , dan saya pikir akan lebih mudah memiliki sintaks seperti berikut:


 @Validated([validator1, validator2, ...]) var email: String? = nil let errors = $email.errors //   

Validator


Langkah pertama adalah menentukan bagaimana validator akan terlihat dan bekerja. Sebagai bagian dari proyek saya, saya perlu memeriksa validitas data dan menampilkan pesan kesalahan yang sesuai. Tidak perlu penanganan kesalahan yang lebih kompleks, sehingga implementasi validator adalah sebagai berikut:


 struct ValidationError: LocalizedError { var message: String public var errorDescription: String? { message } } protocol Validator { associatedtype ValueType var errorMessage: String { get } func isValid(value: ValueType?) -> Bool } extension Validator { func validate(value: ValueType?) throws { if !isValid(value: value) { throw ValidationError(message: errorMessage) } } } 

Semuanya sederhana di sini. Validator berisi pesan kesalahan yang melempar ValidationError jika validasi gagal. Ini menyederhanakan penanganan kesalahan, karena semua validator mengembalikan jenis kesalahan yang sama, tetapi dengan pesan yang berbeda. Contohnya adalah kode validator yang memeriksa string terhadap ekspresi reguler:


 struct RegexValidator: Validator { public var errorMessage: String private var regex: String public init(regex: String, errorMessage: String) { self.regex = regex self.errorMessage = errorMessage } public func isValid(value: String?) -> Bool { guard let v = value else { return false } let predicate = NSPredicate(format: "SELF MATCHES %@", regex) return predicate.evaluate(with: v) } } 

Implementasi ini mengandung satu masalah yang diketahui banyak masalah. Karena protokol Validator berisi tipe associatedtype , kami tidak dapat membuat variabel tipe


 var validators:[Validator] //Protocol 'Validator' can only be used as a generic constraint because it has Self or associated type requirements 

Untuk mengatasi masalah ini, kami menggunakan pendekatan standar, yaitu, penciptaan struktur AnyValidator.

 private class ValidatorBox<T>: Validator { var errorMessage: String { fatalError() } func isValid(value: T?) -> Bool { fatalError() } } private class ValidatorBoxHelper<T, V:Validator>: ValidatorBox<T> where V.ValueType == T { private let validator: V init(validator: V) { self.validator = validator } override var errorMessage: String { validator.errorMessage } override func isValid(value: T?) -> Bool { validator.isValid(value: value) } } struct AnyValidator<T>: Validator { private let validator: ValidatorBox<T> public init<V: Validator>(validator: V) where V.ValueType == T { self.validator = ValidatorBoxHelper(validator: validator) } public var errorMessage: String { validator.errorMessage } public func isValid(value: T?) -> Bool { validator.isValid(value: value) } } 

Saya pikir tidak ada yang perlu dikomentari. Ini adalah pendekatan standar untuk menyelesaikan masalah yang dijelaskan di atas. Juga bermanfaat untuk menambahkan ekstensi untuk protokol Validator, memungkinkan Anda untuk membuat objek AnyValidator.

 extension Validator { var validator: AnyValidator<ValueType> { AnyValidator(validator: self) } } 

Pembungkus properti


Dengan validator yang disortir, Anda dapat langsung menuju implementasi pembungkus @Validated .


 @propertyWrapper class Validated<Value> { private var validators: [AnyValidator<Value>] var wrappedValue: Value? init(wrappedValue value: Value?, _ validators: [AnyValidator<Value>]) { wrappedValue = value self.validators = validators } var projectedValue: Validated<Value> { self } public var errors: [ValidationError] { var errors: [ValidationError] = [] validators.forEach { do { try $0.validate(value: wrappedValue) } catch { errors.append(error as! ValidationError) } } return errors } } 

Tujuan artikel ini bukan untuk menganalisis cara kerja pembungkus propertyWrapper dan sintaks apa yang mereka gunakan. Jika Anda belum dapat bertemu dengan mereka, saya menyarankan Anda untuk membaca artikel saya yang lain, Cara Pendekatan Wrappers for Swift Properties (Bahasa Inggris).


Implementasi ini memungkinkan kami untuk mendeklarasikan properti yang memerlukan validasi sebagai berikut:


 @Validated([ NotEmptyValidator(errorMessage: "Email can't be empty").validator, RegexValidator(regex:"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64} ", errorMessage:"Email has wrong format").validator ]) var email: String? = nil 

Dan dapatkan berbagai kesalahan validasi pada waktu tertentu sebagai berikut


 let errors = $email.errors 

gambar
Ada kemungkinan beberapa kombinasi validator (misalnya, validasi email) akan muncul di aplikasi pada beberapa layar. Untuk menghindari menyalin kode, dalam kasus seperti itu dimungkinkan untuk membuat pembungkus terpisah yang diwarisi dari Validated .


 @propertyWrapper final class Email: Validated<String> { override var wrappedValue: String? { get { super.wrappedValue } set { super.wrappedValue = newValue } } override var projectedValue: Validated<String> { super.projectedValue } init(wrappedValue value: String?) { let notEmptyValidator = NotEmptyValidator(errorMessage: "Email can't be empty") let regexValidator = RegexValidator(regex:"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64} ", errorMessage:"Email has wrong format").validator super.init(wrappedValue: value, [notEmptyValidator, regexValidator]) } } @Email var email: String? = nil 

Sayangnya, saat ini, anotasi @propertyWrapper membutuhkan wrappedValue dan nilai yang projectedValue , jika tidak kita akan mendapatkan kesalahan kompilasi. Sepertinya bug implementasi, jadi ada kemungkinan bahwa di versi Swift yang akan datang ini akan diperbaiki.


Tambahkan Reaktif


Bersama dengan iOS13, kerangka kerja asli Combine reactive muncul. Dan saya pikir mungkin ada gunanya memiliki sintaks berikut:


 let cancellable = $email .publisher .map { $0.map { $0.localizedDescription }.joined(separator: ", ") } .receive(on: RunLoop.main) .assign(to: \.text, on: emailErrorLabel) 

Ini akan memungkinkan pembaruan informasi tentang kesalahan validasi secara real time (setelah setiap karakter dimasukkan). Implementasi awal dari ide ini adalah sebagai berikut:


 @propertyWrapper class Validated<Value> { private var _subject: Any! @available(iOS 13.0, *) private var subject: PassthroughSubject<[ValidationError], Never> { return _subject as! PassthroughSubject<[ValidationError], Never> } open var wrappedValue: Value? { didSet { if #available(iOS 13.0, *) { subject.send(errors) } } } public init(wrappedValue value: Value?, _ validators: [AnyValidator<Value>]) { wrappedValue = value self.validators = validators if #available(iOS 13.0, *) { _subject = PassthroughSubject<[ValidationError], Never>() } } @available(iOS 13.0, *) public var publisher: AnyPublisher<[ValidationError], Never> { subject.eraseToAnyPublisher() } // The rest of the code } 

Karena fakta bahwa properti yang disimpan tidak dapat ditandai dengan penjelasan @available , saya harus menggunakan pekerjaan sekitar dengan properti _subject dan subject . Kalau tidak, semuanya harus sangat jelas. PassthroughObject dibuat yang mengirim pesan setiap kali perubahan Nilai yang wrappedValue .


Akibatnya, pesan kesalahan validasi berubah saat pengguna mengisi formulir.
gambar
Dalam proses pengujian solusi ini, satu bug terungkap. Validasi terjadi setiap kali properti berubah, terlepas dari keberadaan pelanggan untuk acara ini. Di satu sisi, ini tidak mempengaruhi hasil, tetapi di sisi lain, dalam kasus ketika kita tidak perlu validasi secara real time, tindakan yang tidak perlu akan dilakukan. Ini akan memvalidasi dengan benar dan mengirim pesan hanya jika ada setidaknya satu pelanggan. Akibatnya, kode tersebut diperbaiki dengan persyaratan ini.


 @propertyWrapper class Validated<Value> { private var _subject: Any! @available(iOS 13.0, *) private var subject: Publishers.HandleEvents<PassthroughSubject<[ValidationError], Never>> { return _subject as! Publishers.HandleEvents<PassthroughSubject<[ValidationError], Never>> } private var subscribed: Bool = false open var wrappedValue: Value? { didSet { if #available(iOS 13.0, *) { if subscribed { subject.upstream.send(errors) } } } } public init(wrappedValue value: Value?, _ validators: [AnyValidator<Value>]) { wrappedValue = value self.validators = validators if #available(iOS 13.0, *) { _subject = PassthroughSubject<[ValidationError], Never>() .handleEvents(receiveSubscription: {[weak self] _ in self?.subscribed = true }) } } // The rest of the code } 

Akibatnya, saya mendapat solusi yang relatif universal untuk validasi data dalam aplikasi. Ini mungkin tidak memecahkan beberapa masalah, misalnya, penanganan kesalahan yang lebih rumit daripada output pesan sederhana, tetapi cocok untuk validasi sederhana input pengguna. Anda dapat memeriksa solusi lengkapnya di GitHub .

Source: https://habr.com/ru/post/id485092/


All Articles