从错误到行动警报

哈Ha!对于用户而言,错误消息通常看起来像“出了点问题,AAAA!”。当然,他希望看到错误“修复全部”而不是犯错。好吧,还是其他选择。我们开始积极地将它们添加到我们自己中,我想谈一谈如何做到这一点。



首先,自我介绍-我的名字叫Alexander,最近六年我致力于iOS开发。现在,我负责ManyChat移动应用程序,我将使用他的示例解决问题。

让我们立即制定我们将要做的事情:

  • 向错误类型添加功能
  • 将错误变成用户友好的警报
  • 我们在界面中显示可能的进一步操作并处理其点击

所有这些都将放在Swift上:)

我们将用一个例子解决这个问题。服务器返回的错误代码为500,而不是预期的200。开发人员应该怎么做?至少,带着悲伤的心情通知用户-无法下载带有盖章的预期帖子。在Apple中,标准模式是警报,因此让我们编写一个简单的函数:

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为简单起见,大多数代码都在控制器中。您可以在体系结构中自由使用相同的方法。文章代码将在存储库中提供,在文章末尾,此链接也将可用。

我们得到以下图片:



理论上,我们完成了任务。但是,有几件事立即显而易见:

  • 我们没有机会以某种方式从错误的情况转换为成功的情况。在当前情况下还可以,它只是隐藏警报-这不是解决方案
  • 从用户体验的角度来看,文本需要变得更加清晰,中性。这样用户就不会害怕,也不会在AppStore中给应用程序加一星。在这种情况下,调试时的详细文字对我们很有用
  • 而且,老实说-警报在某种程度上已经过时(作为解决方案)(越来越多的虚拟屏幕或吐司出现在应用程序中)。但这已经是一个问题,应该与团队单独讨论

同意,下面显示的选项看起来更有同情心。



无论您选择哪种选项,对于其中任何一个,您都需要考虑这样一种机制来显示一条消息,该消息在发生任意错误时看起来很棒,它将为用户提供一个清晰的脚本,以便在应用程序中进行进一步的工作并提供一系列操作。解决方案是:

  • 必须是可扩展的。我们都知道固有的设计可变性。我们的机制必须为任何事情做好准备
  • 通过几行代码将其添加到对象(并删除)
  • 经过测试

但是在此之前,让我们深入探讨Swift中错误的理论最小值。

Swift中的错误


本段是一般错误的顶级概述。如果您已经在应用程序中积极使用错误,则可以安全地进行下一段。

怎么了 某种错误的操作或不正确的结果。通常,我们可以假设可能的错误,并提前在代码中对其进行描述。

对于这种情况,Apple为我们提供了错误类型。如果我们打开Apple文档,则Error会看起来像这样(与Swift 5.1相关):

public protocol Error {
}

只是一个协议,没有其他要求。该文档很好地解释了-缺少必需的参数允许在Swift错误处理系统中使用任何类型。有了如此温和的协议,我们将轻松工作。

立即想到使用枚举的想法:已知的错误数量有限,它们可能具有某种参数。苹果正在做什么。例如,您可以考虑实现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)
    
    ...

利用苹果的最佳实践。以简化形式想象一组可能的网络错误:

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

现在,在应用程序中发生错误的任何地方,我们都可以使用Network.Error。

如何处理错误?有一个捕获机制。如果一个函数可以引发错误,则将其标记为throws关键字。现在,它的每个用户都需要通过do catch构造对其进行访问。如果没有错误,我们将陷入错误的do块中,进入catch块。导致错误的函数在do块中可以是任何数字。唯一的负面影响是,在catch中,我们收到错误类型为Error的错误。您将需要将错误转换为所需的类型。

作为替代方案,我们可以使用可选的方法,即在出现错误的情况下获取nil并摆脱笨重的设计。有时它更方便:假设我们得到一个可选变量,然后对其应用一个throws函数。可以将代码放在一个if / guard块中,它将保持简洁。

这是使用throws函数的示例:

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不要与其他语言的“抓捕”混淆。 Swift不会抛出异常,而是将错误的值(如果发生了)写入一个特殊的寄存器。如果有一个值,它将转到错误块,如果没有,则继续执行do块。最好奇的来源:www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html

此方法适用于处理同步事件,但不适用于长时间操作(例如,通过网络请求数据),这可能很耗时。然后,您可以使用简单的完成。

作为Swift 5的替代方案,引入了Result-一个包含两个选项的成功枚举-成功和失败。就其本身而言,它不需要使用Error。它与异步没有直接关系。但是将这种类型精确地返回完成对于异步事件更为方便(否则,您将必须执行两个完成,成功和失败,或者返回两个参数)。让我们写一个例子:

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

这些信息足以供我们工作。

再次简要地:

  • Swift中的错误是一个协议
  • 以枚举的形式表示错误很方便
  • 有两种处理错误的方法-同步(捕获)和异步(您自己的竞争或结果)

错误文字


让我们回到本文的主题。在上面的段落中,我们创建了自己的错误类型。他在那:

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

现在,我们需要将每个错误与用户可以理解的文本进行匹配。如果发生错误,我们将在界面中显示它。LocalizedError协议急于为我们提供帮助。它继承协议错误并补充4个属性:

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

我们实现该协议:

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

错误显示几乎不会改变:

	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)

太好了,使用文字一切都很容易。让我们继续按钮。

错误恢复


让我们以简单的图表形式介绍错误处理算法。对于由于错误而导致的情况,我们将显示一个对话框,其中包含“重试”,“取消”选项以及可能的某些特定选项,我们可以找到方案:



让我们从头开始解决问题。我们需要一个显示n + 1个警报的函数。我们抛出,因为我们想显示一个错误:

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

确定错误类型并发送信号以显示警报的功能:

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


