Vom Fehler zum Alarm mit Aktionen

Hallo Habr! Für den Benutzer sehen die Fehlermeldungen oft so aus: "Irgendwas stimmt nicht, AAAA!". Natürlich möchte er anstelle von Fehlern den magischen Fehler „Repariere alles“ sehen. Nun, oder andere Optionen. Wir haben begonnen, diese aktiv zu uns selbst hinzuzufügen, und ich möchte darüber sprechen, wie Sie dies tun können.



Stellen Sie sich zunächst vor - mein Name ist Alexander, die letzten sechs Jahre habe ich der iOS-Entwicklung gewidmet. Jetzt bin ich für die ManyChat-Mobilanwendung verantwortlich und werde Probleme anhand seines Beispiels lösen.

Lassen Sie uns sofort formulieren, was wir tun werden:

  • Funktion zum Fehlertyp hinzufügen
  • Verwandeln Sie Fehler in benutzerfreundliche Warnungen
  • Wir zeigen die möglichen weiteren Aktionen in der Oberfläche an und verarbeiten deren Klicks

Und das alles wird auf Swift sein :)

Wir werden das Problem anhand eines Beispiels lösen. Der Server hat einen Fehler mit dem Code 500 anstelle der erwarteten 200 zurückgegeben. Was soll der Entwickler tun? Zumindest mit der Trauer, den Benutzer zu informieren - der erwartete Beitrag mit Siegeln konnte nicht heruntergeladen werden. In Apple ist das Standardmuster alarmiert. Schreiben wir also eine einfache Funktion:

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 Der Einfachheit halber befindet sich der größte Teil des Codes in der Steuerung. Es steht Ihnen frei, dieselben Ansätze in Ihrer Architektur zu verwenden. Der Artikelcode wird im Repository verfügbar sein . Am Ende des Artikels befindet sich auch dieser Link.

Wir erhalten folgendes Bild:



Theoretisch haben wir die Aufgabe erledigt. Aber einige Dinge sind sofort offensichtlich:

  • Wir haben nicht die Möglichkeit gegeben, irgendwie von einem fehlerhaften zu einem erfolgreichen Szenario zu wechseln. OK, im aktuellen Fall wird nur die Warnung ausgeblendet - und dies ist keine Lösung
  • Aus Sicht der Benutzererfahrung muss der Text klarer und neutraler gestaltet werden. Damit der Benutzer keine Angst hat und nicht läuft, um Ihrer Anwendung einen Stern in den AppStore zu setzen. In diesem Fall wäre ein detaillierter Text beim Debuggen hilfreich
  • Und um ehrlich zu sein - Warnungen sind als Lösung etwas veraltet (in Anwendungen werden zunehmend Dummy-Bildschirme oder Toasts angezeigt). Dies ist jedoch bereits eine Frage, die separat mit dem Team besprochen werden sollte

Stimmen Sie zu, die unten dargestellte Option sieht viel sympathischer aus.



Unabhängig davon, welche Option Sie auswählen, müssen Sie für jede dieser Optionen einen solchen Mechanismus zum Anzeigen einer Nachricht ausdenken, die bei Auftreten eines beliebigen Fehlers gut aussieht, dem Benutzer ein klares Skript für die weitere Arbeit in der Anwendung bieten und eine Reihe von Aktionen bereitstellen. Die Lösung ist:

  • Muss erweiterbar sein. Wir alle kennen die inhärente Variabilität des Designs. Unser Mechanismus muss zu allem bereit sein
  • Es wird dem Objekt in einigen Codezeilen hinzugefügt (und entfernt)
  • Gut getestet

Aber lassen Sie uns vorher in das theoretische Minimum für Fehler in Swift eintauchen.

Fehler in Swift


Dieser Absatz bietet eine allgemeine Übersicht über Fehler auf oberster Ebene. Wenn Sie Ihre Fehler in der Anwendung bereits aktiv verwenden, können Sie sicher mit dem nächsten Absatz fortfahren.

Was ist ein Fehler? Eine falsche Handlung oder ein falsches Ergebnis. Oft können wir mögliche Fehler annehmen und diese im Voraus im Code beschreiben.

In diesem Fall gibt Apple den Typ Fehler an. Wenn wir die Apple-Dokumentation öffnen, sieht der Fehler folgendermaßen aus (relevant für Swift 5.1):

public protocol Error {
}

Nur ein Protokoll ohne zusätzliche Anforderungen. In der Dokumentation wird freundlicherweise erläutert, dass aufgrund des Fehlens der erforderlichen Parameter jeder Typ im Swift-Fehlerbehandlungssystem verwendet werden kann. Mit einem so sanften Protokoll werden wir einfach arbeiten.

