कार्यों से त्रुटि के प्रति सतर्कता से

हेलो, हेब्र! उपयोगकर्ता के लिए, त्रुटि संदेश अक्सर "कुछ गलत है, AAAA!" जैसा दिखता है। बेशक, वह जादू की त्रुटि "यह सब ठीक करें" को देखने के लिए गलतियों के बजाय चाहेंगे। खैर, या अन्य विकल्प। हमने इन्हें सक्रिय रूप से खुद से जोड़ना शुरू किया, और मैं इस बारे में बात करना चाहता हूं कि आप यह कैसे कर सकते हैं।



सबसे पहले, अपना परिचय दें - मेरा नाम अलेक्जेंडर है, पिछले छह वर्षों में मैंने आईओएस विकास को समर्पित किया है। अब मैं ManyChat मोबाइल एप्लिकेशन के लिए जिम्मेदार हूं और मैं उनके उदाहरण का उपयोग करके समस्याओं का समाधान करूंगा।

चलो तुरंत तैयार करते हैं कि हम क्या करेंगे:

  • त्रुटि प्रकार में कार्यक्षमता जोड़ें
  • त्रुटियों को उपयोगकर्ता के अनुकूल अलर्ट में बदल दें
  • हम इंटरफ़ेस में संभावित आगे की कार्रवाई प्रदर्शित करते हैं और उनके क्लिक को संसाधित करते हैं

और यह सब स्विफ्ट पर होगा :)

हम एक उदाहरण के साथ समस्या को हल करेंगे। सर्वर ने अपेक्षित 200 के बजाय कोड 500 के साथ एक त्रुटि लौटा दी। डेवलपर को क्या करना चाहिए? बहुत कम से कम, उदासी के साथ उपयोगकर्ता को सूचित करने के लिए - जवानों के साथ अपेक्षित पोस्ट डाउनलोड नहीं किया जा सका। 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 में आपके आवेदन में एक स्टार लगाने के लिए न चले। इस मामले में, डीबग करते समय एक विस्तृत पाठ हमारे लिए उपयोगी होगा
  • और, ईमानदार होने के लिए - अलर्ट कुछ हद तक समाधान के रूप में पुराना है (तेजी से, डमी स्क्रीन या अनुप्रयोगों में टोस्ट दिखाई देते हैं)। लेकिन यह पहले से ही एक सवाल है जिस पर टीम के साथ अलग से चर्चा की जानी चाहिए

सहमति दें, नीचे प्रस्तुत विकल्प बहुत अधिक सहानुभूतिपूर्ण लगता है।



आप जो भी विकल्प चुनते हैं, उनमें से किसी के लिए आपको एक संदेश प्रदर्शित करने के लिए इस तरह के तंत्र को सोचने की आवश्यकता होगी जो एक मनमानी त्रुटि होने पर बहुत अच्छा लगेगा, उपयोगकर्ता को आवेदन में आगे के काम के लिए एक स्पष्ट स्क्रिप्ट प्रदान करेगा और कार्यों का एक सेट प्रदान करेगा। समाधान है:

  • एक्स्टेंसिबल होना चाहिए। हम सभी निहित डिजाइन परिवर्तनशीलता के बारे में जानते हैं। हमारा तंत्र किसी भी चीज के लिए तैयार होना चाहिए
  • इसे कोड की कुछ पंक्तियों में ऑब्जेक्ट (और हटा दिया गया) में जोड़ा जाता है
  • अच्छी तरह से परीक्षण किया

लेकिन इससे पहले, चलो स्विफ्ट में त्रुटियों के लिए सैद्धांतिक न्यूनतम में डुबकी लगाते हैं।

स्विफ्ट में त्रुटि


यह पैराग्राफ सामान्य रूप से त्रुटियों का शीर्ष-स्तरीय अवलोकन है। यदि आप पहले से ही एप्लिकेशन में अपनी गलतियों का उपयोग कर रहे हैं, तो आप सुरक्षित रूप से अगले पैराग्राफ में आगे बढ़ सकते हैं।

गलती क्या है? किसी प्रकार की गलत क्रिया या गलत परिणाम। अक्सर हम संभावित त्रुटियों को मान सकते हैं और कोड में उन्हें पहले से वर्णित कर सकते हैं।

इस मामले के लिए, Apple हमें त्रुटि देता है। यदि हम Apple प्रलेखन खोलते हैं, तो त्रुटि इस तरह दिखाई देगी (स्विफ्ट 5.1 के लिए प्रासंगिक):

public protocol Error {
}

अतिरिक्त आवश्यकताओं के बिना बस एक प्रोटोकॉल। प्रलेखन कृपया समझाता है - आवश्यक मापदंडों की कमी स्विफ्ट त्रुटि हैंडलिंग प्रणाली में किसी भी प्रकार का उपयोग करने की अनुमति देती है। ऐसे कोमल प्रोटोकॉल के साथ, हम बस काम करेंगे।

