Last active
April 28, 2021 17:10
-
-
Save Aximem/7bdd0150db6bd9e4e6157bd5196d48ee to your computer and use it in GitHub Desktop.
Classe to manage safely requests / Store them if it generates an error and send back when connexion came back or when user choose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// DegradedMode.swift | |
// | |
// Created by Maxime Charruel on 25/07/2017. | |
// Copyright © 2017 Maxime Charruel. All rights reserved. | |
// | |
// DegradedMode allow you to sent safely requests and prevent error cases (e.g. timeout, connexion | |
// issues, ...). | |
// When a request failed, it is stored in UserDefault and send back it when user choose to or | |
// automatically when connexion come back | |
// Just call DegradedMode.shared.sendRequest to send safely requests | |
// And DegradedMode.shared.trySendRequestsNotSent if you want to force send back | |
import Alamofire | |
import SwiftyJSON | |
open class DegradedMode { | |
private var reachability: Reachability! | |
private var requestItemsToSend: [RequestItem] = [] | |
/// Singleton | |
public static let shared : DegradedMode = { | |
let instance = DegradedMode.init() | |
return instance | |
}() | |
/// On init, get userDefaults requests not sended | |
public required init() { | |
let userDefaults = UserDefaults.standard | |
if userDefaults.object(forKey: "requestItemsToSend") != nil { | |
let decodedRequestItemsToSend = userDefaults.object(forKey: "requestItemsToSend") as! Data | |
requestItemsToSend = NSKeyedUnarchiver.unarchiveObject(with: decodedRequestItemsToSend) as! [RequestItem] | |
} | |
} | |
/// Save requests not sended in userDefaults | |
open func saveRequestItemsToSend () { | |
let userDefaults = UserDefaults.standard | |
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: DegradedMode.shared.requestItemsToSend) | |
userDefaults.set(encodedData, forKey: "requestItemsToSend") | |
userDefaults.synchronize() | |
} | |
/// Start Observing Network changes | |
open func startObservingNetwork () { | |
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged), name: ReachabilityChangedNotification, object: nil) | |
self.reachability = Reachability.init() | |
do { | |
try self.reachability.startNotifier() | |
} catch { | |
print("Couldn't start observing, something went wrong") | |
} | |
} | |
/// On Network changes | |
/// | |
/// - Parameter notification: NotificationCenter notification | |
@objc func reachabilityChanged(notification: Notification) { | |
let reachability = notification.object as! Reachability | |
if reachability.isReachable { | |
print("Network reachable") | |
trySendRequestsNotSent() | |
} else { | |
print("Network not reachable") | |
} | |
} | |
/// Method called when you want to send a request, prevents its fail and store it. If request | |
/// doesn't fail, it does nothing. | |
/// You should call this method everytime you want to send a request, it will does the job | |
/// | |
/// - Parameters: | |
/// - requestItem: your request formatted with Object RequestItem | |
/// - completionHandler: return JSONData answer and error (if there is) | |
open func sendRequest (requestItem: RequestItem, completionHandler: @escaping (JSON, Error?) -> ()) { | |
Alamofire.request(requestItem.urlToCall, method: requestItem.method, parameters: requestItem.params, headers: requestItem.headers).responseJSON { response in | |
switch response.result { | |
case .success: | |
// Do nothing | |
break | |
case .failure: | |
// Something went wrong, request added to requests to send later | |
self.addRequestItemToSendLater(requestItem: requestItem) | |
break | |
} | |
completionHandler(JSON(data: response.data!), response.result.error!) | |
} | |
APIController.shared.sendRequest(requestItem: requestItem, completionHandler: { jsonData, error in | |
if error != nil { | |
// Something went wrong, request added to requests to send later | |
self.addRequestItemToSendLater(requestItem: requestItem) | |
} | |
completionHandler(jsonData, error) | |
}) | |
} | |
/// Private method used to store not sended request and add it to UserDefaults Session | |
/// | |
/// - Parameter requestItem: your request formatted with Object RequestItem | |
private func addRequestItemToSendLater (requestItem: RequestItem) { | |
requestItemsToSend.append(requestItem) | |
saveRequestItemsToSend() | |
} | |
/// Method to send not sended requests, this method is automatically called when reachilibity | |
/// changed but you can force send back by calling this method | |
/// | |
/// - Returns: Array of Error, for requests which still have not been sent | |
public func trySendRequestsNotSent (trySendRequestsNotSentCompletionHandler: @escaping ([Error]) -> () = { _ in }) { | |
// If there is requests | |
if !requestItemsToSend.isEmpty { | |
let requestItemsToSendCopy = requestItemsToSend | |
NSLog("Send list started") | |
launchRequestsInOrder(requestItemsToSendCopy, 0, [], launchRequestsInOrderCompletionBlock: { index, errors in | |
trySendRequestsNotSentCompletionHandler(errors) | |
}) | |
} | |
else { | |
trySendRequestsNotSentCompletionHandler([]) | |
} | |
} | |
/// Private method to launch requests (still not sent !) in the same order there were stored | |
/// | |
/// - Parameters: | |
/// - requestItemsToSend: Array of RequestItem to send | |
/// - index: the current index of request sent | |
/// - errors: Array of error, updated by each request if there is an error | |
/// - launchRequestsInOrderCompletionBlock: Completion executed when launch is done | |
private func launchRequestsInOrder (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], launchRequestsInOrderCompletionBlock: @escaping (_ index: Int, _ errors: [Error] ) -> Void) { | |
executeRequest(requestItemsToSend, index, errors, executeRequestCompletionBlock: { currentIndex, errors in | |
if currentIndex < requestItemsToSend.count { | |
// We didn't reach last request, launch next request | |
self.launchRequestsInOrder(requestItemsToSend, currentIndex, errors, launchRequestsInOrderCompletionBlock: { index, errors in | |
launchRequestsInOrderCompletionBlock(currentIndex, errors) | |
}) | |
} | |
else { | |
// We parse and send all requests | |
NSLog("Send list finished") | |
launchRequestsInOrderCompletionBlock(currentIndex, errors) | |
} | |
}) | |
} | |
/// Private method called to send a specific request | |
/// | |
/// - Parameters: | |
/// - requestItemsToSend: your request formatted with Object RequestItem | |
/// - index: the current index of request sent | |
/// - errors: Array of errors | |
/// - executeRequestCompletionBlock: Completion executed when request has been sent | |
private func executeRequest (_ requestItemsToSend: [RequestItem], _ index: Int, _ errors: [Error], executeRequestCompletionBlock: @escaping (_ index: Int, _ errors: [Error]) -> Void) { | |
NSLog("Send request %d", index) | |
Alamofire.request(requestItemsToSend[index].urlToCall, method: requestItemsToSend[index].method, parameters: requestItemsToSend[index].params, headers: requestItemsToSend[index].headers).responseJSON { response in | |
var errors: [Error] = errors | |
switch response.result { | |
case .success: | |
// Request sended successfully, we can remove it from not sended request array | |
self.requestItemsToSend.remove(at: index) | |
break | |
case .failure: | |
// Still not send we append arror | |
errors.append(response.result.error!) | |
break | |
} | |
NSLog("Receive request %d", index) | |
executeRequestCompletionBlock(index+1, errors) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Copyright (c) 2014, Ashley Mills | |
All rights reserved. | |
Redistribution and use in source and binary forms, with or without | |
modification, are permitted provided that the following conditions are met: | |
1. Redistributions of source code must retain the above copyright notice, this | |
list of conditions and the following disclaimer. | |
2. Redistributions in binary form must reproduce the above copyright notice, | |
this list of conditions and the following disclaimer in the documentation | |
and/or other materials provided with the distribution. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
POSSIBILITY OF SUCH DAMAGE. | |
*/ | |
import SystemConfiguration | |
import Foundation | |
public enum ReachabilityError: Error { | |
case FailedToCreateWithAddress(sockaddr_in) | |
case FailedToCreateWithHostname(String) | |
case UnableToSetCallback | |
case UnableToSetDispatchQueue | |
} | |
public let ReachabilityChangedNotification = NSNotification.Name("ReachabilityChangedNotification") | |
func callback(reachability:SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutableRawPointer?) { | |
guard let info = info else { return } | |
let reachability = Unmanaged<Reachability>.fromOpaque(info).takeUnretainedValue() | |
DispatchQueue.main.async { | |
reachability.reachabilityChanged() | |
} | |
} | |
public class Reachability { | |
public typealias NetworkReachable = (Reachability) -> () | |
public typealias NetworkUnreachable = (Reachability) -> () | |
public enum NetworkStatus: CustomStringConvertible { | |
case notReachable, reachableViaWiFi, reachableViaWWAN | |
public var description: String { | |
switch self { | |
case .reachableViaWWAN: return "Cellular" | |
case .reachableViaWiFi: return "WiFi" | |
case .notReachable: return "No Connection" | |
} | |
} | |
} | |
public var whenReachable: NetworkReachable? | |
public var whenUnreachable: NetworkUnreachable? | |
public var reachableOnWWAN: Bool | |
// The notification center on which "reachability changed" events are being posted | |
public var notificationCenter: NotificationCenter = NotificationCenter.default | |
public var currentReachabilityString: String { | |
return "\(currentReachabilityStatus)" | |
} | |
public var currentReachabilityStatus: NetworkStatus { | |
guard isReachable else { return .notReachable } | |
if isReachableViaWiFi { | |
return .reachableViaWiFi | |
} | |
if isRunningOnDevice { | |
return .reachableViaWWAN | |
} | |
return .notReachable | |
} | |
fileprivate var previousFlags: SCNetworkReachabilityFlags? | |
fileprivate var isRunningOnDevice: Bool = { | |
#if (arch(i386) || arch(x86_64)) && os(iOS) | |
return false | |
#else | |
return true | |
#endif | |
}() | |
fileprivate var notifierRunning = false | |
fileprivate var reachabilityRef: SCNetworkReachability? | |
fileprivate let reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability") | |
required public init(reachabilityRef: SCNetworkReachability) { | |
reachableOnWWAN = true | |
self.reachabilityRef = reachabilityRef | |
} | |
public convenience init?(hostname: String) { | |
guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { return nil } | |
self.init(reachabilityRef: ref) | |
} | |
public convenience init?() { | |
var zeroAddress = sockaddr() | |
zeroAddress.sa_len = UInt8(MemoryLayout<sockaddr>.size) | |
zeroAddress.sa_family = sa_family_t(AF_INET) | |
guard let ref: SCNetworkReachability = withUnsafePointer(to: &zeroAddress, { | |
SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) | |
}) else { return nil } | |
self.init(reachabilityRef: ref) | |
} | |
deinit { | |
stopNotifier() | |
reachabilityRef = nil | |
whenReachable = nil | |
whenUnreachable = nil | |
} | |
} | |
public extension Reachability { | |
// MARK: - *** Notifier methods *** | |
func startNotifier() throws { | |
guard let reachabilityRef = reachabilityRef, !notifierRunning else { return } | |
var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) | |
context.info = UnsafeMutableRawPointer(Unmanaged<Reachability>.passUnretained(self).toOpaque()) | |
if !SCNetworkReachabilitySetCallback(reachabilityRef, callback, &context) { | |
stopNotifier() | |
throw ReachabilityError.UnableToSetCallback | |
} | |
if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef, reachabilitySerialQueue) { | |
stopNotifier() | |
throw ReachabilityError.UnableToSetDispatchQueue | |
} | |
// Perform an initial check | |
reachabilitySerialQueue.async { | |
self.reachabilityChanged() | |
} | |
notifierRunning = true | |
} | |
func stopNotifier() { | |
defer { notifierRunning = false } | |
guard let reachabilityRef = reachabilityRef else { return } | |
SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil) | |
SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil) | |
} | |
// MARK: - *** Connection test methods *** | |
var isReachable: Bool { | |
guard isReachableFlagSet else { return false } | |
if isConnectionRequiredAndTransientFlagSet { | |
return false | |
} | |
if isRunningOnDevice { | |
if isOnWWANFlagSet && !reachableOnWWAN { | |
// We don't want to connect when on 3G. | |
return false | |
} | |
} | |
return true | |
} | |
var isReachableViaWWAN: Bool { | |
// Check we're not on the simulator, we're REACHABLE and check we're on WWAN | |
return isRunningOnDevice && isReachableFlagSet && isOnWWANFlagSet | |
} | |
var isReachableViaWiFi: Bool { | |
// Check we're reachable | |
guard isReachableFlagSet else { return false } | |
// If reachable we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi | |
guard isRunningOnDevice else { return true } | |
// Check we're NOT on WWAN | |
return !isOnWWANFlagSet | |
} | |
var description: String { | |
let W = isRunningOnDevice ? (isOnWWANFlagSet ? "W" : "-") : "X" | |
let R = isReachableFlagSet ? "R" : "-" | |
let c = isConnectionRequiredFlagSet ? "c" : "-" | |
let t = isTransientConnectionFlagSet ? "t" : "-" | |
let i = isInterventionRequiredFlagSet ? "i" : "-" | |
let C = isConnectionOnTrafficFlagSet ? "C" : "-" | |
let D = isConnectionOnDemandFlagSet ? "D" : "-" | |
let l = isLocalAddressFlagSet ? "l" : "-" | |
let d = isDirectFlagSet ? "d" : "-" | |
return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" | |
} | |
} | |
fileprivate extension Reachability { | |
func reachabilityChanged() { | |
let flags = reachabilityFlags | |
guard previousFlags != flags else { return } | |
let block = isReachable ? whenReachable : whenUnreachable | |
block?(self) | |
self.notificationCenter.post(name: ReachabilityChangedNotification, object:self) | |
previousFlags = flags | |
} | |
var isOnWWANFlagSet: Bool { | |
#if os(iOS) | |
return reachabilityFlags.contains(.isWWAN) | |
#else | |
return false | |
#endif | |
} | |
var isReachableFlagSet: Bool { | |
return reachabilityFlags.contains(.reachable) | |
} | |
var isConnectionRequiredFlagSet: Bool { | |
return reachabilityFlags.contains(.connectionRequired) | |
} | |
var isInterventionRequiredFlagSet: Bool { | |
return reachabilityFlags.contains(.interventionRequired) | |
} | |
var isConnectionOnTrafficFlagSet: Bool { | |
return reachabilityFlags.contains(.connectionOnTraffic) | |
} | |
var isConnectionOnDemandFlagSet: Bool { | |
return reachabilityFlags.contains(.connectionOnDemand) | |
} | |
var isConnectionOnTrafficOrDemandFlagSet: Bool { | |
return !reachabilityFlags.intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty | |
} | |
var isTransientConnectionFlagSet: Bool { | |
return reachabilityFlags.contains(.transientConnection) | |
} | |
var isLocalAddressFlagSet: Bool { | |
return reachabilityFlags.contains(.isLocalAddress) | |
} | |
var isDirectFlagSet: Bool { | |
return reachabilityFlags.contains(.isDirect) | |
} | |
var isConnectionRequiredAndTransientFlagSet: Bool { | |
return reachabilityFlags.intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection] | |
} | |
var reachabilityFlags: SCNetworkReachabilityFlags { | |
guard let reachabilityRef = reachabilityRef else { return SCNetworkReachabilityFlags() } | |
var flags = SCNetworkReachabilityFlags() | |
let gotFlags = withUnsafeMutablePointer(to: &flags) { | |
SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0)) | |
} | |
if gotFlags { | |
return flags | |
} else { | |
return SCNetworkReachabilityFlags() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// RequestItem.swift | |
// | |
// Created by Maxime Charruel on 01/09/2017. | |
// Copyright © 2017 Acheteza. All rights reserved. | |
// | |
import Alamofire | |
public class RequestItem: NSObject, NSCoding { | |
public var url: String = "" | |
public var method: HTTPMethod = .get | |
public var params: [String: String] = [:] | |
public var headers: [String: String] = [:] | |
/// Initializer | |
/// | |
/// - Parameters: | |
/// - url: url to call | |
/// - method: method .get / .post / ... | |
/// - params: parameters | |
/// - headers: headers | |
public convenience init (url: String, method: HTTPMethod, params: [String: String], headers: [String: String]) { | |
self.init() | |
self.url = url | |
self.method = method | |
self.params = params | |
self.headers = headers | |
} | |
/// Decode requests from UserDefaults | |
/// | |
/// - Parameter aDecoder: coder | |
required convenience public init(coder aDecoder: NSCoder) { | |
self.init() | |
url = aDecoder.decodeObject(forKey: "url") as! String | |
method = HTTPMethod(rawValue: aDecoder.decodeObject(forKey: "method") as! String)! | |
params = aDecoder.decodeObject(forKey: "params") as! [String: String] | |
headers = aDecoder.decodeObject(forKey: "headers") as! [String: String] | |
} | |
/// Encode requests from UserDefaults | |
/// | |
/// - Parameter aCoder: coder | |
open func encode(with aCoder: NSCoder) { | |
aCoder.encode(url, forKey: "url") | |
aCoder.encode(method.rawValue, forKey: "method") | |
aCoder.encode(params, forKey: "params") | |
aCoder.encode(params, forKey: "headers") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment