哈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)
}
case typeMismatch(Any.Type, DecodingError.Context)
case valueNotFound(Any.Type, DecodingError.Context)
...
利用苹果的最佳实践。以简化形式想象一组可能的网络错误:enum NetworkError: Error {
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 {
print(error)
}
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 {
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:
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并传递选择器。答案如下:造自行车
让我们实现该协议,它不会对我们造成伤害: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 RecoveryOptions {
case tryAgain(action: (() -> Void))
case cancel
}
现在您需要显示错误。我真的很喜欢协议,因此我将通过它们解决问题。让我们创建一个用于从错误创建UIAlertController的通用协议:protocol ErrorAlertCreatable: class, ErrorReasonExtractable {
func createAlert(for error: Error) -> UIAlertController
}
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?
}
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)
}
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 { }
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):
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)
}
}
}
extension ViewController: ErrorAlertCreatable { }
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
}
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 {
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 }
}
extension ErrorViewAddable where Self: ErrorViewCreatable {
func presentErrorView(from error: Error) {
let errorView = createErrorView(for: error)
errorViewSuperview.addSubview(errorView)
errorView.center = errorViewSuperview.center
}
}
extension ViewController: ErrorViewCreatable { }
extension ViewController: ErrorViewAddable {
var errorViewSuperview: UIView {
return self
}
}
现在我们可以以信息视图的形式显示错误。此外,我们可以决定如何显示它们。例如,第一次进入屏幕时出现错误-显示信息视图。如果屏幕加载成功,但是屏幕上的操作返回了错误-显示警报。是否无法访问视图?
有时您需要抛出一个错误,但是无法访问该视图。或者,我们不知道当前哪个视图处于活动状态,我们想在所有视图之上显示警报。如何解决这个问题呢?(在我看来)最简单的方法之一是做与Apple键盘相同的事情。在当前屏幕顶部创建一个新窗口。我们开始做吧:@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
static private(set) var errorWindow: UIWindow = {
let alertWindow = UIWindow.init(frame: UIScreen.main.bounds)
alertWindow.backgroundColor = .clear
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
}
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)
}
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)
[flowListOperation, delayOperation] >>> handler
operationQueue.addOperations([flowListOperation, delayOperation, handler])
对于一般情况,我可以提供以下设计:
DispatchQueue.global().asyncAfter(deadline: .now() + 0.15) {
}
或者我们可以对异步操作进行抽象并为其添加可管理性: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() {
controller.viewInput = ViewInputMock()
controller.requestFeed()
XCTAssert(controller.viewInput.presentAlertCalled)
}
与本文一起,我们:- 制作了方便的警报显示机制
- 为用户提供重试失败操作的选项
- 并尝试通过我们的应用程序改善用户体验
→ 链接到代码谢谢您的宝贵时间,我很乐意在评论中回答您的问题。