Accéder au contenu principal

 

Intégration d’Apple Intelligence dans JeValide 




Contexte et objectif

JeValide est une application de suivi pédagogique dont le déploiement cible iOS 15 et supérieur. Dans l’espace professeur, l’onglet « Actions », puis observations, permet à l’enseignant de rédiger des observations textuelles sur les progrès d’un élève pour une période donnée (trimestre, semestre, année scolaire).

Ces observations sont normalement de simples textes entrés par l’enseignant. Le but de l’utilistion d’Intelligence est de construire automatiquement le texte à partir des données de validation de l’élève. L’application crée un bilan en texte à partir dedes validation de chaque item (ainsi que des dictée à l’adulte).

L’objectif de cette fonctionnalité est de résumer ce texte brut en un paragraphe synthétique, intelligible par les parents, grâce au modèle de langage embarqué d’Apple Intelligence, et ainsi de générer une observation que l’enseignant pourra utiliser en ne faisant que des ajustements.


Contrainte principale : iOS 15 vs iOS 26

Apple Intelligence et le framework FoundationModels ne sont disponibles qu’à partir d’iOS 26. Or, JeValide supporte iOS 15 comme cible minimale, et doit continuer à fonctionner sur les appareils plus anciens.

La règle est simple : la fonctionnalité ne doit exister que là où elle peut fonctionner, sans dégrader l’expérience sur les appareils non compatibles.

Cette contrainte a dicté toutes les décisions d’architecture décrites ci-dessous.


Architecture : Swift dans un projet Objective-C

Le projet JeValide est presque entièrement écrit en Objective-C. L’intégration de FoundationModels impose d’écrire du Swift, car ce framework n’est pas accessible depuis Objective-C directement. La stratégie choisie est le pont Swift ↔ Objective-C via un fichier Swift annoté @objc.

Le fichier SwiftUIViewResume.swift

Ce fichier contient deux éléments distincts :

1. Une vue SwiftUI (SwiftUIViewResume)

C’est l’interface utilisateur de la fonctionnalité. Elle présente :

  • Un éditeur pour le texte source (le bilan en texte)
  • Un éditeur pour l’instruction système envoyée au modèle (modifiable et persistée dans UserDefaults)
  • Un bouton « Résumer » qui déclenche l’inférence
  • Un éditeur pour le résultat, modifiable avant validation
@available(iOS 26, *)
struct SwiftUIViewResume: View {
    private static let defaultInstruction = """
        Tu es un enseignant. Résume le texte fourni dans un paragraphe \
        de moins de 100 mots, sous la forme d'observations qui seront \
        dans le bilan de compétences. Le résumé est pour les parents, \
        résume les progrès de leur enfant dans les grandes lignes, \
        sans répéter les phrases du texte source.
        """
    // ...
}

L’instruction (prompt) par défaut est enregistrée dans UserDefaults sous la clé resumeInstruction, ce qui permet à l’enseignant de l’adapter à son style rédactionnel.

2. Un UIViewController enveloppeur (ResumeViewController)

Pour qu’Objective-C puisse instancier et présenter cette interface SwiftUI, il faut un UIViewController exposé via @objc : On se servira d’un système de delegate pour renvoyer le texte généré dans le reste du code de JeValide (objC).

@available(iOS 26, *)
@objc public class ResumeViewController: UIViewController {

    @objc public static var isIntelligenceAvailable: Bool {
        SystemLanguageModel.default.isAvailable
    }

    @objc public init(text: String) {
        self.initialText = text
        super.init(nibName: nil, bundle: nil)
        modalPresentationStyle = .formSheet
    }

    public override func viewDidLoad() {
        super.viewDidLoad()
        let swiftUIView = SwiftUIViewResume(text: initialText) { [weak self] in
            self?.dismiss(animated: true)
        } onConfirm: { [weak self] summary in
            guard let self else { return }
            delegate?.resumeViewController(self, didFinishWithSummary: summary)
            dismiss(animated: true)
        }
        let hosting = UIHostingController(rootView: swiftUIView)
        // ... contraintes Auto Layout standard
    }
}

La vue SwiftUI est intégrée via UIHostingController, dont la vue est contrainte aux quatre bords du contrôleur parent. C’est le mécanisme standard pour héberger du SwiftUI dans un contexte UIKit.

3. Le protocole délégué

Le retour du résumé vers Objective-C se fait via un protocole @objc :

@available(iOS 26, *)
@objc public protocol ResumeViewControllerDelegate: AnyObject {
    func resumeViewController(_ controller: ResumeViewController,
                              didFinishWithSummary summary: String)
}

Le contrôleur Objective-C implémente ce protocole et reçoit le texte résumé dans sa méthode déléguée.

L’appel depuis Objective-C

Le code Objective-C importe le pont Swift généré automatiquement par Xcode :

