Classe to manage safely requests / Store them if it generates an error and send back when connexion came back or when user choose
// | |
// 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) | |
} | |
} | |
} |
/* | |
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() | |
} | |
} | |
} |
// | |
// 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