Die Idee, enum zu verwenden, kommt mir sofort in den Sinn: Es gibt eine endliche bekannte Anzahl von Fehlern, sie können eine Art von Parametern haben. Welches ist, was Apple tut. Sie können beispielsweise einen DecodingError implementieren:

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)
    
    ...

Nutzen Sie die Best Practices von Apple. Stellen Sie sich eine Gruppe möglicher Netzwerkfehler in vereinfachter Form vor:

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

Jetzt können wir überall in unserer Anwendung, wo der Fehler auftritt, unseren Network.Error verwenden.

Wie arbeite ich mit Fehlern? Es gibt einen Fangmechanismus. Wenn eine Funktion einen Fehler auslösen kann, wird sie mit dem Schlüsselwort throw markiert. Jetzt muss jeder seiner Benutzer über das Konstrukt do catch darauf zugreifen. Wenn es keinen Fehler gibt, fallen wir mit einem Fehler in den do-Block in den catch-Block. Die Funktionen, die zum Fehler führen, können eine beliebige Zahl im do-Block sein. Der einzige Nachteil ist, dass wir im Fang einen Fehler vom Typ Fehler erhalten. Sie müssen den Fehler in den gewünschten Typ umwandeln.

Alternativ können wir die Option verwenden, dh im Fehlerfall Null erhalten und das sperrige Design entfernen. Manchmal ist es bequemer: Nehmen wir an, wir erhalten eine optionale Variable und wenden dann eine Wurffunktion darauf an. Der Code kann in einen if / guard-Block eingefügt werden und bleibt präzise.

Hier ist ein Beispiel für die Arbeit mit der Wurffunktion:

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 Verwechseln Sie nicht mit do catch in anderen Sprachen. Swift löst keine Ausnahme aus, schreibt jedoch den Wert des Fehlers (falls er aufgetreten ist) in ein spezielles Register. Wenn es einen Wert gibt, geht er zum Fehlerblock. Wenn nicht, wird der do-Block fortgesetzt. Quellen für die Neugierigsten: www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html

Diese Methode eignet sich für die Behandlung synchroner Ereignisse und ist für lange Vorgänge nicht so praktisch (z. B. Daten über das Netzwerk anfordern), was möglicherweise zeitaufwändig sein kann. Dann können Sie die einfache Vervollständigung verwenden.

Als Alternative zu Swift 5 wurde Result eingeführt - eine vorbereitete Aufzählung, die zwei Optionen enthält - Erfolg und Misserfolg. An sich ist die Verwendung von Error nicht erforderlich. Und es hat keine direkte Beziehung zur Asynchronität. Für asynchrone Ereignisse ist es jedoch bequemer, genau diesen Typ auf den Abschluss zurückzusetzen (andernfalls müssen Sie zwei Abschlüsse ausführen, Erfolg und Misserfolg, oder zwei Parameter zurückgeben). Schreiben wir ein Beispiel:

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)
	}
})

Diese Informationen reichen für uns aus, um zu arbeiten.

Noch einmal kurz:

  • Fehler in Swift ist ein Protokoll
  • Es ist zweckmäßig, Fehler in Form einer Aufzählung darzustellen
  • Es gibt zwei Möglichkeiten, mit Fehlern umzugehen: synchron (fangen) und asynchron (Ihre eigene Konkurrenz oder Ihr eigenes Ergebnis).

Fehlertext


Kommen wir zurück zum Thema des Artikels. Im obigen Absatz haben wir unsere eigene Art von Fehlern erstellt. Da ist er:

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

Jetzt müssen wir jeden Fehler mit einem Text abgleichen, der für den Benutzer verständlich ist. Wir werden es im Fehlerfall in der Benutzeroberfläche anzeigen. LocalizedError Protocol beeilt sich, uns zu helfen. Es erbt den Protokollfehler und ergänzt ihn mit 4 Eigenschaften:

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

Wir implementieren das Protokoll:

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"
            }
        }
    }

Die Fehleranzeige wird sich kaum ändern:

	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)

Großartig, mit dem Text war alles einfach. Fahren wir mit den Schaltflächen fort.

Fehlerbehebung


Lassen Sie uns den Fehlerbehandlungsalgorithmus in einem einfachen Diagramm darstellen. In einer Situation, in der aufgrund eines Fehlers ein Dialogfeld mit den Optionen "Erneut versuchen", "Abbrechen" und möglicherweise einigen bestimmten Optionen angezeigt wird, erhalten wir das folgende Schema:



Wir werden das Problem am Ende lösen. Wir brauchen eine Funktion, die einen Alarm mit n + 1 Optionen anzeigt. Wir werfen, da wir einen Fehler zeigen möchten:

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)
    }

