Un développeur ayant travaillé pendant plus de dix ans sur des applications iOS de premier plan a récemment partagé une mise en garde cinglante concernant l'utilisation de la bibliothèque swift-dependencies pour la gestion des dépendances dans les applications Swift. Dans un billet de blog détaillé, il explique que cette solution, bien que présentée comme « l'équivalent de l'environnement SwiftUI pour tout », repose sur un mécanisme de stockage global de type @TaskLocal avec un cache caché, qui, selon lui, entraîne des bugs silencieux en production et des plantages à l'exécution. Il précise d'emblée que la bibliothèque est conçue pour de petits systèmes avec quelques services d'envergure globale, mais que pour des applications plus complexes avec des cycles de vie de session, des connexions et déconnexions, ou un usage intensif de l'asynchrone, le rapport coût/correction est médiocre.

Trois primitives sources de problèmes

L'analyse identifie trois primitives fondamentales dans swift-dependencies, responsables de ce qu'il nomme des « footguns ». La première est le stockage via @TaskLocal : chaque dépendance vit dans une valeur locale à la tâche. La propagation obéit aux règles de SE-0311, ce qui signifie qu'un Task enfant hérite des valeurs, mais pas un Task.detached, et qu'il est impossible de lier une valeur locale à l'intérieur du corps d'un withTaskGroup. La deuxième primitive est le « capture at init » : le wrapper de propriété @Dependency capture l'état courant de DependencyValues._current au moment de l'initialisation de l'instance, puis fusionne cette capture avec la valeur locale actuelle à chaque lecture. La troisième est le « cache-on-first-access » : la valeur d'une DependencyKey est mise en cache au premier accès et n'est jamais recalculée. Un commentaire dans le code source le confirme : « si votre liveValue est implémentée comme une propriété calculée au lieu d'un static let, alors elle ne sera appelée qu'une seule fois ». Le développeur souligne que la bibliothèque ne propage pas les dépendances à travers le graphe d'objets ou l'arbre de vues : elle lit un global au moment de la construction, met le résultat en cache. Une fois ce mécanisme compris, tous les bugs « étranges » deviennent prévisibles.

Des bugs silencieux en production

Le premier défaut signalé concerne les closures échappantes qui perdent silencieusement les surcharges. La bibliothèque le reconnaît dans son propre fichier WithDependencies.swift : « les dépendances ne se propagent pas automatiquement à travers les limites échappantes comme elles le font dans les contextes structurés et dans les Tasks... En règle générale, vous devez entourer tout code échappant qui peut accéder aux dépendances avec cette aide, et vous devez utiliser yield(_:) immédiatement à l'intérieur de la closure échappante. Sinon, vous risquez que le code échappé utilise les mauvaises dépendances. » Un exemple canonique est donné avec un Reducer utilisant @Dependency(\ .mainQueue) : une surcharge de mainQueue avec un scheduler immédiat pour les tests n'est jamais observée par .receive(on:) car la closure de Combine est échappante. Brandon Williams, de Point-Free, a répondu sur le fil de discussion que « ce n'est pas un bug de la bibliothèque, mais une conséquence intentionnelle de l'utilisation de closures échappantes ». Le développeur dénonce une « taxe sur la vigilance » : il faudrait utiliser withEscapedDependencies partout, une obligation de correction que le système de types ne peut pas imposer.

Plantages dans les task groups

Le deuxième défaut est un plantage à l'exécution dans les task groups. SE-0311 interdit explicitement de lier une valeur locale à une tâche à l'intérieur du corps d'un withTaskGroup. Un exemple de code naturel qui échoue : utiliser withDependencies pour surcharger un logger par élément, puis appeler group.addTask à l'intérieur. Cela provoque un plantage avec le message « illegal task-local value binding ». La réponse officielle est que la surcharge doit être déplacée à l'intérieur de chaque addTask. Le développeur estime que c'est une limitation qui rend le code naturel non fonctionnel sans que le compilateur puisse aider à trouver la bonne forme.

Un cache qui fige le graphe de dépendances

Le troisième défaut concerne le cache qui gèle silencieusement le graphe de dépendances. Lorsqu'une dépendance en résout une autre au moment de la construction (par exemple, un client API qui lit un token d'authentification), cette résolution est mise en cache une fois pour toutes. Si l'utilisateur se connecte plus tard avec un token valide, la surcharge est ignorée car le client API a déjà été créé avec l'ancienne valeur. Des utilisateurs ont rapporté ce problème ; la réponse a été « fonctionne comme prévu, car un comportement non mis en cache était encore plus déroutant ». Le développeur conclut que la bibliothèque a choisi un défaut qui cause silencieusement un mauvais comportement dans un cas d'usage courant.

Un appel à la prudence

Le développeur insiste sur le fait que ces problèmes ne sont pas des bugs, mais des conséquences intentionnelles des choix de conception de swift-dependencies. Pour les petites applications avec quelques services globaux, la bibliothèque fonctionne bien. Mais pour les applications réelles, avec des cycles de vie de session, des combinaisons de Combine, NIO, GCD, delegates et async/await, et des dépendances qui doivent vivre et mourir avec un écran ou une session, il déconseille fortement l'adoption de cette solution, quel que soit l'investissement déjà réalisé dans l'écosystème TCA (The Composable Architecture).