Les coulisses de Renala
18 articles sur la création d'un analyseur de disque macOS en Swift et SwiftUI. De la première maquette à l'App Store. Choix de conception, optimisation des performances, rendu de treemaps, et ce qu'il faut pour publier une app Mac native en tant que développeur solo.
Partie 1 : Fondation
Pourquoi j'ai créé un analyseur de disque
Choisir un projet au périmètre défini, aux algorithmes intéressants et au résultat visible. Comment la frustration face aux outils existants a mené à la création de Renala, un analyseur d'espace disque pour macOS.
Hello, Xcode
Je m'attendais à un compilateur. J'ai trouvé une religion. XcodeGen, deux systèmes de build, la signature de code, les entitlements et le Makefile comme issue de secours.
Le problème du treemap
C'est un rectangle. À l'intérieur, des rectangles plus petits. Implémentation des treemaps squarifiés d'après l'article de Bruls/van Wijk de 1999, et pourquoi les couleurs plates ne suffisent pas.
Partie 2 : La plateforme
Swift 6 et le vertige de la concurrence
Le compilateur m'a dit que mon code était faux. Il avait raison. @MainActor, Sendable, l'isolation des acteurs et les bugs que la concurrence stricte de Swift 6 a détectés avant la publication.
SwiftUI n'est pas React
Je cherchais useState. Il n'existe pas. Le modèle @Observable, MVVM en pratique et l'API Canvas pour le rendu en mode immédiat dans un framework déclaratif.
Peindre des pixels : ombrage de Lambert
J'ai passé trois jours sur des pixels. Ça valait le coup. Ombrage de Lambert pixel par pixel, pourquoi le Canvas SwiftUI ne gère pas ça nativement et le pipeline de Task en arrière-plan.
Partie 3 : Performance
Le goulot d'étranglement du scanner
44 secondes. Puis 26 secondes. Un seul appel système a tout changé. Les coûts cachés de FileManager, getattrlistbulk et l'arithmétique de pointeurs bruts sur macOS.
Un battement de cœur pour le thread principal
Si on ne peut pas le mesurer, on ne peut pas le corriger. PerfLogWriter, détection de MAIN_THREAD_BLOCKED et le pattern de flush par lots de 250 ms qui garde l'UI réactive.
D'un gigaoctet à quatre cents mégaoctets
J'avais 5,2 millions d'objets FileNode. Ils étaient superflus. UUID vers UInt32, chemins reconstruits depuis parent + nom et le NodeStore plat qui a remplacé l'arbre.
Trouver les doublons à grande échelle
Hasher 300 Go de fichiers sur un portable sans le faire planter. Le pipeline multi-phases, le TaskGroup à 6 workers et les gains de débit par la mise à jour groupée de l'UI.
Tester une application visuelle
526 tests. Aucun ne regarde un pixel. Tests d'algorithme, tests du scanner sur de vrais fichiers, garde-fous de performance O(n) et pourquoi les vues SwiftUI ne sont délibérément pas testées.
Partie 4 : Publier
Onze langues, un seul bouton
Personne ne livre 11 langues dès le premier jour. Je l'ai fait quand même. Changement de langue en direct, pipeline de traduction Python, support RTL et catégories de pluriel arabe.
La sandbox applicative
On ne peut pas ouvrir un fichier sans demander la permission. À chaque fois. Les bookmarks à portée de sécurité, NSHomeDirectory() qui renvoie le mauvais chemin et pourquoi lecture-écriture compte.
Le parcours du combattant App Store
Je pensais que construire l'app était le plus dur. Privacy Manifest, StoreKit 2, captures d'écran en 2880x1800 et le pipeline CI/CD qui gère la signature, la notarisation et la soumission.
Rejeté
Deux motifs de rejet. L'un était de ma faute. L'autre, presque. Un crash sur macOS Sequoia causé par une closure @MainActor sur une file d'arrière-plan et un pot à pourboires vide pendant la review.
En attente
L'application est dans la file d'attente. Six avis, quatre versions, trois semaines. Ce que le fossé entre « terminé » et « publié » coûte vraiment.
Annexes
Où s'exécute le néant ?
Quand on passe une fermeture vide à une API, qui décide sur quel thread elle s'exécute ? Comparaison entre JavaScript, C#, Java, Kotlin et Swift 6.
Comment SwiftUI décide quoi redessiner
SwiftUI suit les propriétés que chaque vue lit et saute du travail quand les entrées n'ont pas changé. Les closures cassent ce modèle. Des références stables de ViewModel le corrigent.