Dari kesalahan hingga peringatan dengan tindakan

Halo, Habr! Untuk pengguna, pesan kesalahan sering terlihat seperti "Ada yang salah, AAAA!". Tentu saja, dia ingin bukannya kesalahan untuk melihat kesalahan ajaib "Perbaiki semuanya". Nah, atau opsi lainnya. Kami mulai secara aktif menambahkan ini ke diri kami sendiri, dan saya ingin berbicara tentang bagaimana Anda bisa melakukan ini.



Pertama, perkenalkan diri saya - nama saya Alexander, enam tahun terakhir saya telah mencurahkan pengembangan iOS. Sekarang saya bertanggung jawab untuk aplikasi seluler ManyChat dan saya akan memecahkan masalah menggunakan contohnya.

Mari segera merumuskan apa yang akan kita lakukan:

  • Tambahkan Fungsi ke Jenis Kesalahan
  • Ubah kesalahan menjadi peringatan ramah pengguna
  • Kami menampilkan tindakan lebih lanjut yang mungkin dalam antarmuka dan memproses klik mereka

Dan semua ini ada di Swift :)

Kami akan memecahkan masalah dengan sebuah contoh. Server mengembalikan kesalahan dengan kode 500 bukannya 200 yang diharapkan. Apa yang harus dilakukan pengembang? Paling tidak, dengan sedih memberi tahu pengguna - pos yang diharapkan dengan segel tidak dapat diunduh. Di Apple, pola standar adalah peringatan, jadi mari kita menulis fungsi sederhana:

final class FeedViewController: UIViewController {
  // -   

	func handleFeedResponse(...) {
	/// -    
	if let error = error {
		let alertVC = UIAlertController(
			title: "Error",
			message: "Error connecting to the server",
			preferredStyle: .alert)
		let action = UIAlertAction(title: "OK", style: .default, handler: nil)
		alertVC.addAction(action)
		self.present(alertVC, animated: true, completion: nil)
	}
}

PS Untuk kesederhanaan, sebagian besar kode akan berada di controller. Anda bebas menggunakan pendekatan yang sama dalam arsitektur Anda. Kode artikel akan tersedia di repositori , di akhir artikel tautan ini juga akan.

Kami mendapatkan gambar berikut:



Secara teoritis, kami menyelesaikan tugas. Tetapi beberapa hal segera terbukti:

  • Kami tidak memberikan kesempatan untuk beralih dari skenario yang salah ke skenario yang sukses. OK dalam kasus saat ini, itu hanya menyembunyikan peringatan - dan ini bukan solusi
  • Dari sudut pandang pengalaman pengguna, teks perlu dibuat lebih jelas, netral. Agar pengguna tidak takut dan tidak lari untuk menempatkan satu bintang di AppStore ke aplikasi Anda. Dalam hal ini, teks terperinci akan bermanfaat bagi kami saat melakukan debugging
  • Dan, sejujurnya - peringatan agak ketinggalan jaman sebagai solusi (semakin, layar tiruan atau bersulang muncul di aplikasi). Tetapi ini sudah merupakan pertanyaan yang harus dibahas secara terpisah dengan tim

Setuju, opsi yang disajikan di bawah ini terlihat jauh lebih simpatik.



Opsi mana pun yang Anda pilih, untuk siapa pun di antara Anda yang perlu memikirkan mekanisme untuk menampilkan pesan yang akan tampak hebat ketika kesalahan sewenang-wenang terjadi, akan menawarkan pengguna skrip yang jelas untuk pekerjaan lebih lanjut dalam aplikasi dan memberikan serangkaian tindakan. Solusinya adalah:

  • Harus bisa diperluas. Kita semua tahu tentang variabilitas desain yang melekat. Mekanisme kami harus siap untuk apa pun
  • Itu ditambahkan ke objek (dan dihapus) dalam beberapa baris kode
  • Teruji dengan baik

Tetapi sebelum itu, mari kita terjun ke minimum teoritis untuk kesalahan di Swift.

Kesalahan dalam Swift


Paragraf ini adalah gambaran umum tingkat atas dari kesalahan secara umum. Jika Anda sudah aktif menggunakan kesalahan dalam aplikasi, Anda dapat melanjutkan ke paragraf berikutnya dengan aman.

Apa itu kesalahan? Semacam tindakan salah atau hasil salah. Seringkali kita dapat mengasumsikan kesalahan yang mungkin terjadi dan menjelaskannya terlebih dahulu dalam kode.

Untuk kasus ini, Apple memberi kita tipe Kesalahan. Jika kita membuka dokumentasi Apple, maka Error akan terlihat seperti ini (relevan untuk Swift 5.1):

public protocol Error {
}

Hanya protokol tanpa persyaratan tambahan. Dokumentasi menjelaskan dengan baik - kurangnya parameter yang diperlukan memungkinkan jenis apa pun untuk digunakan dalam sistem penanganan kesalahan Swift. Dengan protokol yang begitu lembut, kami hanya akan bekerja.

Gagasan untuk menggunakan enum langsung muncul di benak saya: ada sejumlah kesalahan yang diketahui, mereka mungkin memiliki beberapa jenis parameter. Itulah yang dilakukan Apple. Misalnya, Anda dapat mempertimbangkan menerapkan DecodingError:

public enum DecodingError : Error {
    