#import "JeValide-Swift.h"

Cet en-tête, généré à la compilation, expose toutes les classes et protocoles Swift annotés @objc public. Les annotations @available(iOS 26, *) sont traduites en macros API_AVAILABLE(ios(26.0)) dans l’en-tête généré.


Gestion des versions iOS

Toute la logique de détection de version est concentrée dans ObservationEditTextViewController, le contrôleur Objective-C qui orchestre la fonctionnalité.

Affichage conditionnel du bouton

Le bouton « Résumer » est caché par défaut dans le XIB. Il n’est rendu visible qu’en viewDidLoad, après une double vérification :

- (void)viewDidLoad {
    [super viewDidLoad];
    
    buttonResumer.hidden = YES; // caché par défaut
    if (@available(iOS 26, *)) {
        if ([ResumeViewController isIntelligenceAvailable]) {
            buttonResumer.hidden = NO;
            return;
        }
    }
    
    // Sur iOS < 26 : adaptation du layout clavier sans le bouton
    bottomConstraint.active = NO;
    [myTextView.bottomAnchor constraintEqualToAnchor:self.view.keyboardLayoutGuide.topAnchor
                                            constant:-8].active = YES;
}

Note : le layout du bas de la vue est ajusté différemment selon que le bouton est présent ou non, car celui-ci occupe de l’espace vertical.

Vérification à l’appui du bouton

Lorsque l’utilisateur appuie sur « Résumer », une double garde est appliquée :

- (IBAction)resumer:(id)sender {
    if (@available(iOS 26, *)) {
        if (![ResumeViewController isIntelligenceAvailable]) {
            [Eleves AlertWithTitle:@"Intelligence"
                        andMessage:@"Apple Intelligence non disponible"
                     andController:self
                         forAction:nil];
            return;
        }
        ResumeViewController *vc = [[ResumeViewController alloc] initWithText:[self bilanText]];
        vc.delegate = self;
        [self presentViewController:vc animated:YES completion:nil];
        return;
    }

    [Eleves AlertWithTitle:@"Intelligence"
                andMessage:@"IOS 26 minimum"
             andController:self
                 forAction:nil];
}

La macro @available(iOS 26, *) est vérifiée en premier. Si la version est insuffisante, une alerte indique que iOS 26 est requis. Si la version est correcte mais qu’Intelligence n’est pas disponible sur l’appareil (non configuré ou non supporté), une seconde alerte le signale.


Gestion des appareils sans Intelligence

Apple Intelligence n’est pas disponible dans tous les cas, même sur iOS 26 :

  • L’appareil ne la supporte pas matériellement (modèles anciens)
  • L’utilisateur ne l’a pas activée dans les réglages système
  • La langue du système n’est pas supportée
  • Le modèle n’est pas encore téléchargé

La propriété statique isIntelligenceAvailable encapsule cette vérification :

@objc public static var isIntelligenceAvailable: Bool {
    SystemLanguageModel.default.isAvailable
}

SystemLanguageModel.default.isAvailable retourne false dans tous ces cas. Côté Objective-C, un seul appel suffit pour couvrir toutes les situations où la fonctionnalité est inaccessible.

Côté interface, la stratégie retenue est l’absence plutôt que l’échec : le bouton n’apparaît jamais si la fonctionnalité ne peut pas fonctionner. L’alerte n’est là que comme filet de sécurité, pour le cas où l’état changerait entre viewDidLoad et l’appui sur le bouton.


Utilisation de Résumer/Summarize

La fonction swift Summarize utilise LanguageModelSession avec l’instruction/prompt :

private func summarize() async {
    isSummarizing = true
    defer { isSummarizing = false }
    do {
        let session = LanguageModelSession(instructions: instruction)
        let response = try await session.respond(to: "Résume ce texte : \(sourceText)")
        summaryText = response.content
    } catch {
        errorMessage = error.localizedDescription
    }
}

Chaque appui sur « Résumer » crée une nouvelle session. L’instruction est rechargée depuis UserDefaults à chaque ouverture de l’interface, ce qui garantit que les modifications de l’utilisateur sont prises en compte immédiatement.

Les erreurs (modèle non disponible, timeout, etc.) sont capturées et affichées dans une alerte SwiftUI native.


Flux de données complet

ObservationEditTextViewController (ObjC)
    │
    ├── bilanText          → construit le texte source à partir des données élève
    │
    └── [resumer:]         → présente ResumeViewController (Swift/@objc)
                                │
                                └── UIHostingController<SwiftUIViewResume>
                                        │
                                        ├── LanguageModelSession (FoundationModels)
                                        │       instruction: UserDefaults
                                        │       prompt: sourceText
                                        │
                                        └── onConfirm(summary)
                                                │
                                                └── delegate?.resumeViewController(_:didFinishWithSummary:)
                                                        │
                                                        └── myTextView.text = summary  (ObjC)

