Module 8 — Intégration dans une application iOS
L'écosystème iOS pose deux questions supplémentaires que le module 7 n'avait pas à traiter : « pourquoi ne pas utiliser directement Core ML ? » et « quels sont les points de conformité spécifiques à Apple ? ». Ce module y répond avant de dérouler l'intégration proprement dite en Swift.
TensorFlow Lite ou Core ML : ce n'est pas le même choix
Core ML est le format natif d'Apple pour les modèles embarqués. Il est bien optimisé, s'intègre parfaitement à l'écosystème, et bénéficie automatiquement de l'accélération sur le Neural Engine — la NPU dédiée des iPhone à partir de l'A11 (iPhone 8, 2017). À qualité équivalente, un modèle Core ML est souvent 20 à 40 % plus rapide qu'un TFLite avec délégué Core ML sur la même puce.
Alors pourquoi rester sur TFLite ?
- Portabilité : le même
.tflitetourne sur Android, iOS, Linux embarqué, micro-contrôleur. Un modèle Core ML n'existe que sur iOS et macOS. Pour une application multi-plateforme, un unique pipeline d'entraînement et un unique fichier binaire simplifient la maintenance. - Chaîne d'outils unifiée : quantification, élagage, QAT, mesures — les quatre modules précédents s'appliquent à un TFLite déployé sur les deux plateformes. Répliquer cette chaîne côté Core ML double le travail.
- Contrôle des délégués : le module 6 impose une stratégie de repli parc-dépendante que Core ML ne permet pas aussi finement.
Le compromis retenu dans le fil rouge : TFLite comme format principal, avec délégué Core ML côté iOS pour bénéficier du Neural Engine. On garde la portabilité tout en récupérant l'essentiel des gains matériels d'Apple.
Les dépendances : CocoaPods ou Swift Package Manager
Deux gestionnaires selon le projet. Avec CocoaPods :
# Podfile
platform :ios, '13.0'
target 'PlantVillageApp' do
use_frameworks!
pod 'TensorFlowLiteSwift', '2.14.0'
pod 'TensorFlowLiteSwift/CoreML', '2.14.0'
end
Avec Swift Package Manager (dans Xcode → File → Add Packages) :
https://github.com/tensorflow/tensorflow.git Branch: main
et sélectionner les produits TensorFlowLite et TensorFlowLiteCoreML. Le
paquet Swift est plus léger à intégrer, CocoaPods offre plus de versions
historiques et un contrôle plus fin. Pour un nouveau projet, SPM est
préférable.
Placement du modèle et chargement
Le fichier .tflite s'ajoute au bundle de l'application (glisser-déposer
dans Xcode, cocher la cible). Le chargement passe par l'API Interpreter :
import TensorFlowLite
import TensorFlowLiteCoreML
final class Classifieur {
private let interprete: Interpreter
init() throws {
let chemin = Bundle.main.path(
forResource: "plantvillage_qat_elague",
ofType: "tflite",
)!
var options = Interpreter.Options()
options.threadCount = 4
var delegues: [Delegate] = []
if let coreMl = CoreMLDelegate() {
delegues.append(coreMl)
}
interprete = try Interpreter(
modelPath: chemin,
options: options,
delegates: delegues,
)
try interprete.allocateTensors()
}
func classifier(_ image: Data) throws -> [Float] {
try interprete.copy(image, toInputAt: 0)
try interprete.invoke()
let sortie = try interprete.output(at: 0)
return sortie.data.toArray(type: Float.self)
}
}
Le point important : CoreMLDelegate() renvoie nil sur les appareils sans
Neural Engine (iPhone 6s à 8) ou sur iOS trop ancien. Le code doit gérer
ce cas — l'interprète tourne alors sur CPU via XNNPACK, sans crash — et ne
jamais forcer le délégué. C'est le pendant iOS de la stratégie de repli du
module 6.
Prétraitement de l'image caméra
Sur iOS, AVFoundation fournit la vidéo caméra. Une image arrive dans un
CMSampleBuffer au format BGRA, qu'il faut convertir en RGB,
redimensionner à 224x224, puis emballer en octets bruts dans l'ordre
attendu.
extension UIImage {
func normalise() -> Data? {
guard let cgImage = self.cgImage else { return nil }
let taille = CGSize(width: 224, height: 224)
UIGraphicsBeginImageContext(taille)
self.draw(in: CGRect(origin: .zero, size: taille))
let redimensionnee = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return redimensionnee?.pixelData() // extension utilitaire : bytes RGB
}
}
func inferer(surImage image: UIImage) throws -> Int {
guard let entree = image.normalise() else { throw ErreurClassifieur.image }
let probabilites = try classifieur.classifier(entree)
return probabilites.enumerated().max { $0.element < $1.element }!.offset
}
Comme sur Android, la normalisation /127.5 - 1 reste dans le modèle. Le
code Swift se contente du redimensionnement et de l'ordre des canaux.
Ne pas bloquer le fil principal
Sur iOS, le fil principal est celui qui redessine les vues à 60 Hz (120 Hz sur iPad Pro et iPhone 13 Pro+). Y exécuter une inférence de 30 ms fait tomber l'animation à 30 fps. La discipline est identique à Android : inférence sur une file de fond, retour marshalé sur le fil principal.
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
let indice = try self.inferer(surImage: image)
DispatchQueue.main.async {
self.afficherResultat(indice)
}
} catch {
DispatchQueue.main.async { self.afficherErreur(error) }
}
}
Une variante plus moderne utilise async/await avec un acteur dédié, mais
la logique reste la même : jamais d'inférence sur MainActor.
Les permissions caméra sont bloquantes sur iOS
C'est le point qui piège le plus une équipe qui migre d'Android. Sur iOS, l'accès à la caméra exige :
- Une entrée
NSCameraUsageDescriptiondansInfo.plist, avec une phrase claire décrivant l'usage. Une chaîne vide ou générique fait rejeter l'application à la revue Apple. - Une demande d'autorisation à l'exécution via
AVCaptureDevice.requestAccess(for: .video). - Une gestion explicite du refus : si l'utilisateur refuse, l'application doit afficher une explication et un bouton renvoyant vers les paramètres iOS.
Une entrée Info.plist typique pour le fil rouge :
<key>NSCameraUsageDescription</key>
<string>PlantVillage utilise la caméra pour analyser vos feuilles hors ligne, sans envoyer d'image sur Internet.</string>
Cette phrase précise ce qui est fait (analyse locale) et ce qui n'est pas fait (aucun envoi). C'est ce que la revue Apple contrôle, et c'est aussi ce qui rassure les utilisateurs sur la lecture de la demande.
Sur iOS, la fiche du magasin d'applications affiche depuis 2020 les « nutrition labels » qui listent les données collectées. Un modèle TFLite qui ne fait aucun appel réseau permet d'afficher « Aucune donnée collectée », ce qui différencie fortement l'application. C'est le premier retour positif de l'inférence embarquée sur le marketing.
L'impact du Neural Engine, en chiffres
Sur le fil rouge (modèle 2,1 Mo, int8 QAT + élagage 60 %) :
| Appareil | CPU seul (XNNPACK) | + délégué Core ML |
|---|---|---|
| iPhone 8 (A11) | 65 ms | 65 ms (Neural Engine non exposé à TFLite) |
| iPhone 12 (A14) | 32 ms | 12 ms |
| iPhone 15 (A17 Pro) | 18 ms | 5 ms |
Sur A11, le Neural Engine existe mais reste réservé à Core ML natif ; le délégué TFLite doit se replier sur CPU/GPU. À partir de l'A12 (iPhone Xs), le gain est net et justifie à lui seul l'ajout du délégué.
En résumé
- Rester sur TFLite avec délégué Core ML préserve la portabilité tout en captant l'essentiel du gain Neural Engine à partir de l'A12.
CoreMLDelegate()peut renvoyernil; le code doit s'y attendre et se replier sur XNNPACK sans planter.- Le prétraitement Swift se limite à redimensionner et cast bytes RGB ; la normalisation vit dans le modèle depuis le module 2.
- Les permissions caméra exigent
NSCameraUsageDescriptionavec une phrase claire, sans quoi la revue Apple rejette l'application. - Le Neural Engine ne se déclenche via TFLite qu'à partir de l'A12 ; sur A11, il faut basculer sur un modèle Core ML natif pour l'exploiter.
Module suivant : la mesure de la latence et de la consommation d'énergie sur les trois téléphones du fil rouge.