Skip to content

Instantly share code, notes, and snippets.

@Aximem
Last active April 28, 2021 17:10
Show Gist options
  • Save Aximem/7bdd0150db6bd9e4e6157bd5196d48ee to your computer and use it in GitHub Desktop.
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
//
// 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