Eine Funktion, die die Art des Fehlers bestimmt und ein Signal sendet, um eine Warnung anzuzeigen:

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


Und eine erweiterte Art von Fehler, die einen Kontext und ein Verständnis dafür hat, was mit dieser oder jener Option zu tun ist.

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

Der Kopf zeichnet sofort ein Diagramm Ihres Fahrrads. Aber zuerst überprüfen wir die Apple-Docks. Vielleicht liegt ein Teil des Mechanismus bereits in unseren Händen.

Native Implementierung?


Ein bisschen Internet-Suche führt zum Protokoll 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
}

Es sieht so aus, als ob wir suchen:

  • recoveryOptions: [String] - eine Eigenschaft, in der Wiederherstellungsoptionen gespeichert werden
  • func tryRecovery (optionIndex: Int) -> Bool - stellt einen Fehler synchron wieder her. Richtig - auf Erfolg
  • func tryRecovery (optionIndex: Int, resultHandler: (Bool) -> Void) - Asynchrone Option, die Idee ist dieselbe

Mit Bedienungsanleitungen ist alles bescheidener. Eine kleine Suche auf der Apple-Website und in der Umgebung führt zu einem Artikel über die Fehlerbehandlung, der vor den öffentlichen Ankündigungen von Swift verfasst wurde.

Kurz:

  • Der Mechanismus ist für MacOs-Anwendungen gedacht und zeigt ein Dialogfeld
  • Es wurde ursprünglich um NSError herum gebaut.
  • Das RecoveryAttempter-Objekt ist in den Fehler in userInfo eingekapselt, der die Fehlerbedingungen kennt und die beste Lösung für das Problem auswählen kann. Objekt darf nicht Null sein
  • RecoveryAttempter muss das informelle Protokoll NSErrorRecoveryAttempting unterstützen
  • Auch in userInfo sollte Wiederherstellungsoption sein
  • Und alles hängt mit dem Aufruf der presentError-Methode zusammen, die nur im macOS SDK enthalten ist. Er zeigt eine Warnung
  • Wenn die Warnung über presentError angezeigt wird, wird bei Auswahl einer Option im Popup-Fenster in AppDelegate eine interessante Funktion angezeigt:

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

Aber da wir keinen PresentError haben, können wir ihn nicht ziehen.



An diesem Punkt scheint es, als hätten wir eher eine Leiche als einen Schatz ausgegraben. Wir müssen Error in NSError umwandeln und unsere eigene Funktion schreiben, um die Warnung von der Anwendung anzuzeigen. Eine Reihe impliziter Verbindungen. Es ist möglich, schwierig und nicht ganz klar - "Warum?".

Während die nächste Tasse Tee brüht, könnte man sich fragen, warum die obige Funktion delegate als Any verwendet und den Selektor übergibt. Die Antwort ist unten:

Antworten
iOS 2. ! ( , ). :)

Ein Fahrrad bauen


Lassen Sie uns das Protokoll implementieren, es wird uns nicht schaden:

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)
	}
}

Die Indexabhängigkeit ist nicht die bequemste Lösung (wir können leicht über das Array hinausgehen und die Anwendung zum Absturz bringen). Aber für MVP reicht das. Nehmen Sie die Idee von Apple, modernisieren Sie sie einfach. Wir benötigen ein separates Attempter-Objekt und Schaltflächenoptionen, die wir ihm geben werden:

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
    }

Jetzt müssen Sie den Fehler anzeigen. Ich mag Protokolle sehr, deshalb werde ich das Problem durch sie lösen. Erstellen wir ein universelles Protokoll zum Erstellen eines UIAlertControllers aus Fehlern:

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"
    	}
    }

Und das Protokoll zum Anzeigen der erstellten Warnungen:

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)
	}
}

Es stellte sich als umständlich, aber überschaubar heraus. Wir können neue Möglichkeiten zum Anzeigen eines Fehlers erstellen (z. B. Toasten oder Anzeigen einer benutzerdefinierten Ansicht) und die Standardimplementierung registrieren, ohne die aufgerufene Methode zu ändern.

Angenommen, unsere Ansicht wäre durch ein Protokoll abgedeckt:

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

Unser Beispiel ist jedoch viel einfacher, daher unterstützen wir beide Protokolle und führen die Anwendung aus:

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 { }



Es scheint, dass alles geklappt hat. Eine der Anfangsbedingungen war in 2-3 Zeilen. Wir werden unseren Versucher mit einem praktischen Konstruktor erweitern:

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)
    			}
    		}
    	}

Wir haben die MVP-Lösung erhalten, und es wird für uns nicht schwierig sein, eine Verbindung herzustellen und sie irgendwo in unserer Anwendung aufzurufen. Beginnen wir mit der Überprüfung der Kantenfälle und der Skalierbarkeit.