还有扩展的错误类型,它具有上下文并了解如何使用此选项或该选项。

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

头部立即绘制出您的自行车图。但是首先,让我们检查一下Apple基座。机制的一部分也许已经掌握在我们手中。

本机实现?


互联网搜索会导致协议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
}

看起来我们正在寻找:

  • recoveryOptions:[String]-存储恢复选项的属性
  • func tryRecovery(optionIndex:Int)-> Bool-从错误中同步恢复。真实-成功
  • func tryRecovery(optionIndex:Int,resultHandler:(Bool)-> Void)-异步选项,想法是相同的

有了使用指南,一切都变得更加温和。在Apple网站及其周围地区进行的一次小型搜索导致在Swift的公告之前撰写一篇有关错误处理文章

简要地:

  • 该机制是为MacOs应用程序考虑的,并显示一个对话框
  • 它最初是围绕NSError构建的。
  • RecoveryAttempter对象封装在userInfo的错误中,该信息了解错误的情况,并可以选择最佳的解决方案。对象不能为零
  • RecoveryAttempter必须支持非正式协议NSErrorRecoveryAttempting
  • 另外在userInfo应该是恢复选项
  • 一切都与调用presentError方法有关,该方法仅在macOS SDK中。他显示警报
  • 如果警报是通过presentError显示的,那么当您在AppDelegate的弹出窗口中选择一个选项时,一个有趣的函数就会抽搐:

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

但是,由于我们没有presentError,因此无法将其拉出。



在这一点上,感觉就像我们是在挖尸体而不是宝藏。我们将必须将Error转换为NSError并编写我们自己的函数以由应用程序显示警报。一堆隐式连接。可能,困难且不完全清楚-“为什么?”。

在冲泡下一杯茶时,您可能想知道为什么上面的函数将委托作为Any并传递选择器。答案如下:

回答
iOS 2. ! ( , ). :)

造自行车


让我们实现该协议,它不会对我们造成伤害:

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

索引依赖不是最方便的解决方案(我们可以轻松地超越数组并使应用程序崩溃)。但是对于MVP来说会的。以Apple的想法为例,对其进行现代化改造。我们需要一个单独的Attempter对象和按钮选项:

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
    }

现在您需要显示错误。我真的很喜欢协议,因此我将通过它们解决问题。让我们创建一个用于从错误创建UIAlertController的通用协议:

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

以及用于显示创建的警报的协议:

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

原来很麻烦,但是很容易管理。我们可以创建显示错误的新方法(例如,吐司或显示自定义视图)并注册默认实现,而无需在调用的方法中进行任何更改。

假设协议涵盖了我们的观点:

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

但是我们的示例更加简单,因此我们同时支持两种协议并运行该应用程序:

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



似乎一切顺利。初始条件之一是2-3行。我们将通过一个方便的构造函数来扩展我们的尝试器:

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

我们获得了MVP解决方案,对于我们来说,在应用程序中的任何位置进行连接和调用都不会困难。让我们开始检查极端情况和可伸缩性。

如果我们有几种退出方案怎么办?


假设用户在我们的应用程序中有一个存储库。保险库有位置限制。在这种情况下,用户有两种退出该错误的方案:用户可以释放空间或购买更多空间。我们将编写以下代码:

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



这很容易解决。

如果我们不想在屏幕中间显示警报,而是显示信息视图?




通过类推几个新协议可以解决我们的问题:

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

现在我们可以以信息视图的形式显示错误。此外,我们可以决定如何显示它们。例如,第一次进入屏幕时出现错误-显示信息视图。如果屏幕加载成功,但是屏幕上的操作返回了错误-显示警报。

是否无法访问视图?


有时您需要抛出一个错误,但是无法访问该视图。或者,我们不知道当前哪个视图处于活动状态,我们想在所有视图之上显示警报。如何解决这个问题呢?

(在我看来)最简单的方法之一是做与Apple键盘相同的事情。在当前屏幕顶部创建一个新窗口。我们开始做吧:

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

创建一个可以显示在所有内容之上的新警报:

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



在外观上,什么都没有改变,但是现在我们摆脱了视图控制器的层次结构。我强烈建议您不要错过这个机会。最好在具有相同权限的路由器或实体中调用显示代码。以透明和清晰的名义。

我们为用户提供了一个出色的工具,可以在发生故障,维护等过程中向服务器发送垃圾邮件。我们可以改善什么?

最短要求时间


假设我们关闭互联网,然后再试一次。运行加载程序。答案将立即出现并获得一个迷你游戏“ Clicker”。具有闪烁的动画。不太好



让我们将即时错误变成一个过程。这个想法很简单-我们将使请求时间最短。这里的实现取决于您的联网方法。假设我使用Operation,对我来说,它看起来像这样:

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

对于一般情况,我可以提供以下设计:

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

或者我们可以对异步操作进行抽象并为其添加可管理性:

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

现在,即使离线,我们的动画也不会显得那么清晰。我建议在大多数带有动画的地方使用这种方法。



对于飞行模式,最好显示警报提示(用户可能会忘记关闭该模式以开始使用该应用程序)。就像电报一样。对于重要的查询,最好先在引擎盖下重复几次,然后再显示警报...但是,下次再说吧:)

可测性


当所有逻辑都转储到viewController中时(如我们现在所述),很难进行测试。但是,如果您的viewController与业务逻辑共享,则测试将成为一项琐碎的任务。轻弹一下裤子,业务逻辑就会变成:

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
}

与本文一起,我们:

  • 制作了方便的警报显示机制
  • 为用户提供重试失败操作的选项
  • 并尝试通过我们的应用程序改善用户体验

→  链接到代码

谢谢您的宝贵时间,我很乐意在评论中回答您的问题。

All Articles