Cache Handling with Moya - swift

We have implemented Moya, RxSwift And Alamofire as pods in our project.
Does anyone know how you gain control of the cache policies per url request using this tech?
I have read through quite a few of the issues on Moya's GitHub page but still can't find anything. Also tried using actual json response stored as files for the sampleData like so:
var sampleData: Data {
guard let path = Bundle.main.path(forResource: "SampleAggregate", ofType: "txt") else {
return "sampleData".utf8Encoded
}
let sample = try? String(contentsOfFile: path, encoding: String.Encoding.utf8)
return sample!.utf8Encoded
}
Thanks in advance for any pro tips :)

As to my understanding, the "cleanest" way to solve this, is to use a custom Moya Plugin. Here's an implementation:
protocol CachePolicyGettable {
var cachePolicy: URLRequest.CachePolicy { get }
}
final class CachePolicyPlugin: PluginType {
public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
if let cachePolicyGettable = target as? CachePolicyGettable {
var mutableRequest = request
mutableRequest.cachePolicy = cachePolicyGettable.cachePolicy
return mutableRequest
}
return request
}
}
To actually use this plugin, there are two required steps left:
The plugin should be added to your Moya provider like this:
let moyaProvider = MoyaProvider<YourMoyaTarget>(plugins: [CachePolicyPlugin()])
YourMoyaTargetshould conform to CachePolicyGettable and thereby define the cache policy for each target:
extension YourMoyaTarget: CachePolicyGettable {
var cachePolicy: URLRequest.CachePolicy {
switch self {
case .sampleTarget, .someOtherSampleTarget:
return .reloadIgnoringLocalCacheData
default:
return .useProtocolCachePolicy
}
}
}
Note: This approach uses a protocol to associate cache policies with target types; one could also realise this via a closure being passed to the plugin. Such closure would then decide which cache policy to use depending on the target type passed as an input parameter to the closure.

Based on #fredpi answer, I slightly improved caching plugin for Moya. Below is my version:
import Foundation
import Moya
protocol CachePolicyGettable {
var cachePolicy: URLRequest.CachePolicy { get }
}
final class NetworkDataCachingPlugin: PluginType {
init (configuration: URLSessionConfiguration, inMemoryCapacity: Int, diskCapacity: Int, diskPath: String?) {
configuration.urlCache = URLCache(memoryCapacity: inMemoryCapacity, diskCapacity: diskCapacity, diskPath: diskPath)
}
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
if let cacheableTarget = target as? CachePolicyGettable {
var mutableRequest = request
mutableRequest.cachePolicy = cacheableTarget.cachePolicy
return mutableRequest
}
return request
}
}
extension NetworkApiService: CachePolicyGettable {
var cachePolicy: URLRequest.CachePolicy {
switch self {
case .getUserProfile:
return .returnCacheDataElseLoad
default:
return .useProtocolCachePolicy
}
}
}
In order to clear the cache, you need to have an access to urlRequest object/objects. How to retrieve an urlRequest for Moya route you can find in the following topic.
To clear the cache you can use following code:
public func clearCache(urlRequests: [URLRequest] = []) {
let provider = ... // your Moya provider
guard let urlCache = provider.manager.session.configuration.urlCache else { return }
if urlRequests.isEmpty {
urlCache.removeAllCachedResponses()
} else {
urlRequests.forEach { urlCache.removeCachedResponse(for: $0) }
}
}

Subclass MoyaProvider and compose requestClosure.
It should look something like:
final class MyProvider<Target: TargetType>: MoyaProvider<Target> {
public init(
endpointClosure: #escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
requestClosure: #escaping RequestClosure = MoyaProvider.defaultRequestMapping,
stubClosure: #escaping StubClosure = MoyaProvider.neverStub,
manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
plugins: [PluginType] = [],
trackInflights: Bool = false
) {
super.init(
endpointClosure: endpointClosure,
requestClosure: { endpoint, closure in
var request = try! endpoint.urlRequest() //Feel free to embed proper error handling
if request.url == URL(string: "http://google.com")! {
request.cachePolicy = .returnCacheDataDontLoad
} else {
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
}
closure(.success(request))
},
stubClosure: stubClosure,
manager: manager,
plugins: plugins,
trackInflights: trackInflights
)
}
}