Was ist, wenn wir mehrere Exit-Szenarien haben?


Angenommen, ein Benutzer hat ein Repository in unserer Anwendung. Das Gewölbe hat ein Platzlimit. In diesem Fall hat der Benutzer zwei Szenarien zum Beenden des Fehlers: Der Benutzer kann entweder Speicherplatz freigeben oder mehr kaufen. Wir werden den folgenden Code schreiben:

//  
    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)
    	}
    }



Dies war leicht zu handhaben.

Wenn wir keine Warnung, sondern eine Informationsansicht in der Mitte des Bildschirms anzeigen möchten?




Ein paar neue Protokolle in Analogie werden unser Problem lösen:

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
    	}
    }

Jetzt können wir Fehler in Form einer Informationsansicht anzeigen. Darüber hinaus können wir entscheiden, wie wir sie zeigen möchten. Zum Beispiel, wenn Sie zum ersten Mal den Bildschirm und den Fehler aufrufen - Informationsansicht anzeigen. Und wenn der Bildschirm erfolgreich geladen wurde, die Aktion auf dem Bildschirm jedoch einen Fehler zurückgab, zeigen Sie eine Warnung an.

Wenn es keinen Zugriff auf die Ansicht gibt?


Manchmal müssen Sie einen Fehler auslösen, aber es gibt keinen Zugriff auf die Ansicht. Oder wir wissen nicht, welche Ansicht gerade aktiv ist, und wir möchten zusätzlich zu allem eine Warnung anzeigen. Wie kann man dieses Problem lösen?

Eine der einfachsten Möglichkeiten (meiner Meinung nach), dasselbe zu tun wie Apple mit der Tastatur. Erstellen Sie ein neues Fenster über dem aktuellen Bildschirm. Machen wir das:

@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
	}()

Erstellen Sie eine neue Warnung, die über allem angezeigt werden kann:

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)
    	}
    }



Im Erscheinungsbild hat sich nichts geändert, aber jetzt haben wir die Hierarchie der Ansichts-Controller beseitigt. Ich empfehle dringend, sich von dieser Gelegenheit nicht mitreißen zu lassen. Es ist besser, den Anzeigecode in einem Router oder einer Entität mit denselben Rechten aufzurufen. Im Namen von Transparenz und Klarheit.

Wir haben Benutzern ein großartiges Tool zum Spammen von Servern bei Fehlfunktionen, Wartung usw. zur Verfügung gestellt. Was können wir verbessern?

Minimale Anforderungszeit


Angenommen, wir schalten das Internet aus und versuchen es erneut. Führen Sie den Lader aus. Die Antwort wird sofort kommen und ein Minispiel "Clicker" erhalten. Mit blinkender Animation. Nicht zu schön.



Lassen Sie uns einen sofortigen Fehler in einen Prozess verwandeln. Die Idee ist einfach - wir machen die minimale Anforderungszeit. Hier hängt die Implementierung von Ihrem Netzwerkansatz ab. Angenommen, ich verwende Operation und für mich sieht es so aus:

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])

Für den allgemeinen Fall kann ich dieses Design anbieten:

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

Oder wir können eine Abstraktion über unsere asynchronen Aktionen erstellen und die Verwaltbarkeit erweitern:

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()
    	}

Jetzt erscheint unsere Animation auch im Offline-Modus nicht mehr so ​​scharf. Ich empfehle diesen Ansatz an den meisten Stellen mit Animation.



Im Flugzeugmodus empfiehlt es sich, eine Warnmeldung anzuzeigen (der Benutzer kann vergessen, den Modus auszuschalten, um mit der Anwendung zu arbeiten). Wie macht zum Beispiel ein Telegramm. Und für wichtige Fragen ist es gut, einige Male unter der Haube zu wiederholen, bevor eine Warnung angezeigt wird ... Aber dazu ein anderes Mal mehr :)

Testbarkeit


Wenn die gesamte Logik im viewController gespeichert ist (wie jetzt), ist es schwierig zu testen. Wenn Ihr viewController jedoch mit der Geschäftslogik geteilt wird, wird das Testen zu einer trivialen Aufgabe. Mit einer Bewegung der Arm Hosen, dreht sich die Geschäftslogik in:

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
}

Zusammen mit diesem Artikel haben wir:

  • Ein praktischer Mechanismus zum Anzeigen von Warnungen
  • Benutzer hatten die Möglichkeit, einen erfolglosen Vorgang erneut zu versuchen
  • Und versucht, die Benutzererfahrung mit unserer Anwendung zu verbessern

→  Link zum Code

Vielen Dank für Ihre Zeit, ich werde Ihre Fragen gerne in den Kommentaren beantworten.

All Articles