fabernovel loader

Comment utiliser le cache HTTP avec iOS

fabernovel loader

Dans la programmation, la mise de données en cache est l’un des sujets les plus complexes. Il n’y a pas de solution miracle à cette problématique et celles que l’on connaît exigent certains compromis.

Dans cet article, nous allons expliquer comment nous avons réalisé une mise en cache basique dans la plupart de nos écrans en utilisant le cache HTTP. L’objectif était de fournir du contenu à l’utilisateur même sans connexion Internet, de la manière la plus simple possible. L’idée n’est pas compliquée : pour toutes les requêtes GET, nous mettons en cache la réponse que nous recevons. Ensuite, s’il n’y a pas de connexion, nous allons chercher la réponse précédente dans le cache et affichons un message d’avertissement à l’utilisateur, l’informant que les données sont peut-être périmées.

Mise en cache des données

L’idée principale est de mettre en cache toutes les réponses que nous recevons. Ceci peut être fait avec la méthode urlSession(_:dataTask:willCacheResponse:completionHandler:) de URLSessionDataDelegate.

La documentation de cette méthode indique :

Cette méthode n’est appliquée que si le protocole NSURL, traitant la requête, décide de mettre la réponse en cache. En règle générale, les réponses ne sont mises en cache que lorsque toutes les informations suivantes sont vraies :

  • La demande concerne une URL HTTP ou HTTPS (ou votre propre protocole réseau personnalisé qui prend en charge la mise en cache).
  • La demande a été acceptée (avec un code d’état compris entre 200 et 299).
  • La réponse fournie provenait du serveur, plutôt que du cache.
  • La politique de cache de la configuration de session permet la mise en cache.
  • La politique de cache de l’objet URLRequest fourni (si applicable) permet la mise en cache.
  • Les en-têtes liés au cache dans la réponse du serveur (si présent) permettent la mise en cache.
  • La taille de la réponse est suffisamment petite pour tenir raisonnablement dans le cache. (Par exemple, si vous fournissez un cache disque, la réponse ne doit pas dépasser 5% de la taille du cache disque.)

La mise en cache de toutes les requêtes nécessite que le serveur renvoie les en-têtes suivants :

Expires: ou Cache-Control: avec avec un paramètre max-age ou s- maxage. En utilisant Alamofire, voici à quoi ressemble le code :

dataTaskWillCacheResponse = { [weak self] (urlSession, urlSessionDataTask, cachedURLResponse) -> CachedURLResponse? in
    let modifiedResponse = self?
       .whipeAuthenticationHeaders(from: cachedURLResponse.response)
       ?? cachedURLResponse.response
    let date = NSDate()
    let userInfo = [
      Keys.cacheDateHeader: date.timeIntervalSince1970
    ]
    return CachedURLResponse(
        response: modifiedResponse,
        data: cachedURLResponse.data,
        userInfo: userInfo,
        storagePolicy: .allowed
    )
}

Notez que nous sauvegardons la date du jour avec la réponse. Nous l’utiliserons plus tard pour afficher la fraîcheur des données. Vous vous demandez peut-être ce que la méthodewhipeAuthenticationHeaders fait ici. Si nous sauvegardons la réponse telle quelle, lorsque nous récupérons la réponse mise en cache à partir du cache local, les en-têtes d’authentification potentiels seraient périmés et entraîneraient l’échec des prochaines requêtes parce que le token est expiré. C’est pourquoi nous supprimons tous les en-têtes d’authentification avant de mettre la réponse en cache. De cette façon, ils ne seront pas utilisés par l’application. C’est ad hoc pour notre méthode d’authentification, mais sachez que ce genre de problème peut arriver.

Récupération des données mises en cache

Le mécanisme pour récupérer la réponse mise en cache est implémenté dans un behavior. Un behavior est un objet qui peut exécuter le code à différents moments de la vie d’une requête. Ce n’est pas le sujet de cet article, mais vous pouvez en apprendre plus ici.

Rappelez-vous simplement que dans notre cas, nous voulons modifier la réponse lorsque l’appel réseau échoue, et retourner les données en cache au lieu d’afficher un message d’erreur.

class LoadFromCacheIfUnreachableBehavior: RequestBehavior {

    private let cache: URLCache

    init(cache: URLCache) {
        self.cache = cache
    }

    func modify(request: HTTPRequest, response: JSONResponse) -> JSONResponse {
      // Verify that we need to use the cache
         guard case let .error(error) = response.result,
             let apiError = error as? APIError,
             apiError == .unreachableService else {
                 return response
    }

    guard let urlRequest = response.request else {
        return response
    }

    guard let cachedResponse = cache.cachedResponse(for: urlRequest),
        let httpURLResponse = cachedResponse.response as? HTTPURLResponse else {
            return response
    }

    // Get the date back from the cache
    var date: Date?
    if let userInfo = cachedResponse.userInfo,
        let cacheDate = userInfo[Keys.cacheDateHeader] as? TimeInterval {
        date = Date(timeIntervalSince1970: cacheDate)
    }

   do {
      // Create the response from cached data
      let json = try JSON(data: cachedResponse.data)
      let newResponse = JSONResponse(
          result: .value(json),
          request: response.request,
          response: httpURLResponse,
          date: date
      )
      return newResponse
   } catch {
        return response
   }
  }
}

Plus simplement, lorsque nous sollicitons le réseaux et obtenons une erreur au retour due à un service inaccessible (une erreur de

URLSession), nous demandons alors la réponse en cache pour la requête url en cours.

L’affichage

Dans le reste de l’application, les objets responsables de la récupération des données, appelés repositories, ont des méthodes avec des signatures comme celle-ci :

protocol AccountRepository {
    func getAccounts(completion: ((Result<CachedValue<[Account]>>) -> Void)?)
}

Ils utilisent objet CacheValue, défini comme suit :

enum CachedValue<T> {
    case fresh(value: T)
    case cached(value: T, date: Date)
}

Soit les données sont fraîches lorsque l’appel réseau a réussi, soit les données sont lues dans le cache et contiennent la date de la réponse en cache. Dans le contrôleur de vue, nous affichons une barre d’information si les données sont mises en cache pour informer l’utilisateur final qu’une erreur s’est produite et que ces données ne sont pas les plus récentes.

private func display(cachedAccounts: CachedValue<[Account]>) {
    self.accounts = cachedAccounts.value
    tableView.reloadData()

    if let date = cachedAccounts.date {
        displayInformationBar(date: date)
    } else {
        hideInformationBar()
    }
}

Lorsque la requête réseau échoue, une barre d’information est affichée pour aider l’utilisateur à comprendre ce qui se passe.

Conclusion

Nous avons vu comment créer un système de cache local basé uniquement sur le cache HTTP. Cette implémentation ne fonctionne que pour les requêtes GET, bien sûr, et c’est très loin d’une expérience hors ligne, mais les utilisateurs verront du contenu même s’ils n’ont pas de connexion réseau. C’est déjà un bon début et une belle amélioration par rapport au simple affichage d’un message erreur.

Ces sujets vous intéressent ?

Suivez-nous !
à lire
MOBILE
L'accessibilité des applications iOS

Pour une de nos dernières applications nous avions une exigence forte de la part du client. L’application devait être entièrement accessible. Qu’est-ce que cela veut dire en pratiq ...

Sécurité des Applications iOS - Les Débogueurs

Avec environ un demi-milliard d’appareils iOS vendus à travers le monde, la sécurité des applications iOS devient un enjeu majeur. Les attaques sur une application peuvent être trè ...