If you want to disable the stored cookies as well
request.httpShouldHandleCookies = false

Related

Alamofire: How to conditionally change cache policy based on network status?

The end result I would like to achieve is to use cached data when the network is unavailable and load data from the server when the network is available.
The closest thing I've found is this thread, but I am still having trouble getting it to work using Alamofire.
How to cache response in iOS and show only when internet is not available?
I have two classes, one to check the network status and another that configures an Alamofire url session. Detecting the network status seems to be working, but it's not changing the session configuration. What am I doing wrong? Thanks!
NetworkReachability.shared.startNetworkMonitoring() is run when the app loads in didFinishLaunchingWithOptions.
class NetworkReachability {
static let shared = NetworkReachability()
let reachabilityManager = NetworkReachabilityManager(host: "www.google.com")
func startNetworkMonitoring() {
reachabilityManager?.startListening(onUpdatePerforming: { status in
switch status {
case .notReachable:
ApiManager.configuration.requestCachePolicy = .returnCacheDataDontLoad
case .reachable(.cellular):
ApiManager.configuration.requestCachePolicy = .reloadIgnoringCacheData
case .reachable(.ethernetOrWiFi):
ApiManager.configuration.requestCachePolicy = .reloadIgnoringCacheData
case .unknown:
print("Unknown network state")
}
})
}
}
A custom api manager to configure the cache.
class ApiManager {
static let shared = ApiManager()
static let configuration = URLSessionConfiguration.af.default
public let sessionManager: Alamofire.Session = {
let responseCacher = ResponseCacher(behavior: .modify { _, response in
let userInfo = ["date": Date()]
return CachedURLResponse(
response: response.response,
data: response.data,
userInfo: userInfo,
storagePolicy: .allowed)
})
return Session(
configuration: configuration,
cachedResponseHandler: responseCacher)
}()
}
I'm writing my APIClient here which I've written using Alamofire.
import Alamofire
class NetworkLogger: EventMonitor {
let queue = DispatchQueue(label: "com.ketan.almofire.networklogger")
func requestDidFinish(_ request: Request) {
print(request.description)
}
func request<Value>(
_ request: DataRequest,
didParseResponse response: DataResponse<Value, AFError>
) {
guard let data = response.data else {
return
}
if let json = try? JSONSerialization
.jsonObject(with: data, options: .mutableContainers) {
print(json)
}
}
}
class APIClient {
private var manager = Session()
init() {
configureSession()
}
private func manageCachePolicy() {
if NetworkReachabilityManager()?.isReachable ?? false {
manager.sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
} else {
manager.sessionConfiguration.requestCachePolicy = .returnCacheDataElseLoad
}
}
private func configureSession() {
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
if NetworkReachabilityManager()?.isReachable ?? false {
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
}
manager = Session(configuration: configuration, eventMonitors: [NetworkLogger()])
///When we don't want network logs
// manager = Session(configuration: configuration)
}
// MARK: - Request
func requestData(_ convertible: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil,
completion: #escaping (Result<Data, ErrorResult>) -> Void) {
manageCachePolicy()
manager.request(convertible,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers).validate().responseData
{ (response) in
switch response.result {
case .success(let data):
completion(.success(data))
case .failure(let error):
completion(.failure(.network(string: error.localizedDescription)))
}
}
}
}

How to implement multiple Routers with similar asURLRequest()

