Tous les articles
10 min·

iOS 26 a livré 5 améliorations discrètes en SwiftUI

5 nouveautés SwiftUI discrètes d'iOS 26 : navigationSubtitle, searchable breaking change, ToolbarSpacer, scrollEdgeEffectStyle et openURL in-app.

Par Carolane Lefebvre
SwiftUIiOS
iOS 26 a livré 5 améliorations discrètes en SwiftUI

Liquid Glass a dominé le keynote de la WWDC 2025. Les tweets, les threads, les vidéos YouTube, tout le monde n'a parlé que de ça. Mais pendant que l'écosystème débattait sur le design translucide, Apple a glissé dans iOS 26 une poignée d'améliorations SwiftUI concrètes qui vont changer ton code au quotidien bien avant que tu touches au moindre effet verre.

Ces ajouts ne sont pas spectaculaires. Ils ne méritent pas un tweet viral. Mais ils résolvent des problèmes réels que les développeurs iOS contournaient avec des hacks depuis des années. Un sous-titre de navigation qu'on bricolait en custom, une barre de recherche qui change de position sans prévenir, un espacement de toolbar qu'on simulait avec des boutons invisibles.

Voici cinq améliorations qui valent le détour, avec le contexte sur pourquoi Apple les a introduites et les pièges à connaître.


Depuis les premières versions d'iOS, les apps Apple affichent des sous-titres dans la barre de navigation. Mail montre le nombre de messages non lus sous le nom du dossier. Notes affiche la date de dernière modification. Fichiers indique le nombre d'éléments. Mais en tant que développeur tiers, tu ne pouvais pas faire ça proprement.

La solution classique, c'était de créer une UIView custom avec un UILabel principal et un UILabel secondaire, puis de l'injecter via navigationItem.titleView en UIKit. En SwiftUI, c'était encore pire : tu devais passer par un UIViewControllerRepresentable ou utiliser .toolbar avec un ToolbarItem(placement: .principal) contenant un VStack, ce qui cassait les animations de transition et le comportement inline/large title.

Avec iOS 26, c'est un modifier d'une ligne :

NavigationStack {
    DocumentView()
        .navigationTitle("Q3 Roadmap")
        .navigationSubtitle("Modifié il y a 5 min")
}

Le sous-titre s'affiche juste sous le titre principal, avec la bonne taille de police, la bonne couleur secondaire, et les bonnes animations de transition quand tu passes du large title au inline. Tout est géré par le système.

Les cas d'usage concrets sont nombreux. Une app de gestion de documents peut afficher "Dernière sauvegarde il y a 2 min" pour rassurer l'utilisateur. Une app de messagerie peut montrer "En ligne" ou "Vu à 14h32" sous le nom du contact. Une app de suivi de projets peut indiquer "3 tâches restantes" sous le nom du sprint.

.navigationSubtitle(document.isModified ? "Modifications non sauvegardées" : "À jour")

Le sous-titre est réactif : tu peux le lier à un @State ou un @Observable et il se met à jour en temps réel. C'est le genre de micro-détail qui rend une app plus professionnelle sans effort.

Un point à noter : le sous-titre n'apparaît que quand le titre est en mode inline (quand l'utilisateur a scrollé). En mode large title, il n'est pas affiché. C'est cohérent avec le comportement des apps Apple.

.searchable : Le breaking change silencieux

Celui-ci est critique. C'est le genre de changement qui casse ton app sans que tu t'en rendes compte si tu ne recompiles pas et testes sur iOS 26.

Sur iOS 25, quand tu utilisais .searchable sans spécifier de placement, la valeur .automatic se résolvait vers .navigationBarDrawer, la barre de recherche apparaissait en haut, sous le titre de navigation, dans le tiroir classique. C'est le comportement que tout le monde connaît depuis iOS 16.

Sur iOS 26, .automatic se résout vers le bas de l'écran. La barre de recherche apparaît en bas, proche du pouce, dans un style inspiré de Safari et Plans.

// Ce code se comporte différemment sur iOS 25 et iOS 26
NavigationStack {
    List(items) { item in
        ItemRow(item)
    }
    .searchable(text: $query)  // Haut sur iOS 25, bas sur iOS 26
}

