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 :

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 :

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 :

  1. Couche Interface Utilisateur : Interface SwiftUI permettant à l'utilisateur d'interagir avec le système
  2. Couche Orchestrateur : Logique de décision déterminant si une requête doit être traitée localement ou via le cloud
  3. 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

// 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

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 :

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.

👉 Inscrivez-vous sur HolySheep AI — crédits offerts