Swiftly

Techniques modernes de Hot Reloading en iOS/Mac OS

Introduction

Le Hot Reloading ou ‘le rechargement à chaud’ désigne l’affichage en temps réel des modifications UI sans devoir relancer l’application informatique. Cela, permet de gagner énormément en terme de productivité vu qu’on aperçoit en temps réel les effets de notre changement sans recompiler toute l’application.

Le Hot Reloading est connu au niveau des technologies mobiles hybride comme par exemple React Native, Flutter… étant donné que les langages derrière comme JS, Type Script ou Dart sont des langages interprétables. Le Hot Reloading est un peu méconnu dans les langages compilés tel que Swift, Objective-C, Kotlin… qui doivent passer par tout le cycle de compilation et d’édition de liens pour générer l’artefact final.

Dans cette article, j’essayerai d’explorer les techniques actuels de Hot reloading en iOS native. On explorera à travers des projets démos en iOS et Mac OS des frameworks comme InjectionIII et Inject.

Historique des solutions natives

Il n’y a pas de solutions natives offerte par Apple pour réaliser le hot reloading. Si nous comparons l’effort fait par Google à travers Instant run dite maintenant Live Edit, on peut dire que Xcode n’offre rien en ce sujet.

Cependant, la communauté a pu créer plusieurs frameworks dont l’idée de réaliser ce challenge.

  • Dynamic code injection(DyCI): un outil à base d’Objective-C crée par Paul Taykalo.
  • Vaccine: une framework développé par Christoffer Winterkvist et Richard Topchii
  • Inject: un outil crée par Krzysztof Zabłocki.

Toutes ces solutions passent par un logiciel qui permet d’injecter en temps réel les changements du code au niveau de l’application et communiquer avec le système d’exploitation pour réaliser ce challenge. Cette application est InjectorIII Injection for Xcode (InjectionIII): un outil crée par John Holdsworth.

Dans la suite de cette article, on va essayer de prendre un exemple d’application Mac OS à base de SwiftUI et comment on peut utiliser Inject et InjectorIII pour faire du live changes sur notre code.

Installation et configuration de InjectionIII et Inject

  1. Installation de InjectionIII:

Pour pouvoir installer InjectionIII, vous pouvez installer depuis App Store ou depuis GitHub

Une fois installé (l’application est de type menu bar application), vous allez trouvé bien dans l’écran principal

InjectionIII

Vous devez ensuite sélectionner votre projet sur lequel InjectionIII va s’intégrer, pour notre cas ça sera un projet Mac OS, une application que j’ai appelé Reminders pour gérer les notes.

Pour pouvoir, vous cliquez sur l’icône d’InjectionIII ensuite Add directory et vous sélectionnez le dossier qui contient le fichier .xcodeproj

Ajouter un projet native à InjectionIII

Lorsque vous lancez votre application, vous devez avoir un message de connection de injectionIII avec votre application Mac OS

Connection de l’application Reminders avec InjectionIII

Vous remarquez aussi que la couleur de l’icône de InjectionIII a changé vers l’orange ce qui signifie que la liaison est bien faite avec l’application. Mais pour réaliser cette liaison, il faut passer par des étapes avant qu’on va découvrir ensemble.

Configuration d’un fichier AppDelegate.swift

On peut créer un fichier AppDelegate.swift et le lier ensuite avec le fichier de chargement principal de mon application Reminders

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        setupInjector()
    }
    
    private func setupInjector() {
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
    }
}

le code ne sera exécuté que si une instance de AppDelegate sera appelé dans le fichier RemindersApp.swift

@main
struct RemindersApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
}

La fonction load permet au système d’exploitation de charger une application externe genre InjectionIII au moment de l’exécution de l’application en cours. Dans notre cas, on va charger le bundle injectionIII Mac OS qui existe au niveau de ma machine à cette endroit /Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle

Vous pouvez explorer les différents types de bundles supportés, juste en accédant au dossier d’installation de InjectionIII ensuite l’ouvrir (clique droit -> ouvrir paquet )

InjectionIII bundles types

