Introduction
En tant que développeur iOS depuis plus de sept ans, j'ai personnellement vécu la frustration de voir mes applications IA échouer complètement lorsque l'utilisateur perd sa connexion internet. En 2024, j'ai passé trois semaines à résoudre ce problème pour une application de reconnaissance d'images utilisée par des journalistes en zone rurale. Cette expérience m'a convaincu que l'avenir du développement IA mobile repose sur une architecture hybride combinant le traitement local via CoreML et les API cloud pour les tâches complexes.
Ce tutoriel vous guide depuis les bases absolues jusqu'à la mise en production d'une solution complète de raisonnement hybride. Aucune expérience préalable avec les API ou le machine learning n'est requise. Nous construirons ensemble une application qui bascule intelligemment entre le traitement local et cloud selon la connectivité et la complexité de la tâche.
Comprendre CoreML et le raisonnement hybride
Qu'est-ce que CoreML exactement ?
CoreML est le framework d'Apple permettant d'exécuter des modèles de machine learning directement sur l'appareil. Développé en Swift et intégré nativement dans iOS, il exploite les puces Neural Engine des appareils Apple pour un traitement ultra-rapide et économe en batterie. Contrairement aux croyankes populaires, CoreML ne se limite pas à la reconnaissance d'images : il supporte les modèles de langage, la segmentation sémantique, et même certains modèles de génération de texte.
Pourquoi une approche hybride ?
Voici la réalité que peu de tutoriels mentionnent : aucun modèle CoreML ne peut égaler les capacités d'un modèle cloud comme GPT-4 ou Claude en termes de compréhension contextuelle. Cependant, le cloud présente trois faiblesses majeures pour les applications mobiles :
- La latence réseau ajoute typiquement 200 à 800 millisecondes par requête
- La dépendance à la connexion rend l'application inutilisable hors ligne
- Les coûts s'accumulent rapidement avec des millions d'utilisateurs actifs
L'architecture hybride que nous allons construire optimise chaque requête en fonction de sa nature. Les tâches simples et urgentes (reconnaissance d'objets, OCR basique) s'exécutent localement. Les demandes complexes nécessitant un raisonnement avancé transitent vers le cloud.
Configuration de l'environnement de développement
Prérequis matériels et logiciels
Pour suivre ce tutoriel, vous aurez besoin de :
- Un Mac avec Xcode 15.0 ou supérieur installé
- Un iPhone ou iPad avec iOS 17.0 ou supérieur (pour tester le traitement local)
- Minimum 16 Go de RAM pour la compilation des modèles
- Un compte développeur Apple (gratuit suffit pour le développement)
Création du projet Xcode
Ouvrez Xcode et créez un nouveau projet iOS App. Configurez les options comme suit :
// Configuration du projet dans Xcode
// Product Name: HybridAIApp
// Interface: SwiftUI
// Language: Swift
// Deployment Target: iOS 17.0
// Fichier: HybridAIApp.swift
import SwiftUI
@main
struct HybridAIApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Installation des dépendances via Swift Package Manager
Ajoutez les packages nécessaires à votre projet :
// Dans Xcode : File > Add Package Dependencies
// URL du package: https://github.com/apple/swift-nio
// Version: 2.65.0
// Fichier: Package.swift (résumé des dépendances)
dependencies: [
.package(url: "https://github.com/apple/swift-nio", from: "2.65.0"),
]
Architecture de l'application hybride
Diagramme de l'architecture
Notre application repose sur trois couches distinctes :
- Couche Interface Utilisateur : Interface SwiftUI permettant à l'utilisateur d'interagir avec le système
- Couche Orchestrateur : Logique de décision déterminant si une requête doit être traitée localement ou via le cloud
- Couche Traitement : CoreML pour le local et client HTTP pour le cloud
Implémentation du gestionnaire de connectivité
import Foundation
import Network
import Combine
class ConnectivityManager: ObservableObject {
@Published var isConnected: Bool = true
@Published var connectionType: NWInterface.InterfaceType?
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "ConnectivityMonitor")
init() {
startMonitoring()
}
func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
self?.connectionType = path.availableInterfaces.first?.type
}
}
monitor.start(queue: queue)
}
func stopMonitoring() {
monitor.cancel()
}
}
Gestionnaire de modèle CoreML local
import Foundation
import CoreML
import Vision
class LocalAIManager: ObservableObject {
@Published var isProcessing: Bool = false
@Published var lastError: Error?
private var classificationModel: VNClassifyImageRequest?
// Initialisation du modèle MobileNetV3 pour la classification d'images
init() {
loadModel()
}
private func loadModel() {
// MobileNetV3 Small - modèle optimisé pour les appareils iOS
// Téléchargeable depuis Apple Developer Documentation
do {
let config = MLModelConfiguration()
config.computeUnits = .all // Utilise CPU + GPU + Neural Engine
// Utilisation d'un modèle converti pour CoreML
// Remplacez MobileNetV3 par votre modèle personnalisé si nécessaire
let model = try VNClassifyImageRequest.supportedTextRecognizerRequests
print("Modèle CoreML chargé avec succès")
} catch {
lastError = error
print("Erreur lors du chargement du modèle: \(error.localizedDescription)")
}
}
// Classification d'image locale
func classifyImage(_ image: CGImage, completion: @escaping (Result<[String: Double], Error>) -> Void) {
isProcessing = true
let request = VNClassifyImageRequest { [weak self] request, error in
DispatchQueue.main.async {
self?.isProcessing = false
if let error = error {
completion(.failure(error))
return
}
guard let observations = request.results as? [VNClassificationObservation] else {
completion(.failure(AIError.noResults))
return
}
// Retourne les top 5 classifications avec leurs confidences
let results = Dictionary(uniqueKeysWithValues: observations.prefix(5).map {
($0.identifier, Double($0.confidence))
})
completion(.success(results))
}
}
let handler = VNImageRequestHandler(cgImage: image, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try handler.perform([request])
} catch {
DispatchQueue.main.async { [weak self] in
self?.isProcessing = false
completion(.failure(error))
}
}
}
}
}
enum AIError: LocalizedError {
case noResults
case modelNotLoaded
case invalidImage
var errorDescription: String? {
switch self {
case .noResults: return "Aucun résultat obtenu du modèle"
case .modelNotLoaded: return "Le modèle CoreML n'est pas chargé"
case .invalidImage: return "L'image fournie est invalide"
}
}
}
Intégration d'une API cloud pour les tâches complexes
Client API universel
import Foundation
class CloudAIClient: ObservableObject {
@Published var isProcessing: Bool = false
@Published var lastError: Error?
private let session: URLSession
private let baseURL: String
private let apiKey: String
// Configuration avec vos credentials API
init(baseURL: String, apiKey: String) {
self.baseURL = baseURL
self.apiKey = apiKey
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
self.session = URLSession(configuration: config)
}
// Méthode universelle pour les requêtes chat
func sendChatRequest(
messages: [[String: String]],
model: String = "gpt-4",
temperature: Double = 0.7,
completion: @escaping (Result) -> Void
) {
isProcessing = true
guard let url = URL(string: "\(baseURL)/chat/completions") else {
completion(.failure(CloudAIError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let body: [String: Any] = [
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": 1000
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
} catch {
completion(.failure(error))
return
}
let task = session.dataTask(with: request) { [weak self] data, response, error in
DispatchQueue.main.async {
self?.isProcessing = false
if let error = error {
self?.lastError = error
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(CloudAIError.invalidResponse))
return
}
guard (200...299).contains(httpResponse.statusCode) else {
completion(.failure(CloudAIError.httpError(httpResponse.statusCode)))
return
}
guard let data = data else {
completion(.failure(CloudAIError.noData))
return
}
do {
let decoder = JSONDecoder()
let response = try decoder.decode(ChatResponse.self, from: data)
completion(.success(response))
} catch {
completion(.failure(error))
}
}
}
task.resume()
}
}
// Types de réponse
struct ChatResponse: Codable {
let id: String
let choices: [Choice]
let usage: Usage
struct Choice: Codable {
let message: Message
let finishReason: String
enum CodingKeys: String, CodingKey {
case message
case finishReason = "finish_reason"
}
}
struct Message: Codable {
let role: String
let content: String
}
struct Usage: Codable {
let promptTokens: Int
let completionTokens: Int
let totalTokens: Int
enum CodingKeys: String, CodingKey {
case promptTokens = "prompt_tokens"
case completionTokens = "completion_tokens"
case totalTokens = "total_tokens"
}
}
}
enum CloudAIError: LocalizedError {
case invalidURL
case invalidResponse
case httpError(Int)
case noData
var errorDescription: String? {
switch self {
case .invalidURL: return "URL de l'API invalide"
case .invalidResponse: return "Réponse serveur invalide"
case .httpError(let code): return "Erreur HTTP: \(code)"
case .noData: return "Aucune donnée reçue"
}
}
}
L'orchestrateur intelligent : le cœur du système hybride
Algorithme de décision automatique
import Foundation
import Combine
import UIKit
class HybridAIManager: ObservableObject {
@Published var currentResult: String = ""
@Published var isProcessing: Bool = false
@Published var processingSource: ProcessingSource = .none
enum ProcessingSource {
case none
case local
case cloud
}
// Seuil de confiance pour basculer vers le cloud
private let confidenceThreshold: Double = 0.7
private let connectivityManager = ConnectivityManager()
private let localManager = LocalAIManager()
private var cloudClient: CloudAIClient?
// Configuration du client cloud
func configureCloudClient(baseURL: String, apiKey: String) {
self.cloudClient = CloudAIClient(baseURL: baseURL, apiKey: apiKey)
}
// Classification d'image intelligente
func classifyImage(_ image: UIImage, forceCloud: Bool = false) {
isProcessing = true
// Décision: utiliser CoreML ou le cloud ?
let useCloud = shouldUseCloud(for: .classification, forceCloud: forceCloud)
guard let cgImage = image.cgImage else {
currentResult = "Erreur: Impossible de traiter l'image"
isProcessing = false
return
}
if useCloud {
processingSource = .cloud
processOnCloud(cgImage: cgImage)
} else {
processingSource = .local
processLocally(cgImage: cgImage)
}
}
// Analyse de texte complexe (toujours cloud)
func analyzeText(_ text: String, task: String) {
isProcessing = true
processingSource = .cloud
guard let client = cloudClient else {
currentResult = "Erreur: Client cloud non configuré"
isProcessing = false
return
}
let messages: [[String: String]] = [
["role": "system", "content": "Tu es un assistant expert en analyse de texte. Réponds de manière concise."],
["role": "user", "content": "\(task): \(text)"]
]
client.sendChatRequest(messages: messages) { [weak self] result in
DispatchQueue.main.async {
self?.isProcessing = false
switch result {
case .success(let response):
self?.currentResult = response.choices.first?.message.content ?? "Réponse vide"
case .failure(let error):
self?.currentResult = "Erreur: \(error.localizedDescription)"
}
}
}
}
// Logique de décision
private func shouldUseCloud(for task: AITask, forceCloud: Bool) -> Bool {
if forceCloud { return true }
switch task {
case .classification:
// Utilise le cloud si pas de connexion ou pour une analyse plus approfondie
return !connectivityManager.isConnected
case .analysis, .reasoning, .generation:
// Ces tâches requièrent toujours le cloud
return connectivityManager.isConnected
}
}
private func processLocally(cgImage: CGImage) {
localManager.classifyImage(cgImage) { [weak self] result in
DispatchQueue.main.async {
self?.isProcessing = false
switch result {
case .success(let classifications):
let topResult = classifications.max(by: { $0.value < $1.value })
self?.currentResult = "Local: \(topResult?.key ?? "Inconnu") (\(String(format: "%.1f", (topResult?.value ?? 0) * 100))%)"
case .failure(let error):
self?.currentResult = "Erreur local: \(error.localizedDescription)"
}
}
}
}
private func processOnCloud(cgImage: CGImage) {
// Conversion de l'image en base64 pour l'envoi
let uiImage = UIImage(cgImage: cgImage)
guard let imageData = uiImage.jpegData(compressionQuality: 0.8) else {
currentResult = "Erreur: Impossible d'encoder l'image"
isProcessing = false
return
}
let base64Image = imageData.base64EncodedString()
// Envoi vers l'API cloud
guard let client = cloudClient else {
currentResult = "Erreur: API non configurée"
isProcessing = false
return
}
let messages: [[String: String]] = [
["role": "system", "content": "Tu es un expert en vision par ordinateur. Décris l'image de manière concise."],
["role": "user", "content": [
["type": "text", "text": "Décris cette image en une phrase:"],
["type": "image_url", "image_url": ["url": "data:image/jpeg;base64,\(base64Image)"]]
]]
]
// Note: Cette structure dépend de l'API utilisée
client.sendChatRequest(messages: messages, model: "gpt-4-vision-preview") { [weak self] result in
DispatchQueue.main.async {
self?.isProcessing = false
switch result {
case .success(let response):
self?.currentResult = "Cloud: \(response.choices.first?.message.content ?? "Réponse vide")"
case .failure(let error):
self?.currentResult = "Erreur cloud: \(error.localizedDescription)"
}
}
}
}
}
enum AITask {
case classification
case analysis
case reasoning
case generation
}
Interface utilisateur complète
import SwiftUI
import PhotosUI
struct ContentView: View {
@StateObject private var hybridManager = HybridAIManager()
@State private var selectedItem: PhotosPickerItem?
@State private var inputImage: UIImage?
@State private var textInput: String = ""
@State private var showingAPIConfig: Bool = false
// Configuration API - REMPLACEZ PAR VOS CREDENTIALS
@AppStorage("api_base_url") private var apiBaseURL: String = "https://api.holysheep.ai/v1"
@AppStorage("api_key") private var apiKey: String = ""
var body: some View {
NavigationStack {
VStack(spacing: 20) {
// Indicateur de source de traitement
HStack {
Image(systemName: hybridManager.processingSource == .local ? "cpu" : "cloud")
.foregroundColor(hybridManager.processingSource == .local ? .green : .blue)
Text(sourceText)
.font(.caption)
}
.padding(.horizontal)
// Zone d'image
PhotosPicker(selection: $selectedItem, matching: .images) {
if let image = inputImage {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(maxHeight: 250)
.clipShape(RoundedRectangle(cornerRadius: 12))
} else {
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.2))
.frame(height: 200)
VStack {
Image(systemName: "photo.badge.plus")
.font(.largeTitle)
Text("Sélectionner une image")
.font(.caption)
}
.foregroundColor(.secondary)
}
}
}
.onChange(of: selectedItem) { _, newValue in
Task {
if let data = try? await newValue?.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
inputImage = image
}
}
}
// Champ de texte pour analyse
TextField("Entrez du texte à analyser...", text: $textInput, axis: .vertical)
.textFieldStyle(.roundedBorder)
.frame(height: 100)
.padding(.horizontal)
// Boutons d'action
HStack(spacing: 16) {
Button(action: classifyImage) {
Label("Classifier", systemImage: "eye")
}
.buttonStyle(.borderedProminent)
.disabled(inputImage == nil || hybridManager.isProcessing)
Button(action: analyzeText) {
Label("Analyser", systemImage: "text.magnifyingglass")
}
.buttonStyle(.bordered)
.disabled(textInput.isEmpty || hybridManager.isProcessing)
}
// Résultat
if !hybridManager.currentResult.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text("Résultat:")
.font(.headline)
Text(hybridManager.currentResult)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.padding(.horizontal)
}
Spacer()
}
.padding(.top)
.navigationTitle("IA Hybride")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAPIConfig = true }) {
Image(systemName: "gear")
}
}
}
.sheet(isPresented: $showingAPIConfig) {
APIConfigView(apiBaseURL: $apiBaseURL, apiKey: $apiKey) {
hybridManager.configureCloudClient(baseURL: apiBaseURL, apiKey: apiKey)
}
}
.onAppear {
hybridManager.configureCloudClient(baseURL: apiBaseURL, apiKey: apiKey)
}
}
}
private var sourceText: String {
switch hybridManager.processingSource {
case .none: return "En attente"
case .local: return "Traitement local (CoreML)"
case .cloud: return "Traitement cloud"
}
}
private func classifyImage() {
guard let image = inputImage else { return }
hybridManager.classifyImage(image)
}
private func analyzeText() {
guard !textInput.isEmpty else { return }
hybridManager.analyzeText(textInput, task: "Analyse ce texte")
}
}
struct APIConfigView: View {
@Binding var apiBaseURL: String
@Binding var apiKey: String
let onSave: () -> Void
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Form {
Section("Configuration API") {
TextField("URL de l'API", text: $apiBaseURL)
.textContentType(.URL)
.autocapitalization(.none)
SecureField("Clé API", text: $apiKey)
.textContentType(.password)
}
Section {
Text("Obtenez votre clé API depuis votre fournisseur de services IA.")
.font(.caption)
.foregroundColor(.secondary)
}
}
.navigationTitle("Configuration")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Enregistrer") {
onSave()
dismiss()
}
}
}
}
}
}
#Preview {
ContentView()
}
Conversion de modèles pour CoreML
Utilisation de coremltools
Pour convertir un modèle TensorFlow ou PyTorch en format CoreML, installez l'outil de conversion :
# Installation de coremltools
pip install coremltools==7.0
Conversion d'un modèle PyTorch (exemple avec ResNet)
import torch
import torchvision.models as models
import coremltools as ct
Charger le modèle PyTorch
model = models.resnet50(pretrained=True)
model.eval()
Exemple d'entrée pour le tracing
example_input = torch.randn(1, 3, 224, 224)
Tracer le modèle
traced_model = torch.jit.trace(model, example_input)
Convertir vers CoreML
coreml_model = ct.convert(
traced_model,
inputs=[ct.ImageType(name="input", shape=(1, 3, 224, 224))],
compute_units=ct.ComputeUnit.ALL # CPU + GPU + Neural Engine
)
Sauvegarder le modèle
coreml_model.save("ResNet50.mlpackage")
print("Conversion réussie: ResNet50.mlpackage")
Optimisation des performances
Bonnes pratiques CoreML
- Quantification : Réduisez la taille du modèle de 75% avec une perte de précision minimale en utilisant la quantification INT8
- Batch processing : Traitez plusieurs images en une seule passe pour maximiser l'utilisation du Neural Engine
- Préchauffage : Exécutez une inférence au démarrage pour charger le modèle en mémoire
- Réutilisation du handler : Créez VNImageRequestHandler une fois et réutilisez-le
// Optimisation de la quantification du modèle
// Dans Xcode: Edit > Convert File...
// Configuration recommandée pour les modèles quantifiés
let quantizedConfig = MLComputePlanOptions(
allowLowPrecisionAccumulation: true
)
let config = MLModelConfiguration()
config.computeOptions = quantizedConfig
config.computeUnits = .cpuAndNeuralEngine // Exclut le GPU pour sauver batterie
// Chargement optimisé
let optimizedModel = try MyModel(configuration: config)
Erreurs courantes et solutions
1. Erreur: "MLModel not loaded"
Symptôme: L'application crash au démarrage avec une erreur CoreML.
// ❌ ERREUR: Chargement sans vérification d'erreur
let model = try! MyModel()
// ✅ SOLUTION: Gestion robuste des erreurs
do {
let config = MLModelConfiguration()
config.computeUnits = .all
let model = try MyModel(contentsOf: modelURL, configuration: config)
print("Modèle chargé avec succès")
} catch {
print("Erreur de chargement du modèle: \(error)")
// Afficher une alerte à l'utilisateur
// Proposer un fallback vers le cloud
}
2. Erreur: "Image format not supported"
Symptôme: VNClassifyImageRequest échoue avec une image de la galerie photos.
// ❌ ERREUR: Utilisation directe de UIImage sans conversion
let request = VNClassifyImageRequest()
let handler = VNImageRequestHandler(cgImage: uiImage.cgImage!)
// ✅ SOLUTION: Conversion et validation préalable
func prepareImageForVision(_ image: UIImage) -> CGImage? {
guard let cgImage = image.cgImage else {
// Fallback: recréer depuis les données
guard let data = image.jpegData(compressionQuality: 1.0),
let recreated = UIImage(data: data)?.cgImage else {
return nil
}
return recreated
}
// S'assurer que l'image est dans un format supporté (BGRA ou RGBA)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
return cgImage
}
3. Erreur: HTTP 401 Unauthorized
Symptôme: Les requêtes cloud échouent avec une erreur d'authentification.
// ❌ ERREUR: Clé API en dur dans le code
let apiKey = "sk-1234567890abcdef"
// ✅ SOLUTION: Stockage sécurisé avec Keychain
import Security
class KeychainManager {
static func save(key: String, value: String) -> Bool {
let data = value.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func retrieve(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value
}
}
// Utilisation
if let apiKey = KeychainManager.retrieve(key: "holysheep_api_key") {
cloudClient.configureCloudClient(baseURL: apiBaseURL, apiKey: apiKey)
}
4. Problème de latence excessive
Symptôme: Les requêtes cloud prennent plus de 5 secondes.
// ✅ SOLUTION: Timeout adaptatif et retry intelligent
class ResilientCloudClient {
private let maxRetries = 3
private let baseTimeout: TimeInterval = 10
func sendWithRetry(request: URLRequest, completion: @escaping (Result) -> Void) {
var attempt = 0
func attemptRequest() {
var mutableRequest = request
// Timeout adaptatif selon le nombre de tentatives
mutableRequest.timeoutInterval = baseTimeout * Double(attempt + 1)
let task = session.dataTask(with: mutableRequest) { data, response, error in
if let error = error as NSError? {
if error.code == NSURLErrorTimedOut && attempt < self.maxRetries {
attempt += 1
attemptRequest() // Retry
return
}
completion(.failure(error))
return
}
completion(.success(data ?? Data()))
}
task.resume()
}
attemptRequest()
}
}
Considérations de sécurité
Protection des données utilisateur
- Ne jamais envoyer d'images sans consentement explicite vers le cloud
- Chiffrer les communications avec HTTPS uniquement (ATS activé)
- Minimiser les données : redimensionner les images avant envoi
- Stocker les clés API dans le Keychain iOS, jamais en UserDefaults
Tests et déploiement
Configuration pour les différents environnements
// Configuration multi-environnement
enum Environment {
case development
case staging
case production
var apiBaseURL: String {
switch self {
case .development:
return "https://dev-api.holysheep.ai/v1"
case .staging:
return "https://staging-api.holysheep.ai/v1"
case .production:
return "https://api.holysheep.ai/v1"
}
}
}
// Activation selon la configuration
#if DEBUG
let currentEnvironment = Environment.development
#else
let currentEnvironment = Environment.production
#endif
// Test unitaire du gestionnaire hybride
import XCTest
class HybridAIManagerTests: XCTestCase {
func testLocalProcessingWhenOffline() {
let manager = HybridAIManager()
let mockConnectivity = MockConnectivityManager(isConnected: false)
// Vérifie que le traitement local est utilisé quand hors ligne
XCTAssertTrue(manager.shouldUseCloud(for: .classification, forceCloud: false, connectivity: mockConnectivity))
}
}
Conclusion et下一步
Vous disposez maintenant d'une application iOS complète avec une architecture de raisonnement hybride fonctionnelle. Les points clés à retenir :
- CoreML offre un traitement local rapide mais limité en capacités
- L'API cloud complète les lacunes avec des modèles plus puissants
- L'orchestrateur intelligent décide automatiquement selon la connectivité et la tâche
- La sécurité et la gestion d'erreurs sont essentielles pour une expérience utilisateur fluide
Pour aller plus loin, explorez l'optimisation de modèles personnalisés avec Create ML, ou implémentez un système de cache intelligent pour réduire les appels API.