        /// ,     . 
    	///  ,    
        public struct Context {
    
    	    ///      
            public let codingPath: [CodingKey]
            public let debugDescription: String
    
    	    /// ,    . 
            ///      .   
            public let underlyingError: Error?
    
            public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = nil)
        }
    
    	/// N    
        case typeMismatch(Any.Type, DecodingError.Context)
        case valueNotFound(Any.Type, DecodingError.Context)
    
    ...

Manfaatkan praktik terbaik Apple. Bayangkan sekelompok kemungkinan kesalahan jaringan dalam bentuk yang disederhanakan:

enum NetworkError: Error {
       //  500 
	case serverError
        //   ,   
	case responseError
        //  ,   ,  
	case internetError
}

Sekarang, di mana saja di aplikasi kita di mana kesalahan terjadi, kita dapat menggunakan Network.Error kami.

Bagaimana cara mengatasi bug? Ada mekanisme do catch. Jika suatu fungsi dapat melempar kesalahan, maka itu ditandai dengan kata kunci lemparan. Sekarang setiap penggunanya diharuskan untuk mengaksesnya melalui konstruk do catch. Jika tidak ada kesalahan, kita akan jatuh ke blok do, dengan kesalahan, ke blok catch. Fungsi yang mengarah ke kesalahan bisa berupa angka apa saja di blok do. Satu-satunya negatif adalah bahwa dalam tangkapan kita mendapatkan kesalahan ketik Kesalahan. Anda harus memasukkan kesalahan ke dalam tipe yang diinginkan.

Sebagai alternatif, kita dapat menggunakan opsional, yaitu, mendapatkan nihil jika terjadi kesalahan dan menyingkirkan desain besar. Kadang-kadang lebih nyaman: katakanlah ketika kita mendapatkan variabel opsional, dan kemudian menerapkan fungsi melempar ke sana. Kode dapat dimasukkan ke dalam blok if / guard, dan akan tetap singkat.

Berikut adalah contoh bekerja dengan fungsi lemparan:

func blah() -> String throws {
    	throw NetworkError.serverError
    }
    
    do {
    	let string = try blah()
    	//     ,      
    	let anotherString = try blah()
    } catch {
    	//  NetworkError.serverError
    	print(error)
    }
    