Pour pouvoir créer et lancer automatiquement l’appDelegate, on utilise une propriété wrapper de type @NSApplicationDelegateAdaptor. Vous pouvez consulter la documentation officielle sur ce sujet.

Mais ce n’est pas suffisant pour réaliser la liaison, il faut désactiver certaines capabilities au niveau du fichier entitlement de l’application qui sont:

com.apple.security.app-sandbox et com.apple.security.cs.disable-library-validation

<dict>
	<key>com.apple.security.app-sandbox</key>
	<false/>
	<key>com.apple.security.files.user-selected.read-only</key>
	<true/>
        <key>com.apple.security.cs.disable-library-validation</key>
        <false/>
</dict>

Dernier truc important au niveau de la configuration de InjectionIII avec le projet, il est nécessaire d’ajouter les flags -Xlinker et interposable afin de demander au compilateur d’authoriser l’action de recompilation d’un fichier qui existe en mémoire centrale et de remplacer en runtime celui qui est déjà en exécution sinon vous aurez ce message.

Vous ajoutez les deux au niveau du Build settings -> Other linker Flags

-XLinker et -interposable flags

Vu que notre projet est basé sur SwiftUI et non pas AppKit, il faut essayer de recharger l’interface swiftUI à chaque fois qu’il y un changement dans l’interface SwiftUI. A ce moment , on aura besoin de Inject la bibliothèque qui intercepte les changements et demande à InjectionIII de recompiler et recharger l’application.

  1. Installation de Inject

Inject est une framework open source à base Swift qu’on peut installer à travers CocoaPods ou SPM.

Inject via SPM

Il suffit ensuite d’importer au niveau de votre vue SwiftUI puis de déclarer un type par exemple dans notre cas un InjectionObserver qui notifiera InjectionIII à travers une notification spéciale connue par InjectionIII, le INJECTION_BUNDLE_NOTIFICATION, pour recompiler la vue en cours

public let injectionObserver = InjectionObserver()

public class InjectionObserver: ObservableObject {
    @Published var injectionNumber = 0
    var cancellable: AnyCancellable?
    let publisher = PassthroughSubject<Void, Never>()
    init() {
        cancellable = NotificationCenter.default.publisher(for:
            Notification.Name("INJECTION_BUNDLE_NOTIFICATION"))
            .sink { [weak self] _ in
            self?.injectionNumber += 1
            self?.publisher.send()
        }
    }
}

Appel de InjectionObserver dans une vue swiftUI

struct MyView: View {    
#if DEBUG
    @ObservedObject var iO = injectionObserver
#endif

Maintenant, on peut modifier dans notre code et vous allez remarquez les changements en temps réel.

Dans notre exemple, on a un fichier SideBarView.swift qui contient un text, une liste et un button en bas, on vas essayer de mettre en commentaire le code du Button de cette manière. On peut voir sans recompiler que le button se cache.

var body: some View {
        VStack(alignment: .leading) {
            Text("My list")
            MyListsView(viewModel: MyListsViewModel(context: context))
            Spacer()
            Button {
                isPresented = true
            }label: {
                HStack {
                    Image(systemName: Constants.Icons.plusCircle)
                    Text("Add List")
                }
            }.buttonStyle(.plain)
                .padding()
        }.sheet(isPresented: $isPresented) {
            // dismiss
        } content: {
            AddNewListView(viewModel: AddNewListViewModel(context: context))
        }.enableInjection()
    }
SideBar avant la modification

InjectionIII recompile et recharge la vue 🙂

Recompilation du fichier SideBarView.swift

Limites et contraintes

Lorsque j’ai essayé d’utiliser injectionIII et Inject sur un grand projet avec plusieurs frameworks et packages j’ai eu des problèmes à l’exploiter vu que le changement dans les couches inférieurs n’impactent pas instantanément la partie UI. J’étais obliger de faire un aller retour par exemple pour voir ma liste de produits se rafraîchir. En autre, j’ai pas pu l’exploiter sur un device apparemment et je suis pas le seul.

A très bientôt pour un nouveau sujet 🙂

Leave a Comment

Résoudre : *
2 + 21 =


fr_FRFrench