तुरंत एनम का उपयोग करने का विचार मेरे दिमाग में आता है: त्रुटियों की एक सीमित ज्ञात संख्या है, उनके कुछ पैरामीटर हो सकते हैं। जो कि Apple कर रहा है। उदाहरण के लिए, आप डिकोडरिंग लागू करने पर विचार कर सकते हैं:

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
}

अब, हमारे एप्लिकेशन में कहीं भी जहां त्रुटि होती है, हम अपने Network.Error का उपयोग कर सकते हैं।

कीड़े के साथ कैसे काम करें? एक कैच मैकेनिज्म है। यदि कोई फ़ंक्शन किसी त्रुटि को फेंक सकता है, तो यह थ्रो कीवर्ड के साथ चिह्नित है। अब इसके प्रत्येक उपयोगकर्ता को डू कैच कंस्ट्रक्शन के माध्यम से इसे एक्सेस करना आवश्यक है। यदि कोई त्रुटि नहीं है, तो हम त्रुटि ब्लॉक के साथ कैच ब्लॉक में पड़ेंगे। त्रुटि करने के लिए अग्रणी कार्य किसी भी संख्या में ब्लॉक हो सकते हैं। केवल नकारात्मक यह है कि पकड़ने में हमें टाइप एरर की त्रुटि मिलती है। आपको वांछित प्रकार में त्रुटि डालने की आवश्यकता होगी।

एक विकल्प के रूप में, हम वैकल्पिक का उपयोग कर सकते हैं, अर्थात्, त्रुटि के मामले में शून्य प्राप्त करें और भारी डिजाइन से छुटकारा पाएं। कभी-कभी यह अधिक सुविधाजनक होता है: मान लें कि जब हमें एक वैकल्पिक चर मिलता है, और फिर उस पर एक थ्रू फ़ंक्शन लागू होता है। कोड को एक / गार्ड ब्लॉक में रखा जा सकता है, और यह संक्षिप्त रहेगा।

यहाँ थ्रो फंक्शन के साथ काम करने का एक उदाहरण है:

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

पुनश्च अन्य भाषाओं में पकड़ के साथ भ्रमित मत करो। स्विफ्ट एक अपवाद नहीं फेंकती है, लेकिन एक विशेष रजिस्टर में त्रुटि का मान लिखती है (यदि ऐसा हुआ)। यदि कोई मान है, तो यह त्रुटि ब्लॉक में जाता है, यदि नहीं, तो ब्लॉक जारी रहता है। सबसे जिज्ञासु के लिए स्रोत: www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html

यह विधि तुल्यकालिक घटनाओं से निपटने के लिए अच्छा है और लंबे ऑपरेशन के लिए इतना सुविधाजनक नहीं है (उदाहरण के लिए) नेटवर्क पर डेटा का अनुरोध करना), जो संभावित रूप से समय लेने वाला हो सकता है। तब आप सरल पूर्णता का उपयोग कर सकते हैं।

स्विफ्ट 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)
	}
})

यह जानकारी हमारे लिए काम करने के लिए काफी है।

एक बार फिर, संक्षेप में:

  • स्विफ्ट में त्रुटियां एक प्रोटोकॉल है
  • एनम के रूप में त्रुटियों को प्रस्तुत करना सुविधाजनक है
  • त्रुटियों से निपटने के दो तरीके हैं - सिंक्रोनस (कैच) और एसिंक्रोनस (आपका खुद का कॉम्पिटिशन या रिजल्ट)

त्रुटि पाठ


आइए लेख के विषय पर वापस जाएं। ऊपर के पैराग्राफ में, हमने अपनी खुद की प्रकार की त्रुटियां बनाईं। वह वहाँ है:

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

अब हमें एक पाठ के साथ प्रत्येक त्रुटि का मिलान करना होगा जो उपयोगकर्ता के लिए समझ में आएगा। हम त्रुटि के मामले में इसे इंटरफ़ेस में प्रदर्शित करेंगे। LocalizedError प्रोटोकॉल हमारी मदद करने के लिए hurries। यह प्रोटोकॉल त्रुटि प्राप्त करता है और इसे 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
}

सिर तुरंत आपकी बाइक का आरेख खींचता है। लेकिन पहले, आइए एप्पल डॉक की जांच करें। शायद तंत्र का हिस्सा पहले से ही हमारे हाथ में है।

मूल कार्यान्वयन?


इंटरनेट खोज के एक बिट के परिणामस्वरूप प्रोटोकॉल पुनर्प्राप्त करने योग्य होगा :

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

ऐसा लगता है कि हम देख रहे हैं:

  • पुनर्प्राप्ति: [स्ट्रिंग] - वसूली विकल्पों को संग्रहीत करने वाली संपत्ति
  • func tryRecovery (optionIndex: Int) -> बूल - एक त्रुटि से, तुल्यकालिक रूप से पुनर्स्थापित करता है। सच - सफलता पर
  • func tryRecovery (optionIndex: Int, resultHandler: (बूल) -> शून्य) - अतुल्यकालिक विकल्प, विचार एक ही है

उपयोग गाइड के साथ, सब कुछ अधिक विनम्र है। ऐप्पल साइट और आसपास के क्षेत्र पर एक छोटी सी खोज में स्विफ्ट की सार्वजनिक घोषणाओं से पहले लिखे गए त्रुटि से निपटने पर एक लेख आता है