I have been working to re-implement a healthy network layer in our app and Routers are noted in many tutorials / Alamofire documentation. The app has a lot of endpoints, to keep things readable I want to split them out into their subset of services. That was also noted as a best practice.
The very first endpoint that I implemented works perfectly fine, but, when I create another Router there is the asURLRequest() function which would pretty much be a duplicate. The only difference could be the switch/case. Otherwise its almost certain to be the same.
To do this in Kotlin or Java, I would create a class and extend the function calling the super. Im not certain how that works here in Swift.
enum AuthenticationRouter: URLRequestConvertible {
case login(username:String, password:String)
// MARK: - HTTPMethod
private var method: HTTPMethod {
switch self {
case .login:
return .get
}
}
// MARK: - Path
private var path: String {
switch self {
case .login:
return "users/login"
}
}
// MARK: - Parameters
private var parameters: Parameters? {
switch self {
case .login:
return nil
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try K.TestServer.baseURL.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
// Common Headers
urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
switch self {
case .login(let username, let password):
// Handle adjusting headers or request as needed
default: ()
}
// Parameters
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
return urlRequest
}
}
Here is and old code I used. I’d change the base class to protocol instead and continue your usage of enums that conforms to that protocol I like it much better.
import Alamofire
protocol API {
var method: HTTPMethod { get }
var encoding: ParameterEncoding? { get }
var parameters: Parameters { get }
var headers: HTTPHeaders { get }
var timeout:TimeInterval { get }
var path: String { get }
var baseUrl: String { get }
}
class Router: API {
var method: HTTPMethod { .get }
var encoding: ParameterEncoding? { JSONEncoding.default }
var parameters: Parameters {
let params = Parameters()
return params
}
var headers: HTTPHeaders {
let headers = HTTPHeaders()
// TODO - add defualt headers if needed
return headers
}
var timeout: TimeInterval {
#if DEBUG
return 120
#else
return 30
#endif
}
var path: String {
fatalError("Must be overridden in subclass")
}
var baseUrl: String { Consts.serverUrl }
}
extension Router: URLRequestConvertible {
open func asURLRequest() throws -> URLRequest {
guard let baseURL = URL(string: baseUrl) else { throw someError }
let appendedURL = baseURL.appendingPathComponent(path)
let urlRequest = try URLRequest(url: appendedURL, method: method)
guard let encoder = encoding else { throw someError }
var eUrlRequest = try encoder.encode(urlRequest, with: parameters)
eUrlRequest.timeoutInterval = timeout
headers.forEach { eUrlRequest.addValue($0.value, forHTTPHeaderField: $0.name) }
return eUrlRequest
}
}
And then an example UserRouter would be
import Alamofire
final class UserRouter: Router {
enum Endpoint {
case signIn(userName: String, password: String)
}
let endpoint: Endpoint
init(endpoint: Endpoint) {
self.endpoint = endpoint
}
override var method: HTTPMethod {
.get
}
override var parameters: Parameters {
var params = super.parameters
switch endpoint {
case .signIn(let userName, let password):
params[“login”] = [“username”: userName, “password”: password]
}
return params
}
override var path: String {
switch endpoint {
case .signIn:
return "/signIn"
}
}
}
In this way the asUrlRequest() function will be used throughout the code (; the Alamofire.Session().request() function will accept the each one of the routers.
Additionally, there is a great open-source abstraction that does similar work above Alamofire. Moya - https://github.com/Moya/Moya.
It supports Combine and RxSwift right out of the box

Implementing reconnection with URLSession publisher and Combine

I'm wondering if there is a way to implement reconnection mechanism with new Apple framework Combine and use of URLSession publisher
tried to find some examples in WWDC 2019
tried to play with waitsForConnectivity with no luck (it even not calling delegate on custom session)
tried URLSession.background but it crashed during publishing.
I'm also not understanding how do we track progress in this way
Does anyone already tried to do smth like this?
upd:
It seems like waitsForConnectivity is not working in Xcode 11 Beta
upd2:
Xcode 11 GM - waitsForConnectivity is working but ONLY on device. Use default session, set the flag and implement session delegate. Method task is waiting for connectivity will be invoked no matter if u r using init task with callback or without.
public class DriverService: NSObject, ObservableObject {
public var decoder = JSONDecoder()
public private(set) var isOnline = CurrentValueSubject<Bool, Never>(true)
private var subs = Set<AnyCancellable>()
private var base: URLComponents
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
public init(host: String, port: Int) {
base = URLComponents()
base.scheme = "http"
base.host = host
base.port = port
super.init()
// Simulate online/offline state
//
// let pub = Timer.publish(every: 3.0, on: .current, in: .default)
// pub.sink { _ in
// let rnd = Int.random(in: 0...1)
// self.isOnline.send(rnd == 1)
// }.store(in: &subs)
// pub.connect()
}
public func publisher<T>(for driverRequest: Request<T>) -> AnyPublisher<T, Error> {
var components = base
components.path = driverRequest.path
var request = URLRequest(url: components.url!)
request.httpMethod = driverRequest.method
return Future<(data: Data, response: URLResponse), Error> { (complete) in
let task = self.session.dataTask(with: request) { (data, response, error) in
if let err = error {
complete(.failure(err))
} else {
complete(.success((data!, response!)))
}
self.isOnline.send(true)
}
task.resume()
}
.map({ $0.data })
.decode(type: T.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
extension DriverService: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
self.isOnline.send(false)
}
}
Have you tried retry(_:) yet? It’s available on Publishers and reruns the request upon failure.
If you don’t want the request to immediately rerun for all failures then you can use catch(_:) and decide which failures warrant a rerun.
Here's some code to achieve getting the progress.
enum Either<Left, Right> {
case left(Left)
case right(Right)
var left: Left? {
switch self {
case let .left(value):
return value
case .right:
return nil
}
}
var right: Right? {
switch self {
case let .right(value):
return value
case .left:
return nil
}
}
}
extension URLSession {
func dataTaskPublisherWithProgress(for url: URL) -> AnyPublisher<Either<Progress, (data: Data, response: URLResponse)>, URLError> {
typealias TaskEither = Either<Progress, (data: Data, response: URLResponse)>
let completion = PassthroughSubject<(data: Data, response: URLResponse), URLError>()
let task = dataTask(with: url) { data, response, error in
if let data = data, let response = response {
completion.send((data, response))
completion.send(completion: .finished)
} else if let error = error as? URLError {
completion.send(completion: .failure(error))
} else {
fatalError("This should be unreachable, something is clearly wrong.")
}
}
task.resume()
return task.publisher(for: \.progress.completedUnitCount)
.compactMap { [weak task] _ in task?.progress }
.setFailureType(to: URLError.self)
.map(TaskEither.left)
.merge(with: completion.map(TaskEither.right))
.eraseToAnyPublisher()
}
}
I read your question title several times. If you mean reconnect the URLSession's publisher. Due to the URLSession.DataTaskPublisher has two results. Success output or Failure (a.k.a URLError). It's not possible to make it reconnect after the output produced.
You can declare one subject. e.g
let output = CurrentValueSubject<Result<T?, Error>, Never>(.success(nil))
And add a trigger when network connection active then request resources and send the new Result to the output. Subscribe output in the other place. So that you can get new value when network back-online.

Alamofire request cancelled (-999)

I want to make use of the RequestAdapter and RequestRetrier protocols, so I created my own so called AuthenticationHandler class which implements both protocols. I do this because the refresh token may be expired so this mechanism comes in handy.
The RequestAdapter protocol method adapt does get called, but the should RequestRetrier protocol method does not. I have a separate class that does the actual request:
class TestRequest {
var authHandler: AuthenticationHandler?
func executeRequest() {
// For testing purposes a false access token is passed
self.authHandler = AuthenticationHandler(accessToken: "some_default_token")
let sessionManager = SessionManager()
sessionManager.adapter = authHandler
sessionManager.retrier = authHandler
var loginModel : LoginMessage = LoginMessage.init()
loginModel.username = "someUserName"
loginModel.password = "WrongPassword"
do {
let binaryData = try loginModel.serializedData()
// Create a file with this binary data in order to use it as part of the multipart formdata
guard let fileURL = createFileFrom(data: binaryData) else {
print("Error creating file")
return
}
// Note: custom headers have been set in the AuthenticationHandler
sessionManager.upload(multipartFormData: { multipartFormData in
multipartFormData.append(fileURL, withName: "content")
},
to: K.endpointLogin) { (encodingResult) in
switch encodingResult{
case .success(let upload, _, _):
upload.responseJSON { response in
print("Encoding result success...")
print("Statuscode: \(response.response?.statusCode)")
print(response)
}
case .failure(let encodingError):
print("Failure: \(encodingError)")
}
}
} catch {
print(error)
}
}
I have followed the example in the documentation here
I have read several previous posts saying it has to do with retaining the sessionManager. But I think that is also covered. My authentication handler looks like this:
class AuthenticationHandler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var accessToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
init(accessToken: String) {
self.accessToken = accessToken
}
// MARK: - RequestAdapter protocol method
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(K.SERVER_URL) {
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
urlRequest.setValue("multipart/form-data", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - RequestRetrier protocol method
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
}
}
My config is as follows:
Alamofire version: 4.7.2
Xcode version: 9.4.1
Swift version: 4
What am I doing wrong here? Why is the request cancelled and is the should method not called?
Any help is greatly appreciated.
Your core issue is that your SessionManager instance is being deinited, which cancels any ongoing tasks. You should keep it around in a singleton or something similar, which will fix the other issue, that of using a new SessionManager for each request, which is an anti pattern.

Swith enum can't inherit methods, best way to prevent rewriting same function for every enum

So I'm writing my networking code using a router design pattern. I'm writing a new router for different components of my app (should i be doing this? I try to limit my objects lines of code). Heres my router enum. If I was using a class, I could define a method once to populate variables like HTTPMethod and override them if necessary. Is there a way to do this with enums? Is it worth implementing or should i repeat the same code. There are a few other places besides httpMethod such as URL construction where I think this could be helpful.
I was thinking i could do something with protocols but am not sure if I'm wasting my time.
enum PRRouter: URLRequestConvertible {
static let baseURLString = "http://localhost:8000/"
case get(Int)
case create([String : Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
var method: HTTPMethod{
switch self {
case .get:
return .get
case .create:
return .post
case.delete:
return .delete
}
}
let params : ([String : Any]?) = {
switch self {
case .get, .delete:
return nil
case .create(let newTodo):
return newTodo
}
}()
let url : URL = {
let relativePath: String?
switch self{
case .get(let number):
relativePath = "test/\(number)"
case .create:
relativePath = "test/"
case .delete:
relativePath = "test/"
}
var url = URL(string: PRRouter.baseURLString)!
if let relativePath = relativePath {
url = url.appendingPathComponent(relativePath)
}
return url
}()
var urlRequest = URLRequest(url:url)
urlRequest.httpMethod = method.rawValue
let encoding = JSONEncoding.default
return try encoding.encode(urlRequest, with: params)
}
Make the enum conform to a protocol with a default implementation.
protocol P {
func f()
}
extension P {
func f() { print("default implementation") }
}
enum E: P {
case Foo
}
let e = E.Foo
e.f()
I do something similar in my own project. Here is an example based on your code to get you started:
protocol APIProtocol {
var path: String { get }
var method: HTTPMethods { get }
var bodyParameters: [String: Any?]? { get }
}
enum HTTPMethods: String {
case get = "GET"
case post = "POST"
}
enum PRRouter: APIProtocol {
case get(Int)
case create([String : Any])
case delete(Int)
var path: String {
switch self {
case let .get(number):
return "test/\(number)"
default:
return "test"
}
}
var method: HTTPMethods {
return .get
}
var bodyParameters: [String : Any?]? {
return nil
}
}
extension APIProtocol {
func execute(completion: #escaping ((Data?) -> Void)) -> URLSessionDataTask? {
guard let url = URL(string: "http://localhost:8000/\(path)") else { return nil }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
if let bodyParameters = bodyParameters {
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: bodyParameters, options: [.prettyPrinted])
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, urlResponse, error) in
completion(data)
}
task.resume()
return task
}
}
Finally you can use it like this:
let dataTask = PRRouter.get(2).execute { (data) in
//
}
You could extend this further by changing the completion block in the execute function to return a deserialized object.