Points clés de l’implémentation

ProblèmeSolution retenue
FoundationModels non accessible en ObjCFichier Swift intermédiaire avec @objc public class
iOS 15 minimum, Intelligence iOS 26@available(iOS 26, *) + @available ObjC côté appelant
Intelligence non configuréeSystemLanguageModel.default.isAvailable avant toute action
SwiftUI dans UIKit/ObjCUIHostingController avec contraintes Auto Layout
Retour de résultat Swift → ObjCProtocole délégué @objc public protocol
Instruction personnalisablePersistée dans UserDefaults, éditée directement dans l’UI

Leçons retenues

Opter pour l’absence plutôt que la dégradation. Cacher le bouton entièrement sur les appareils non compatibles est plus propre que de l’afficher désactivé avec un message d’explication. L’enseignant ne voit la fonctionnalité que si elle est opérationnelle.

Concentrer les vérifications de version. Toute la logique @available côté ObjC est dans un seul endroit (viewDidLoad et resumer:). Les fichiers Swift n’ont pas à se soucier de la version : leurs annotations @available(iOS 26, *) le garantissent à la compilation.

Exposer une API simple vers ObjC. isIntelligenceAvailable est la seule propriété que le code ObjC doit connaître. Tout le reste (la gestion du modèle, l’interface SwiftUI, l’async/await) reste encapsulé côté Swift.

Maintenir la déportabilité de l’instruction. En stockant l’instruction dans UserDefaults avec une valeur par défaut raisonnable, on évite de coder en dur une formulation qui pourrait évoluer, et on donne à l’enseignant la possibilité d’adapter le résumé à son contexte pédagogique.

Commentaires

Posts les plus consultés de ce blog

malloc: xzm: failed to initialize deferred reclamation buffer

Lorsque je lance une de mes applications avec Xcode sur mon iPhone .... J'ai ce message de début :  malloc: xzm: failed to initialize deferred reclamation buffer (46)  Et franchement, je n'aime pas avoir des message que je ne comprends pas et qui contiennent le mot "failed" ! Mais le message n'existe pas avec mon iPad IOS16. Donc, ok, malloc je connais, on parle de la fonction de base d'allocation de mémoire système, il y a pleins d'outils pour les problèmes d'allocation mémoire, "xzm" doit être un de ces outils. Je ne trouve rien sur internet ! RIEN ! Perplexity ou Claude ne savent pas non plus ce que c'est. Je suis obligé de faire des suppositions... XZ est un format de compression conçu pour remplacer BZIP qui est conçu pour remplacer GZIP qui est conçu pour remplacer Z. Peut-être que les données de debug de Xcode transitent en étant compressé par défaut ? Allons voir dans le schéma du projet, pour voir les options concernant les mallo...

IOS 26 et SafeArea

Sur IOS26 est apparu un bug sur mes vieilles applications. La barre système se retrouve au-dessus de mon application, mais tout revient dans l'ordre après une rotation de l'appareil. Après quelques recherche, c'est un bug IOS26 que de nombreux développeurs ont... Mais sans vraie solution. En fait c'est le SafeArea de la vue principale qui ne contient pas de marge top avant la rotation. Une solution serait d'en mettre une, mais SafeAreaInets est en lecture seule. Il faut utiliser additionalSafeAreaInsets du viewController à la place : -( void ) viewWillAppear :( BOOL )animated { [ super viewWillAppear :animated]; if ( @available (iOS 26 ,*)) { if ( self . view . safeAreaInsets . top == 0 ) { UIEdgeInsets insets = UIEdgeInsetsMake ( 32 , 0 , 0 , 0 );; self . additionalSafeAreaInsets = insets; } } Mais ce n'est pas suffisant, car après rotation le SafeAreaInets est bon, et donc le additionnalSafeAreaInsets doit être remis à zéro : -( void )...

UIScene et openURLContexts

De IOS 2 à IOS 9, lorsque vous envoyiez un fichier dans une application (par exemple à partir d'un fichier attaché en mail), cela lançait l'application et la fonction openURL du délégateur de l'application : -( BOOL ) application :( UIApplication *)application            openURL :( NSURL *)url { Si vous retourniez YES, le fichier était considéré comme lu.  Ensuite de IOS 4.2 à 9.0, Apple à ajouté la possibilité de connaître l'application source du fichier. -( BOOL ) application :( UIApplication *)application            openURL :( nonnull NSURL *)url sourceApplication :( nullable NSString *)sourceApplication annotation :( nonnull id )annotation { Ensuite, avec IOS 9, Apple a ajouté des options afin d'y mettre l'application source, les annotations, mais aussi tout ce qu'ils pourrait vouloir dans le futur. On met un dictionnaire à la place de paramètre, et on évitera ainsi de multiplier les fonctions. -( BOOL ) applic...