    //  string = nil
    let string = try? blah()

PS Jangan bingung dengan do catch dalam bahasa lain. Swift tidak melempar pengecualian, tetapi menulis nilai kesalahan (jika itu terjadi) dalam register khusus. Jika ada nilai, ia pergi ke blok kesalahan, jika tidak, blok do berlanjut. Sumber untuk yang paling ingin tahu: www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html

Metode ini baik untuk menangani acara sinkron dan tidak nyaman untuk operasi yang panjang (misalnya, meminta data melalui jaringan), yang berpotensi memakan waktu. Kemudian Anda bisa menggunakan penyelesaian sederhana.

Sebagai alternatif dari Swift 5, Result diperkenalkan - enum siap yang berisi dua opsi - sukses dan gagal. Dengan sendirinya, itu tidak memerlukan penggunaan Kesalahan. Dan itu tidak memiliki hubungan langsung dengan asynchrony. Tetapi mengembalikan tipe ini secara tepat ke penyelesaian lebih nyaman untuk peristiwa asinkron (jika tidak, Anda harus melakukan dua penyelesaian, sukses dan gagal, atau mengembalikan dua parameter). Mari kita tulis sebuah contoh:

func blah<ResultType>(handler: @escaping (Swift.Result<ResultType, Error>) -> Void) {
	handler(.failure(NetworkError.serverError)
}

blah<String>(handler { result in 
	switch result {
		case .success(let value):
			print(value)
		case .failure(let error):
			print(error)
	}
})

Informasi ini cukup bagi kami untuk bekerja.

Sekali lagi, secara singkat:

  • Kesalahan dalam Swift adalah protokol
  • Lebih mudah untuk menyajikan kesalahan sebagai enum
  • Ada dua cara untuk mengatasi kesalahan - sinkron (lakukan tangkapan) dan asinkron (persaingan atau Hasil Anda sendiri)

Teks salah


Mari kita kembali ke topik artikel. Dalam paragraf di atas, kami membuat jenis kesalahan kami sendiri. Itu dia:

enum NetworkError: Error {
        //  500 
	case serverError
        //   ,   
	case responseError
        //  ,   ,  
	case internetError
}

Sekarang kita harus mencocokkan setiap kesalahan dengan teks yang dapat dimengerti oleh pengguna. Kami akan menampilkannya di antarmuka jika ada kesalahan. Protokol Lokalisasi bergegas untuk membantu kami. Ini mewarisi kesalahan protokol dan menambahnya dengan 4 properti:

protocol LocalizedError : Error {
    var errorDescription: String? { get }
    var failureReason: String? { get }
    var recoverySuggestion: String? { get }
    var helpAnchor: String? { get }
}

Kami menerapkan protokol:

extension NetworkError: LocalizedError {
    	var errorDescription: String? {
            switch self {
            case .serverError, .responseError:
                return "Error"
    	    case .internetError:
                return "No Internet Connection"
            }
        }
    
        var failureReason: String? {
            switch self {
            case .serverError, .responseError:
                return "Something went wrong"
    	    case .internetError:
                return nil
            }
        }
    
        var recoverySuggestion: String? {
            switch self {
            case .serverError, .responseError:
                return "Please, try again"
    	    case .internetError:
                return "Please check your internet connection and try again"
            }
        }
    }

Tampilan kesalahan tidak akan berubah:

	if let error = error {
		let errorMessage = [error.failureReason, error.recoverySuggestion].compactMap({ $0 }).joined(separator: ". ")
		let alertVC = UIAlertController(
			title: error.errorDescription,
			message: errorMessage,
			preferredStyle: .alert)
		let action = UIAlertAction(title: "OK", style: .default) { (_) -> Void in }
		alertVC.addAction(action)
		self.present(alertVC, animated: true, competion: nil)

Hebat, semuanya mudah dengan teks. Mari beralih ke tombol.

Kesalahan pemulihan


Mari sajikan algoritma penanganan kesalahan dalam diagram sederhana. Untuk situasi di mana, sebagai akibat dari kesalahan, kami menampilkan kotak dialog dengan Coba Lagi, opsi Batalkan dan, mungkin, beberapa yang spesifik, kami mendapatkan skema:



Mari kita mulai memecahkan masalah dari akhir. Kami membutuhkan fungsi yang menunjukkan peringatan dengan opsi n +1. Kami melempar, karena kami ingin menunjukkan kesalahan:

struct RecovableAction {
    	let title: String
    	let action: () -> Void
    }
    
    func showRecovableOptions(actions: [RecovableAction], from viewController: UIViewController) {
    	let alertActions = actions.map { UIAlertAction(name: $0.title, action: $0.action) }
    	let cancelAction = UIAlertAction(name: "Cancel", action: nil)
    	let alertController = UIAlertController(actions: alertActions)
    	viewController.present(alertController, complition: nil)
    }

Fungsi yang menentukan jenis kesalahan dan mentransmisikan sinyal untuk menampilkan peringatan:

func handleError(error: Error) {
	if error is RecovableError {
		showRecovableOptions(actions: error.actions, from: viewController)
		return
	}
	showErrorAlert(...)
} 


Dan jenis kesalahan yang diperluas, yang memiliki konteks dan pemahaman tentang apa yang harus dilakukan dengan opsi ini atau itu.

struct RecovableError: Error {
	let recovableACtions: [RecovableAction]
	let context: Context
}

Kepala segera menggambar diagram sepeda Anda. Tapi pertama-tama, mari kita periksa dermaga Apple. Mungkin sebagian dari mekanisme sudah ada di tangan kita.

Implementasi asli?


Sedikit pencarian di internet akan menghasilkan protokol RecoverableError :

// A specialized error that may be recoverable by presenting several potential recovery options to the user.
protocol RecoverableError : Error {
    var recoveryOptions: [String] { get }

    func attemptRecovery(optionIndex recoveryOptionIndex: Int, resultHandler handler: @escaping (Bool) -> Void)
    func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool
}

Sepertinya kami sedang mencari:

  • recoveryOptions: [String] - properti yang menyimpan opsi pemulihan
  • func effortRecovery (optionIndex: Int) -> Bool - mengembalikan dari kesalahan, secara sinkron. Benar - Sukses
  • func effortRecovery (optionIndex: Int, resultHandler: (Bool) -> Void) - Opsi asinkron, idenya sama

Dengan panduan penggunaan, semuanya lebih sederhana. Sebuah pencarian kecil di situs Apple dan area sekitarnya mengarah ke sebuah artikel tentang penanganan kesalahan yang ditulis sebelum pengumuman publik Swift.

Secara singkat:

  • Mekanisme ini dipikirkan untuk aplikasi MacO dan memperlihatkan kotak dialog
  • Awalnya dibangun di sekitar NSError.
  • Objek RecoveryAttempter diringkas di dalam kesalahan dalam userInfo, yang tahu tentang kondisi kesalahan dan dapat memilih solusi terbaik untuk masalah tersebut. Objek tidak boleh nol
  • RecoveryAttempter harus mendukung protokol informal NSErrorRecoveryAttempting
  • Juga di userInfo harus menjadi opsi pemulihan
  • Dan semuanya terkait dengan memanggil metode presentError, yang hanya ada di macOS SDK. Dia menunjukkan peringatan
  • Jika peringatan ditampilkan melalui presentError, maka ketika Anda memilih opsi di jendela pop-up di AppDelegate, fungsi yang menarik berkedut:

func attemptRecovery(fromError error: Error, optionIndex recoveryOptionIndex: Int, delegate: Any?, didRecoverSelector: Selector?, contextInfo: UnsafeMutableRawPointer?)

Tetapi karena kita tidak memiliki Korupsi saat ini, kita tidak dapat menariknya.



Pada titik ini, rasanya seperti kita menggali mayat daripada harta. Kita harus mengubah Kesalahan menjadi NSError dan menulis fungsi kita sendiri untuk menampilkan peringatan oleh aplikasi. Banyak koneksi implisit. Itu mungkin, sulit dan tidak sepenuhnya jelas - "Mengapa?".

Sementara secangkir teh berikutnya sedang diseduh, orang mungkin bertanya-tanya mengapa fungsi di atas menggunakan delegasi sebagai Any dan melewati pemilih. Jawabannya di bawah:

Menjawab
iOS 2. ! ( , ). :)

Membangun sepeda


Mari kita implementasikan protokolnya, itu tidak akan merugikan kita:

struct RecoverableError: Foundation.RecoverableError {
	let error: Error
	var recoveryOptions: [String] {
		return ["Try again"]s
	}
	
	func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool {
		//  ,    
		return true
	}
	
	func attemptRecovery(optionIndex: Int, resultHandler: (Bool) -> Void) {
		//      . 
               //      
		switch optionIndex {
			case 0:
				resultHandler(true)
			default: 
				resultHandler(false)
	}
}

Ketergantungan indeks bukan solusi yang paling nyaman (kita dapat dengan mudah melampaui array dan merusak aplikasi). Tetapi untuk MVP akan dilakukan. Ambil ide Apple, baru saja memodernkannya. Kami membutuhkan objek Attempter dan opsi tombol terpisah yang akan kami berikan:

struct RecoveryAttemper {
    	//   
    	private let _recoveryOptions: [RecoveryOptions]
    
    	var recoveryOptionsText: [String] {
    		return _recoveryOptions.map({ $0.title })
    	}
    
    	init(options: [RecoveryOptions] {
    		_recoveryOptions = recoveryOptions
    	}
    
    	//    
    	func attemptRecovery(fromError error: Error, optionIndex: Int) -> Bool {
    		let option = _recoveryOptions[optionIndex]
    				switch option {
    				case .tryAgain(let action)
    					action()
    					return true
    				case .cancel:
    					return false
    				}
    		}
    }
    
    //  enum,       
    enum RecoveryOptions {
    	//      (,     )
    	case tryAgain(action: (() -> Void))
    	case cancel
    }

Sekarang Anda harus menunjukkan kesalahan. Saya sangat suka protokol, jadi saya akan memecahkan masalah melalui mereka. Mari kita membuat protokol universal untuk membuat UIAlertController dari kesalahan:

protocol ErrorAlertCreatable: class, ErrorReasonExtractable {
    	//     
    	func createAlert(for error: Error) -> UIAlertController
    }
    
    // MARK: - Default implementation
    extension ErrorAlertCreatable where Self: UIViewController {
    	func createAlert(for error: Error) -> UIAlertController {
    		//      
    		if let recoverableError = error as? RecoverableError {
    			return createRecoverableAlert(for: recoverableError)
    		}
    		let defaultTitle = "Error"
    		let description = errorReason(from: error)
    
    		//          
    		if let localizedError = error as? LocalizedError {
    			return createAlert(
    				title: localizedError.errorDescription ?? defaultTitle,
    				message: description,
    				actions: [.okAction],
    				aboveAll: aboveAll)
    		}
    
    		return createAlert(title: defaultTitle, message: description, actions: [.okAction])
    	}
    
    	fileprivate func createAlert(title: String?, message: String?, actions: [UIAlertAction]) -> UIAlertController {
    		let alertViewController = UIAlertController(title: title, message: message, preferredStyle: .alert)
    		actions.forEach({ alertViewController.addAction($0) })
    		return alertViewController
    	}
    
    	fileprivate func createRecoverableAlert(for recoverableError: RecoverableError) -> UIAlertController {
    		let title = recoverableError.errorDescription
    		let message = recoverableError.recoverySuggestion
    		//     . 
    		let actions = recoverableError.recoveryOptions.enumerated().map { (element) -> UIAlertAction in
    		let style: UIAlertAction.Style = element.offset == 0 ? .cancel : .default
    		return UIAlertAction(title: element.element, style: style) { _ in
    			recoverableError.attemptRecovery(optionIndex: element.offset)
    		      }
    		}
    		return createAlert(title: title, message: message, actions: actions)
    	}
    
    	func createOKAlert(with text: String) -> UIAlertController {
    		return createAlert(title: text, message: nil, actions: [.okAction])
    	}
    }
    
    extension ERror
    
    //     ok
    extension UIAlertAction {
    	static let okAction = UIAlertAction(title: "OK", style: .cancel) { (_) -> Void in }
    }
    
    //    
    protocol ErrorReasonExtractable {
    	func errorReason(from error: Error) -> String?
    }
    
    // MARK: - Default implementation
    extension ErrorReasonExtractable {
    	func errorReason(from error: Error) -> String? {
    		if let localizedError = error as? LocalizedError {
    			return localizedError.recoverySuggestion
    		}
    		return "Something bad happened. Please try again"
    	}
    }

Dan protokol untuk menampilkan peringatan yang dibuat:

protocol ErrorAlertPresentable: class {
	func presentAlert(from error: Error)
}

// MARK: - Default implementation
extension ErrorAlertPresentable where Self: ErrorAlertCreatable & UIViewController {
	func presentAlert(from error: Error) {
		let alertVC = createAlert(for: error)
		present(alertVC, animated: true, completion: nil)
	}
}

Ternyata menjadi rumit, tetapi dapat dikelola. Kami dapat membuat cara baru untuk menampilkan kesalahan (misalnya, bersulang atau menampilkan tampilan khusus) dan mendaftarkan implementasi default tanpa mengubah apa pun dalam metode yang disebut.

Misalkan jika pandangan kita dilindungi oleh protokol:

protocol ViewControllerInput: class {
     //  
    }
    extension ViewControllerInput: ErrorAlertPresentable { }
    
    extension ViewController: ErrorAlertCreatable { }
    //  ,         ,    
    //    
    
    //       "",    ErrorAlertPresentable      . 
    extension ViewController: ErrorToastCreatable { }

Tetapi contoh kami jauh lebih sederhana, jadi kami mendukung kedua protokol dan menjalankan aplikasi:

func requestFeed(...) {
    		service.requestObject { [weak self] (result) in
    			guard let `self` = self else { return }
    			switch result {
    			case .success:
    				break
    			case .failure(let error):
    				//           
    				// -     (   viewController)  ,
    				//    .     tryAgainOption 
    				let tryAgainOption = RecoveryOptions.tryAgain {
    					self.requestFeed(...)
    				}
    				let recoveryOptions = [tryAgainOption]
    				let attempter = RecoveryAttemper(recoveryOptions: recoveryOptions)
    				let recovableError = RecoverableError(error: error, attempter: attempter)
    				self.presentAlert(from: recovableError)
    			}
    		}
    	}
    
    // MARK: - ErrorAlertCreatable
    extension ViewController: ErrorAlertCreatable { }
    
    // MARK: - ErrorAlertPresentable
    extension ViewController: ErrorAlertPresentable { }



Tampaknya semuanya berhasil. Salah satu kondisi awal adalah dalam 2-3 baris. Kami akan memperluas usaha kami dengan konstruktor yang nyaman:

struct RecoveryAttemper {
    	//
    	...
    	//
    	static func tryAgainAttempter(block: @escaping (() -> Void)) -> Self {
    		return RecoveryAttemper(recoveryOptions: [.cancel, .tryAgain(action: block)])
    	}
    }
    
    
    func requestFeed() {
    		service.requestObject { [weak self] (result) in
    			guard let `self` = self else { return }
    			switch result {
    			case .success:
    				break
    			case .failure(let error):
    				//    
    				let recovableError = RecoverableError(error: error, attempter: .tryAgainAttempter(block: {
    					self.requestFeed()
    				}))
    				self.presentAlert(from: recovableError)
    			}
    		}
    	}

Kami mendapat solusi MVP, dan tidak akan sulit bagi kami untuk terhubung dan menyebutnya di mana saja dalam aplikasi kami. Mari kita mulai memeriksa kasus tepi dan skalabilitas.

Bagaimana jika kita memiliki beberapa skenario keluar?


Misalkan seorang pengguna memiliki repositori di aplikasi kami. Gudang memiliki batas tempat. Dalam hal ini, pengguna memiliki dua skenario untuk keluar dari kesalahan: pengguna dapat membebaskan ruang atau membeli lebih banyak. Kami akan menulis kode berikut:

//  
    func runOutOfSpace() {
    		service.runOfSpace { [weak self] (result) in
    			guard let `self` = self else { return }
    			switch result {
    			case .success:
    				break
    			case .failure(let error):
    				let notEnoughSpace = RecoveryOptions.freeSpace {
    					self.freeSpace()
    				}
    
    				let buyMoreSpace = RecoveryOptions.buyMoreSpace {
    					self.buyMoreSpace()
    				}
    				let options = [notEnoughSpace, buyMoreSpace]
    				let recovableError = RecoverableError(error: error, attempter: .cancalableAttemter(options: options))
    				self.presentAlert(from: recovableError)
    			}
    		}
    	}
    
    	func freeSpace() {
    		let alertViewController = createOKAlert(with: "Free space selected")
    		present(alertViewController, animated: true, completion: nil)
    	}
    
    	func buyMoreSpace() {
    		let alertViewController = createOKAlert(with: "Buy more space selected")
    		present(alertViewController, animated: true, completion: nil)
    	}
    
    
    struct RecoveryAttemper {
    	//
    	...
    	//
    	static func cancalableAttemter(options: [RecoveryOptions]) -> Self {
    		return RecoveryAttemper(recoveryOptions: [.cancel] + options)
    	}
    }



Ini mudah ditangani.

Jika kita ingin menunjukkan bukan peringatan, tetapi tampilan informasi di tengah layar?




Beberapa protokol baru dengan analogi akan menyelesaikan masalah kita:

protocol ErrorViewCreatable {
    	func createErrorView(for error: Error) -> ErrorView
    }
    
    // MARK: - Default implementation
    extension ErrorViewCreatable {
    	func createErrorView(for error: Error) -> ErrorView {
    		if let recoverableError = error as? RecoverableError {
    			return createRecoverableAlert(for: recoverableError)
    		}
    
    		let defaultTitle = "Error"
    		let description = errorReason(from: error)
    		if let localizedError = error as? LocalizedError {
    			return createErrorView(
    				title: localizedError.errorDescription ?? defaultTitle,
    				message: description)
    		}
    		return createErrorView(title: defaultTitle, message: description)
    		}
    
    	fileprivate func createErrorView(title: String?, message: String?, actions: [ErrorView.Action] = []) -> ErrorView {
    		//  ErrorView        . 
                //     github
    		return ErrorView(title: title, description: message, actions: actions)
    	}
    
    	fileprivate func createRecoverableAlert(for recoverableError: RecoverableError) -> ErrorView {
    		let title = recoverableError.errorDescription
    		let message = errorReason(from: recoverableError)
    		let actions = recoverableError.recoveryOptions.enumerated().map { (element) -> ErrorView.Action in
    			return ErrorView.Action(title: element.element) {
    				recoverableError.attemptRecovery(optionIndex: element.offset)
    			}
    		}
    		return createErrorView(title: title, message: message, actions: actions)
    	}
    }

protocol ErrorViewAddable: class {
    	func presentErrorView(from error: Error)
    
    	var errorViewSuperview: UIView { get }
    }
    
    // MARK: - Default implementation
    extension ErrorViewAddable where Self: ErrorViewCreatable {
    	func presentErrorView(from error: Error) {
    		let errorView = createErrorView(for: error)
    		errorViewSuperview.addSubview(errorView)
    		errorView.center = errorViewSuperview.center
    	}
    }
    
    
    //    
    
    // MARK: - ErrorAlertCreatable
    extension ViewController: ErrorViewCreatable { }
    
    // MARK: - ErrorAlertPresentable
    extension ViewController: ErrorViewAddable {
    	var errorViewSuperview: UIView {
    		return self
    	}
    }

Sekarang kita dapat menampilkan kesalahan dalam bentuk tampilan informasi. Terlebih lagi, kita dapat memutuskan bagaimana menunjukkannya. Misalnya, pertama kali Anda memasuki layar dan tampilan informasi kesalahan - tampilkan. Dan jika layar berhasil dimuat, tetapi tindakan di layar mengembalikan kesalahan - tampilkan peringatan.

Jika tidak ada akses ke tampilan?


Terkadang Anda perlu melempar kesalahan, tetapi tidak ada akses ke tampilan. Atau kami tidak tahu tampilan mana yang sedang aktif, dan kami ingin menampilkan lansiran di atas segalanya. Bagaimana cara mengatasi masalah ini?

Salah satu cara termudah (menurut saya) untuk melakukan hal yang sama seperti Apple dengan keyboard. Buat Jendela baru di atas layar saat ini. Ayo lakukan:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

	//   –   . 
	//              DI
	static private(set) var errorWindow: UIWindow = {
		let alertWindow = UIWindow.init(frame: UIScreen.main.bounds)
		alertWindow.backgroundColor = .clear

		//  rootViewController,    present   viewController
		let viewController = UIViewController()
		viewController.view.backgroundColor = .clear
		alertWindow.rootViewController = viewController

		return alertWindow
	}()

Buat lansiran baru yang dapat ditampilkan di atas segalanya:

final class AboveAllAlertController: UIAlertController {
	var alertWindow: UIWindow {
		return AppDelegate.alertWindow
	}

	func show() {
		let topWindow = UIApplication.shared.windows.last
		if let topWindow = topWindow {
			alertWindow.windowLevel = topWindow.windowLevel + 1
		}

		alertWindow.makeKeyAndVisible()
		alertWindow.rootViewController?.present(self, animated: true, completion: nil)
	}

	override func viewWillDisappear(_ animated: Bool) {
		super.viewWillDisappear(animated)

		alertWindow.isHidden = true
	}
}

protocol ErrorAlertCreatable: class {
    	//      
    	func createAlert(for error: Error, aboveAll: Bool) -> UIAlertController
    }
    
    // MARK: - Default implementation
    extension ErrorAlertCreatable where Self: UIViewController {
    	...
    	//     
    	fileprivate func createAlert(title: String?, message: String?, actions: [UIAlertAction], aboveAll: Bool) -> UIAlertController {
    		let alertViewController = aboveAll ?
    			AboveAllAlertController(title: title, message: message, preferredStyle: .alert) :
    			UIAlertController(title: title, message: message, preferredStyle: .alert)
    		actions.forEach({ alertViewController.addAction($0) })
    		return alertViewController
    	}
    }
    
    //     
    protocol ErrorAlertPresentable: class {
    	func presentAlert(from error: Error)
    	func presentAlertAboveAll(from error: Error)
    }
    
    // MARK: - Default implementation
    extension ErrorAlertPresentable where Self: ErrorAlertCreatable & UIViewController {
    	func presentAlert(from error: Error) {
    		let alertVC = createAlert(for: error, aboveAll: false)
    		present(alertVC, animated: true, completion: nil)
    	}
    
    	func presentAlertAboveAll(from error: Error) {
    		let alertVC = createAlert(for: error, aboveAll: true)
    		//         
    		if let alertVC = alertVC as? AboveAllAlertController {
    			alertVC.show()
    			return
    		}
    		//    ,  - 
    		assert(false, "Should create AboveAllAlertController")
    		present(alertVC, animated: true, completion: nil)
    	}
    }



Secara tampilan, tidak ada yang berubah, tetapi sekarang kami telah menyingkirkan hierarki pengontrol tampilan. Saya sangat merekomendasikan untuk tidak terbawa oleh kesempatan ini. Lebih baik memanggil kode tampilan di router atau entitas dengan hak yang sama. Atas nama transparansi dan kejelasan.

Kami memberi pengguna alat yang hebat untuk mengirim spam ke server selama kegagalan fungsi, pemeliharaan, dll. Apa yang bisa kita tingkatkan?

Waktu permintaan minimum


Misalkan kita mematikan Internet dan coba lagi. Jalankan loader. Jawabannya akan datang secara instan dan mendapatkan mini-game "Clicker". Dengan animasi yang berkedip. Tidak terlalu baik.



Mari kita ubah kesalahan instan menjadi sebuah proses. Idenya sederhana - kami akan membuat waktu permintaan minimum. Di sini implementasinya tergantung pada pendekatan Anda terhadap jaringan. Misalkan saya menggunakan Operasi, dan bagi saya tampilannya seperti ini:

final class DelayOperation: AsyncOperation {
	private let _delayTime: Double

	init(delayTime: Double = 0.3) {
		_delayTime = delayTime
	}
	override func main() {
		super.main()

		DispatchQueue.global().asyncAfter(deadline: .now() + _delayTime) {
			self.state = .finished
		}
	}
}
		
// -  
let flowListOperation = flowService.list(for: pageID, path: path, limiter: limiter)
let handler = createHandler(for: flowListOperation)
let delayOperation = DelayOperation(delayTime: 0.5)
///  >>>  addDependency. 
[flowListOperation, delayOperation] >>> handler
operationQueue.addOperations([flowListOperation, delayOperation, handler])

Untuk kasus umum, saya dapat menawarkan desain ini:

//  global      
DispatchQueue.global().asyncAfter(deadline: .now() + 0.15) {
    // your code here
}

Atau kita dapat membuat abstraksi atas tindakan asinkron kami dan menambahkan pengelolaan ke dalamnya:

struct Task {
    	let closure: () -> Void
    
    	private var _delayTime: Double?
    
    	init(closure: @escaping () -> Void) {
    		self.closure = closure
    	}
    
    	fileprivate init(closure: @escaping () -> Void, time: Double) {
    		self.closure = closure
    		_delayTime = time
    	}
    
    	@discardableResult
    	func run() -> Self {
    		if let delayTime = _delayTime {
    			DispatchQueue.global().asyncAfter(deadline: .now() + delayTime) {
    				self.closure()
    			}
    			return self
    		}
    		closure()
    		return self
    	}
    
    	func delayedTask(time: Double) -> Self {
    		return Task(closure: closure, time: time)
    	}
    }
    
    //    
    func requestObject(completionHandler: @escaping ((Result<Bool, Error>) -> Void)) -> Task {
    		return Task {
    			completionHandler(.failure(NetworkError.internetError))
    		}
    			.delayedTask(time: 0.5)
    		.run()
    	}

Sekarang animasi kita tidak akan tampak begitu tajam, bahkan ketika sedang offline. Saya sarankan menggunakan pendekatan ini di sebagian besar tempat dengan animasi.



Untuk mode pesawat, ada baiknya menampilkan prompt peringatan (pengguna bisa lupa mematikan mode untuk mulai bekerja dengan aplikasi). Seperti, katakanlah, membuat telegram. Dan untuk pertanyaan penting, ada baiknya mengulang beberapa kali di bawah tenda sebelum menunjukkan peringatan ... Tapi lebih banyak tentang itu lain kali :)

Testabilitas


Ketika semua logika dibuang di viewController (seperti yang kita miliki sekarang), sulit untuk menguji. Namun, jika viewController Anda dibagikan dengan logika bisnis, pengujian menjadi tugas yang sepele. Dengan jentikan celana lengan , logika bisnis berubah menjadi:

func requestFeed() {
		service.requestObject { [weak self] (result) in
			guard let `self` = self else { return }
			switch result {
			case .success:
				break
			case .failure(let error):
				DispatchQueue.main.async {
					let recoverableError = RecoverableError(error: error, attempter: .tryAgainAttempter(block: {
						self.requestFeed()
					}))
					//     
					self.viewInput?.presentAlert(from: recoverableError)
				}
			}
		}
	}

// -  
func testRequestFeedFailed() {
	// Put out mock that conform to AlertPresntable protocol
	controller.viewInput = ViewInputMock()

	//  .    ,    
	//    expectation
	controller.requestFeed()

	// Our mocked object should save to true to bool variable when method called
	XCTAssert(controller.viewInput.presentAlertCalled)
	// Next we could compare recoverable error attempter to expected attempter
}

Bersama dengan artikel ini kita:

  • Membuat mekanisme yang nyaman untuk menampilkan peringatan
  • Memberi pengguna opsi untuk mencoba kembali operasi yang gagal
  • Dan mencoba meningkatkan pengalaman pengguna dengan aplikasi kami

→  Tautan ke kode

Terima kasih atas waktu Anda, dengan senang hati saya akan menjawab pertanyaan Anda di komentar.

All Articles