संक्षेप में:

  • MacOs अनुप्रयोगों के लिए तंत्र पर विचार किया जाता है और एक संवाद बॉक्स दिखाता है
  • यह मूल रूप से NSError के आसपास बनाया गया था।
  • RecoveryAttempter ऑब्जेक्ट userInfo में त्रुटि के अंदर समझाया गया है, जो त्रुटि की स्थितियों के बारे में जानता है और समस्या का सबसे अच्छा समाधान चुन सकता है। वस्तु शून्य नहीं होनी चाहिए
  • RecoveryAttempter को अनौपचारिक प्रोटोकॉल NSErrorRecoveryAttempting का समर्थन करना चाहिए
  • इसके अलावा userInfo में पुनर्प्राप्ति विकल्प होना चाहिए
  • और सब कुछ presentError विधि को कॉल करने के लिए बंधा हुआ है, जो केवल macOS एसडीके में है। वह एक अलर्ट दिखाता है
  • यदि चेतावनी को 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)
	}
}

सूचकांक निर्भरता सबसे सुविधाजनक समाधान नहीं है (हम आसानी से सरणी से परे जा सकते हैं और एप्लिकेशन को क्रैश कर सकते हैं)। लेकिन एमवीपी के लिए करेंगे। Apple का विचार लें, बस इसे आधुनिक करें। हमें एक अलग एट्रैम्प्टर ऑब्जेक्ट और बटन विकल्प की आवश्यकता है जो हम इसे देंगे:

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

हमें एमवीपी समाधान मिला है, और हमारे लिए इसे हमारे आवेदन में कहीं भी कनेक्ट और कॉल करना मुश्किल नहीं होगा। आइए किनारे के मामलों और मापनीयता की जांच शुरू करें।

यदि हमारे पास कई एक्ज़िट परिदृश्य हैं, तो क्या होगा?


मान लीजिए कि हमारे आवेदन में एक उपयोगकर्ता का भंडार है। तिजोरी की एक जगह सीमा है। इस मामले में, उपयोगकर्ता के पास त्रुटि से बाहर निकलने के लिए दो परिदृश्य हैं: उपयोगकर्ता या तो स्थान खाली कर सकता है या अधिक खरीद सकता है। हम निम्नलिखित कोड लिखेंगे:

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



उपस्थिति में, कुछ भी नहीं बदला है, लेकिन अब हमने दृश्य नियंत्रकों के पदानुक्रम से छुटकारा पा लिया है। मैं अत्यधिक इस अवसर के साथ नहीं ले जाने की सलाह देता हूं। राउटर या एंटिटी में डिस्प्ले कोड को समान अधिकारों के साथ कॉल करना बेहतर है। पारदर्शिता और स्पष्टता के नाम पर।

हमने उपयोगकर्ताओं को खराबी, रखरखाव, आदि के दौरान स्पैमिंग सर्वर के लिए एक महान उपकरण दिया। हम क्या सुधार कर सकते हैं?

न्यूनतम अनुरोध समय


मान लीजिए हम इंटरनेट बंद कर देते हैं और फिर से प्रयास करते हैं। लोडर चलाएं। उत्तर तुरंत आएगा और एक मिनी-गेम "क्लिकर" मिलेगा। ब्लिंकिंग एनीमेशन के साथ। बहुत अच्छा नहीं है।



आइए एक प्रक्रिया में तत्काल त्रुटि को चालू करें। विचार सरल है - हम न्यूनतम अनुरोध समय करेंगे। यहां कार्यान्वयन नेटवर्किंग के आपके दृष्टिकोण पर निर्भर करता है। मान लीजिए कि मैं ऑपरेशन का उपयोग करता हूं, और मेरे लिए यह इस तरह दिखता है:

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

अब हमारा एनीमेशन ऑफ़लाइन होने पर भी इतना तेज नहीं लगेगा। मैं एनीमेशन के साथ अधिकांश स्थानों में इस दृष्टिकोण का उपयोग करने की सलाह देता हूं।



हवाई जहाज मोड के लिए, अलर्ट प्रॉम्प्ट दिखाना अच्छा है (उपयोगकर्ता एप्लिकेशन के साथ काम करना शुरू करने के लिए मोड को बंद करना भूल सकता है)। जैसा कि, कहते हैं, एक तार बनाता है। और महत्वपूर्ण प्रश्नों के लिए, अलर्ट दिखाने से पहले हुड के नीचे कई बार दोहराना अच्छा है ... लेकिन उस समय के बारे में अधिक :)

testability


जब सभी लॉजिक व्यूकंट्रोलर में डंप हो जाते हैं (जैसा कि हमारे पास अभी है), परीक्षण करना मुश्किल है। हालाँकि, यदि आपका दृष्टिकोण व्यापार तर्क के साथ साझा किया जाता है, तो परीक्षण एक तुच्छ कार्य बन जाता है। हाथ पैंट की एक चपलता के साथ , व्यापार तर्क में बदल जाता है:

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