من الخطأ إلى التنبيه بالإجراءات

مرحبا يا هابر! بالنسبة للمستخدم ، غالبًا ما تبدو رسائل الخطأ مثل "هناك خطأ ما ، AAAA!". بالطبع ، يود بدلاً من الأخطاء أن يرى الخطأ السحري "إصلاحه كله". حسنا ، أو خيارات أخرى. بدأنا في إضافة هذه إلى أنفسنا بنشاط ، وأود أن أتحدث عن كيفية القيام بذلك.



أولاً ، أقدم نفسي - اسمي ألكسندر ، في السنوات الست الأخيرة التي خصصتُها لتطوير 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)
	}
}

ملاحظة: للحصول على البساطة ، ستكون معظم الشفرة في وحدة التحكم. أنت حر في استخدام نفس الأساليب في الهندسة المعمارية الخاصة بك. سيكون رمز المقالة متاحًا في المستودع ، وفي نهاية المقالة سيكون هذا الرابط أيضًا.

نحصل على الصورة التالية:



نظريا أكملنا المهمة. لكن هناك عدة أشياء واضحة على الفور:

  • لم نمنح الفرصة للتبديل بطريقة ما من سيناريو خاطئ إلى سيناريو ناجح. حسنًا في الحالة الحالية ، فإنه يخفي التنبيه - وهذا ليس حلاً
  • من وجهة نظر انطباع المستخدم ، يجب أن يكون النص أكثر وضوحًا ومحايدة. حتى لا يخاف المستخدم ولا يعمل لوضع نجمة واحدة في AppStore لتطبيقك. في هذه الحالة ، سيكون من المفيد لنا نص مفصل عند تصحيح الأخطاء
  • ولكي نكون صادقين - التنبيهات قديمة إلى حد ما كحل (على نحو متزايد ، تظهر شاشات وهمية أو نخب في التطبيقات). ولكن هذا سؤال يجب مناقشته بشكل منفصل مع الفريق

موافق ، الخيار المعروض أدناه يبدو أكثر تعاطفًا.



أيًا كان الخيار الذي تختاره ، فبالنسبة لأي منها ستحتاج إلى التفكير في مثل هذه الآلية لعرض رسالة تبدو رائعة عند حدوث خطأ تعسفي ، وسوف تقدم للمستخدم نصًا واضحًا لمزيد من العمل في التطبيق وتوفر مجموعة من الإجراءات. الحل هو:

  • يجب أن تكون قابلة للتوسيع. نحن نعلم جميعًا عن تقلب التصميم المتأصل. يجب أن تكون آليتنا جاهزة لأي شيء
  • تتم إضافته إلى الكائن (وإزالته) في سطرين من التعليمات البرمجية
  • اختبار جيد

ولكن قبل ذلك ، دعنا نغرق في الحد الأدنى النظري للأخطاء في Swift.

خطأ في Swift


هذه الفقرة هي نظرة عامة على أعلى مستوى للأخطاء بشكل عام. إذا كنت تستخدم بالفعل أخطائك بنشاط في التطبيق ، فيمكنك المتابعة بأمان إلى الفقرة التالية.

ما هو الخطأ؟ نوع من العمل الخاطئ أو نتيجة غير صحيحة. غالبًا ما يمكننا افتراض الأخطاء المحتملة ووصفها مسبقًا في التعليمات البرمجية.

في هذه الحالة ، تعطينا Apple نوع الخطأ. إذا فتحنا وثائق Apple ، فسيظهر Error بهذا الشكل (ذو صلة بـ Swift 5.1):

public protocol Error {
}

مجرد بروتوكول بدون متطلبات إضافية. توضح الوثائق بلطف - يسمح نقص المعلمات المطلوبة باستخدام أي نوع في نظام معالجة الأخطاء في Swift. مع هذا البروتوكول اللطيف ، سنعمل ببساطة.

تتبادر إلى ذهني فكرة استخدام التعداد على الفور: هناك عدد محدود معروف من الأخطاء ، قد يكون لديهم نوع من المعلمات. وهو ما تفعله شركة آبل. على سبيل المثال ، قد تفكر في تنفيذ خطأ فك الترميز:

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

استفد من أفضل ممارسات Apple. تخيل مجموعة من أخطاء الشبكة المحتملة في شكل مبسط:

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

الآن ، في أي مكان في تطبيقنا حيث يحدث الخطأ ، يمكننا استخدام شبكتنا.

كيف تعمل مع البق؟ هناك آلية الصيد. إذا كان بإمكان إحدى الوظائف أن تخطئ ، فسيتم تمييزها بكلمة الكلمات الأساسية. الآن مطلوب من كل من مستخدميه الوصول إليه من خلال بناء الصيد. إذا لم يكن هناك خطأ ، فسوف نقع في كتلة do ، مع وجود خطأ ، في block catch. يمكن أن تكون الوظائف التي تؤدي إلى الخطأ أي رقم في كتلة المهام. السلبية الوحيدة هي أنه في الصيد نحصل على خطأ من نوع Error. ستحتاج إلى إرسال الخطأ إلى النوع المطلوب.

كبديل ، يمكننا استخدام الخيار الاختياري ، أي الحصول على صفر في حالة حدوث خطأ والتخلص من التصميم الضخم. في بعض الأحيان يكون الأمر أكثر ملاءمة: دعنا نقول عندما نحصل على متغير اختياري ، ثم نطبق دالة الرميات عليه. يمكن وضع الرمز في كتلة if / guard واحدة ، وستظل موجزة.

فيما يلي مثال للعمل مع وظيفة الرميات:

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 ، تم تقديم النتيجة - تعدادًا يحتوي على خيارين - النجاح والفشل. في حد ذاته ، لا يتطلب استخدام الخطأ. وليس لها علاقة مباشرة بالتزامن. لكن العودة بدقة إلى هذا النوع إلى الاكتمال أكثر ملاءمة للأحداث غير المتزامنة (وإلا سيتعين عليك القيام باثنين من الإكمال والنجاح والفشل ، أو إرجاع معلمتين). لنكتب مثالاً:

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: [سلسلة] - خاصية تخزن خيارات الاسترداد
  • 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?)

ولكن بما أنه ليس لدينا خطأ في الحاضر ، فلا يمكننا سحبه.



عند هذه النقطة ، يبدو أننا حفرنا جثة بدلاً من كنز. سيتعين علينا تحويل الخطأ إلى NSError وكتابة وظيفتنا الخاصة لعرض التنبيه بواسطة التطبيق. حفنة من الاتصالات الضمنية. إنه أمر ممكن وصعب وغير واضح تمامًا - "لماذا؟".

أثناء تحضير فنجان الشاي التالي ، قد يتساءل المرء عن سبب استخدام الوظيفة أعلاه المفوض مثل أي وتجاوز المحدد. الجواب أدناه:

إجابة
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 ستفعل. خذ فكرة آبل ، فقط قم بتحديثها. نحتاج إلى كائن 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". مع الرسوم المتحركة الوامضة. ليس لطيفًا جدًا.



دعنا نحول الخطأ الفوري إلى عملية. الفكرة بسيطة - سنجعل الحد الأدنى من وقت الطلب. هنا يعتمد التنفيذ على أسلوبك في التواصل. لنفترض أنني أستخدم العملية ، ويبدو لي كما يلي:

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