Pourquoi Apple a fait ce changement ? Deux raisons. D'abord, l'ergonomie : sur les écrans de plus en plus grands (6.7 pouces sur l'iPhone 16 Pro Max), la barre de recherche en haut nécessite de changer de prise en main. En bas, elle est accessible au pouce. Ensuite, la cohérence : Safari, Plans et Spotlight utilisent déjà une barre de recherche en bas. Apple unifie l'expérience.

Le problème, c'est que si tu as conçu tes écrans en supposant que la barre de recherche serait en haut, par exemple si tu as un header custom juste en dessous, ton layout est cassé. La barre de recherche a bougé de 600 pixels sans que tu aies touché une ligne de code.

La solution est simple mais il faut la connaître : spécifie explicitement le placement.

// Pour garder l'ancien comportement (haut de l'écran)
.searchable(text: $query, placement: .navigationBarDrawer)

// Pour adopter le nouveau comportement (bas de l'écran)
.searchable(text: $query, placement: .bottomBar)

Mon petit conseil : fais un audit de tous tes écrans qui utilisent .searchable. Cherche dans ton projet avec un simple grep. Pour chaque occurrence sans placement explicite, décide si tu veux le comportement haut ou bas, et spécifie-le. Ne laisse pas .automatic décider pour toi, son comportement a changé une fois, il peut changer encore.

C'est un pattern qu'on voit de plus en plus chez Apple : les valeurs .automatic évoluent avec les versions d'iOS. Si tu comptes sur un comportement implicite, tu t'exposes à ce genre de surprise. Mieux vaut être explicite.

ToolbarSpacer : Fini les boutons invisibles

Avant iOS 26, grouper visuellement les actions dans une toolbar, c'était un cauchemar de hacks. Tu voulais séparer les actions de contenu (Edit, Sort) des actions de validation (Share, Done) ? Tes options étaient limitées.

Le hack le plus courant : ajouter un ToolbarItem contenant un Button invisible ou un Spacer() avec un frame fixe. Ça marchait... parfois. Ca cassait sur certaines tailles d'écran. Ca générait des warnings d'accessibilité parce que VoiceOver détectait un bouton sans label. Bref, c'était sale.

// Avant iOS 26 -- le hack
.toolbar {
    ToolbarItem { Button("Edit") { editAction() } }
    ToolbarItem { Button("Sort") { sortAction() } }
    ToolbarItem {
        // Hack : bouton invisible pour creer un espace
        Button(" ") { }.disabled(true).opacity(0)
    }
    ToolbarItem { Button("Share") { shareAction() } }
    ToolbarItem { Button("Done") { doneAction() } }
}

Avec ToolbarSpacer, c'est propre :

// iOS 26 -- la bonne façon
.toolbar {
    ToolbarItem { Button("Edit") { editAction() } }
    ToolbarItem { Button("Sort") { sortAction() } }
    ToolbarSpacer()
    ToolbarItem { Button("Share") { shareAction() } }
    ToolbarItem { Button("Done") { doneAction() } }
}

Le ToolbarSpacer insère une séparation visuelle sémantique. Ce n'est pas juste un espace vide, c'est une indication au système que ces deux groupes d'actions sont distincts. Edit et Sort concernent la manipulation du contenu. Share et Done concernent le flux de validation. Le spacer rend cette distinction visible et compréhensible.

Cas d'usage concret : une app de traitement photo avec une toolbar complexe. À gauche, les outils de dessin (pinceau, gomme, pipette). À droite, les actions globales (annuler, sauvegarder, exporter). Le ToolbarSpacer entre les deux groupes rend l'interface immédiatement lisible.

Un autre exemple : une app de prise de notes avec "Bold", "Italic", "Underline" d'un côté et "Undo", "Redo" de l'autre. Le groupement visuel aide l'utilisateur à comprendre la structure de l'interface sans réfléchir.

💡 Le spacer est aussi compatible avec VoiceOver : il ne génère pas d'élément focusable parasite, contrairement aux hacks de boutons invisibles !

.scrollEdgeEffectStyle() : Le polish qui fait la différence

Quand du contenu scrolle sous une barre (navigation bar, tab bar), la transition entre le contenu visible et la barre peut être abrupte. Le texte ou les images sont coupés net, sans transition. Ca fonctionne, mais ça manque de finesse.

Avec iOS 26, Apple introduit .scrollEdgeEffectStyle() qui contrôle comment le contenu s'estompe au bord d'un scroll view. Deux options :

  • .soft : un fondu flou progressif. Le contenu s'estompe graduellement avant de passer sous la barre. C'est le nouveau défaut sur iOS 26.

  • .hard : une coupure nette, comme avant. Le contenu est visible puis soudainement masqué.

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemCard(item)
        }
    }
}
.scrollEdgeEffectStyle(.soft, for: .top)

Pourquoi Apple a ajouté ça ? Avec Liquid Glass, les barres de navigation deviennent translucides. Le contenu qui défile dessous est partiellement visible à travers l'effet verre. Sans fondu, le résultat est visuellement brouillon : du texte tronqué visible à travers une barre translucide. Le fondu .soft résout ce problème en faisant disparaître progressivement le contenu avant qu'il n'atteigne la barre.

Tu peux appliquer l'effet sur les deux bords :

.scrollEdgeEffectStyle(.soft, for: .top)
.scrollEdgeEffectStyle(.hard, for: .bottom)

Quand utiliser .hard plutôt que .soft ? Quand tu affiches des données tabulaires (un tableau de chiffres, une grille de données) où le fondu rendrait les dernières lignes illisibles. Ou quand ton design a une ligne de séparation nette et que le fondu créerait une double transition. Le .hard est aussi préférable dans les apps utilitaires type calculatrice ou chronomètre où la précision visuelle prime sur l'esthétique.

L'effet est subtil, presque imperceptible pour l'utilisateur. Mais c'est exactement le genre de micro-polish qui sépare une app "correcte" d'une app qui fait native. Les reviewers App Store le remarquent. Les captures d'écran en bénéficient. Et les utilisateurs le ressentent sans savoir pourquoi ton app "fait Apple".

openURL avec prefersInApp : Fini le SFSafariViewController bricolé

Ouvrir un lien web dans une app iOS, ça a toujours été plus compliqué que ça devrait. Avant iOS 26, tu avais trois options, aucune satisfaisante.

  • Option 1 : UIApplication.shared.open(url). L'utilisateur est éjecté vers Safari. Il perd le contexte de ton app. Pour revenir, il doit retrouver ton app dans le multitâche ou la relancer. C'est la pire expérience utilisateur possible pour un simple lien "Conditions d'utilisation".

  • Option 2 : SFSafariViewController. Un navigateur in-app natif qui garde l'utilisateur dans ton app. Bonne UX, mais en SwiftUI, il faut l'encapsuler dans un UIViewControllerRepresentable, gérer la présentation, le dismiss, et les callbacks. C'est 30-50 lignes de boilerplate pour afficher une page web.

  • Option 3 : WKWebView custom. Encore plus de boilerplate. Tu gères le chargement, les erreurs, la navigation, les cookies. Sauf si tu construis un vrai navigateur in-app, c'est overkill.

iOS 26 résout ça avec un seul paramètre :

@Environment(\.openURL) var openURL

Button("Lire la documentation") {
    if let url = URL(string: "https://developer.apple.com") {
        openURL(url, prefersInApp: true)
    }
}

Quand prefersInApp est à true, le système ouvre un navigateur in-app natif (visuellement identique à SFSafariViewController) sans aucun wrapping. L'utilisateur reste dans ton app, lit le contenu, et ferme le navigateur avec un simple bouton. Zéro boilerplate.

Les cas d'usage sont partout. Les liens vers les conditions d'utilisation dans un flow d'onboarding. Les liens vers la documentation dans une app développeur. Les liens "En savoir plus" dans une fiche produit e-commerce. Les liens vers des articles dans une app d'actualités. À chaque fois que tu veux que l'utilisateur consulte un contenu web sans quitter ton app, prefersInApp: true est la réponse.

// Exemple : écran de profil avec liens externes
VStack(spacing: 16) {
    Button("Portfolio") {
        openURL(user.portfolioURL, prefersInApp: true)
    }
    Button("Conditions d'utilisation") {
        openURL(termsURL, prefersInApp: true)
    }
    Button("Politique de confidentialité") {
        openURL(privacyURL, prefersInApp: true)
    }
}

Un point important : le paramètre s'appelle prefersInApp, pas opensInApp. C'est une préférence, pas une garantie. Le système peut décider d'ouvrir dans Safari si le navigateur in-app n'est pas disponible. En pratique, ça fonctionne toujours sur iPhone, mais c'est bon de le savoir.

L'impact sur la rétention est réel. Chaque fois qu'un utilisateur quitte ton app pour Safari, il y a un risque qu'il ne revienne pas (notification, autre app, distraction). Garder l'utilisateur dans ton app pour un simple lien, c'est du bon sens UX que tu peux maintenant implémenter en une ligne.


À retenir

Aucun de ces ajouts n'a eu droit au keynote. Aucun ne fera de thread viral. Mais ils résolvent des vrais problèmes que les développeurs SwiftUI traînaient depuis des années. .navigationSubtitle() remplace un hack UIKit. Le changement de .searchable est un breaking change silencieux qu'il faut adresser avant de livrer sur iOS 26. ToolbarSpacer élimine les boutons fantômes. .scrollEdgeEffectStyle() apporte le polish que Liquid Glass exige. Et openURL(prefersInApp:) supprime 40 lignes de boilerplate pour un cas d'usage universel.

Liquid Glass fait les gros titres. Ces cinq améliorations, c'est ce que tu utiliseras vraiment au quotidien, et n’oublie pas : have fun coding!

Commentaires

Connecte-toi pour laisser un commentaire.

Aucun commentaire pour le moment. Sois le premier !

Voir d'autres articles

.task vs .onAppear vs .refreshable en SwiftUI

safeAreaInset vs safeAreaBar en SwiftUI

5 modifiers SwiftUI pour maitriser vos Sheets