0

Structure d’une app iOS

Cet article n'a pas été mis à jour depuis plus d'un an, il est possible que certaines informations ne soient plus à jour. Si vous rencontrez des erreurs ou des différences en le suivant, n'hésitez pas à commenter pour me le signaler.

Salut tout le monde ! Après deux bons mois d’absence active (vacances obligent), nous reprenons en grande force les tutoriels sur notre ami commun : le Swift ! Au programme aujourd’hui, nous allons décortiquer l’armature d’une app iOS. On ne va pas faire de code mais essayer de comprendre comment tout s’organise dans l’appareil pour l’exécution d’une application. Si vous ne l’avez pas fait, je vous recommande de bien lire les bases des projets Xcode avant de vous aventurer ici 🙂

Types de fichiers

Créons un nouveau projet dans Xcode pour une application iOS et choisissons le template (le modèle) “Single View App” qui est le modèle pour une application vide sans contenu pré-rempli. Une fois les paramètres de l’app choisis, on voit dans le navigateur de fichier que des fichiers sont déjà présents.


La première ligne (l’icône de projet bleu) est le conteneur principal des fichiers de l’application. C’est également en cliquant là dessus que l’on va pouvoir accéder aux paramètres de compilation de l’application, notamment la signature de code, les plateformes de distribution, les versions minimales des logiciels, et tout un tas d’options très très utiles, nous y reviendrons plus tard.

On voit ensuite deux dossiers contenus dans le projet, l’un portant le nom de l’application que l’on a choisi lors la création du projet et un dossier “Products”. Vous aurez remarqué que les deux dossiers n’ont pas exactement le même icône pour les représenter, le petit triangle foncé dans l’angle inférieur gauche de l’icône de “Products” indique que le dossier est généré automatiquement par Xcode, mais aussi que les fichiers qu’il contient ne sont pas exactement stockés dedans, par exemple le fichier “Swiftement.app” qui est l’exécutable de mon application ne se trouve pas vraiment dans le dossier “Products”. Voyez vous-même :

C’est donc un moyen pratique de voir ce qui est important dans ce type de dossier sans s’embêter à fouiller dans les méandres de la logique d’Xcode.

On trouve dans le dossier contenant le code des fichiers .swift qui, sans surprise, contiennent du code en Swift, de fichiers .storyboard qui sont des fichiers d’interface, un fichier info.plist contenant un grand nombre d’informations sur l’app et un fichier Assets.xcassets avec un icône de dossier qui est un conteneur pour les images utilisées dans l’app. Voyons en détail ces fichiers.

AppDelegate.swift

Littéralement “représentant de l’application”, ce fichier contient le code permettant le relais entre le système d’exploitation (iOS) et notre app. Examinons rapidement son contenu :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

N.B : j’utilise pour ce tutoriel Xcode 12.5.1 et Swift 5, il est possible que les commentaires ou certaines déclarations chez vous soient légèrement différentes, ce n’est pas important ici.

Ici nous pouvons voir la déclaration de la classe “AppDelegate” qui hérite des classes UIResponder et UIApplicationDelegate. On ne va pas parler du fonctionnement des classes et de la programmation orientée objet dans cette série de tutoriels, je vous renvoie à l’introduction à la programmation orientée objet pour cela. Simplement, il s’agit d’un groupement de fonctions conforme à un certain modèle (celui qui nous est donné par défaut par Xcode, entre autres) permettant l’interaction entre le système et l’app. On indique au système qu’il va se servir des fonctions présentes dans cette classe grâce à la ligne qui précède la déclaration de classe : @main.

Par exemple, lorsque l’on va lancer l’application sur notre appareil, le système va le notifier à cette classe en appelant la fonction func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool une fois le lancement effectué (avec éventuellement des options). On retrouve aussi dans cette classe deux autres méthodes qui seront appelées par le système chaque fois qu’on crée ou que l’on détruit une instance de notre app, une scène.

SceneDelegate.swift

Cette fois-ci, “représentant de scène”, la classe SceneDelegate (héritant de UIResponder et UIWindowSceneDelegate) est le gestionnaire de chaque fenêtre de notre app. En effet, si sur un iPhone on ne va avoir qu’une seule fenêtre de notre app ouverte à la fois, on pourra en trouver plusieurs sur iPad ou sur les apps iOS exécutées sur les derniers Macs avec Catalyst.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let _ = (scene as? UIWindowScene) else { return }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }


}

Ainsi le système interagira avec chacune de nos fenêtres via cette class grâce à des méthodes comme func sceneDidBecomeActive(_ scene: UIScene) qui sera appelée chaque fois qu’une fenêtre devient active ou func sceneDidEnterBackground(_ scene: UIScene) qui sera appelée lorsque l’application entre à l’arrière plan. Nous reviendrons plus en détail dans un article dédié sur cette classe primordiale mais pour l’instant on va se contenter de savoir à quoi elle sers plutôt que de comment on s’en sert !

Main.storyboard et LaunchScreen.storyboard

Les fichiers .storyboard sont des fichiers permettant l’édition visuelle de l’interface de notre application. Par défaut, Xcode nous en fournit deux : un pour avoir un écran de lancement de l’application (LaunchScreen.storyboard) et un pour l’interface principal de notre app (Main.storyboard). Lorsque l’on édite l’un de ces fichiers, Xcode nous l’ouvre par défaut dans l’interface builder qui permet l’affichage et l’édition visuelle du contenu de ces fichiers. En réalité, il s’agit d’un code XML (Wikipedia) qui est seulement interprété par Xcode pour un affichage plus simple, voici ce que l’on y trouve, pour les plus curieux :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
        </scene>
    </scenes>
</document>

Je vous donne ce code mais nous n’aurons pratiquement jamais besoin de nous en servir ou de le modifier, c’est simplement pour vous donner un peu de contexte.

L’utilisation de fichiers storyboard n’est absolument pas nécessaire à la construction d’une app, il s’agit d’une méthode efficace et relativement simple d’utilisation pour créer un interface mais il est également possible d’utiliser SwiftUI ou bien simplement de créer intégralement son interface via du code “classique”.

Revenons à nos moutons, les fichiers storyboard contiennent par défaut un contrôleur de vue (View Controller) représenté par un rectangle blanc dans l’éditeur généralement de la taille d’un écran d’iPhone.

Vue hiérarchique des éléments
Storyboard Entry Point
Vue
Modification de la taille de prévisualisation
Classe du contrôleur de vue

 

On remarque une petite flèche qui pointe vers ce cadre blanc qui symbolise le point d’entrée du Storyboard (Storyboard Entry Point), à savoir il désigne le contrôleur de vue qui sera appelé en premier lors du chargement de cet interface. Vu que pour l’instant nous n’en avons qu’un il pointe obligatoirement sur lui, mais plus tard nous aurons plusieurs contrôleurs différents par storyboard et il faudra indiquer à Xcode lequel charger en premier. Ce contrôleur de vue contient par défaut une vue (View) qui contiendra tout le contenu à afficher. On peut modifier la taille de prévisualisation de notre éditeur en bas pour visualiser l’affichage sur différentes tailles d’écran.

Si on sélectionne le contrôleur de vue (dans la vue hiérarchique, à gauche) pour le fichier Main.storyboard et que l’on affiche le sélecteur d’identité de l’objet on peut alors voir qu’il y a une classe qui lui est attribué, nommée par défaut ViewController (comme c’est original). Cela veut dire (en gros) que le code Swift lié à ce contrôleur sera contenu dans la classe ViewController, on pourra donc y noter toutes les fonctions relatives à ce que nous avons dans notre contrôleur, donc à l’affichage. A coté du nom de cette classe, nous avons d’ailleurs une petite flèche nous permettant d’afficher directement la classe concernée.

Au même endroit dans le fichier LaunchScreen.storyboard il n’y a pas de classe personnalisée d’entrée, ce qui veut dire qu’il n’y a pas de code personnalisé pour ce contrôleur.

ViewController.swift

Si on clique sur cette petite flèche, on ouvre tout de suite le fichier ViewController.swift qui contient la déclaration de la classe dont nous venons de parler. En voici son contenu :

1
2
3
4
5
6
7
8
9
10
11
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }


}

On déclare ici notre classe de type UIViewController qui contient par défaut une fonction viewDidLoad() qui sera appelée une fois le chargement du contrôleur effectué (appelé par le storyboard, en l’occurence). Le ViewController n’a pas le même rôle que le SceneDelegate dans l’app car il va uniquement gérer les vues internes qui lui sont liés au lieu de gérer toute l’instance de la fenêtre actuellement à l’écran. Chaque fois que l’on changera de vue, on changera (le plus souvent) de ViewController mais pas de Scene.

Par exemple si l’on prends une app iOS de base avec une barre d’onglets en bas de l’écran (comme l’app App Store par exemple), on aura un contrôleur de vue pour la vue “onglets”, puis un contrôleur de vue pour chacun des onglets eux-même. En revanche nous n’aurons toujours qu’une seule scène car il n’y a qu’une seule fenêtre C’est donc à cet endroit que nous pourrons écrire le code qui sera exécuté en premier dans notre application une fois l’affichage chargé. Evidemment ici nous ne faisons que les présentations grossières, nous approfondirons bien assez souvent les contrôleurs de vue qui constituent le squelette de notre app et qui sont par conséquent incontournables.

Assets.xcassets

Le fichier assets est la librairie principale d’image de votre application. C’est dans ce “dossier” que seront rangées les images en incluant les diverses tailles et résolutions nécessaire pour les différents appareils. Et oui, chaque appareil va avoir sa résolution d’image native, ce qui est défini par la concentration de pixels sur l’écran. Par exemple, un iPad Air va avoir une résolution de 264 ppp (pixels par pouce, ppi en anglais pour pixels per inch) alors qu’un iPhone X aura une résolution de 458 ppp. Donc évidemment, pour qu’une image ait la même taille lorsqu’elle sera affichée sur un écran ou un autre, il faudra l’enregistrer dans des tailles différentes.


Par exemple, si on ajoute une image au catalogue (via un simple glisser déposé), Xcode va nous permettre de rajouter la même image avec d’autres résolutions, par défaut, il va nous demander une version deux et trois fois plus grande.

L’avantage, c’est que le système sélectionnera automatiquement la bonne version de l’image en fonction de l’appareil qui l’exécute sans que l’on ait à se soucier des tailles d’images et des résolutions d’écran dans le code.



Par défaut, il n’y a qu’un emplacement d’image pré-enregistré qui contiendra toutes les versions de l’icône de l’app. Comme vous pouvez le voir, il y en a beaucoup nécessaires.

Vous pouvez trouver la liste des tailles nécessaires sur le site Apple Developer. Fort heureusement, il y a des applications qui permettent de générer automatiquement des sets d’icônes à partir d’une seule image comme Icon Set Creator dont j’ai déjà parlé.

Info.plist

Le fichier info.plist (pour property list) est un fichier XML contenant des informations relatives à la compilation et l’exécution de l’app. Voici un aperçu de son contenu version code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key>
    <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <false/>
        <key>UISceneConfigurations</key>
        <dict>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneConfigurationName</key>
                    <string>Default Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                    <key>UISceneStoryboardFile</key>
                    <string>Main</string>
                </dict>
            </array>
        </dict>
    </dict>
    <key>UIApplicationSupportsIndirectInputEvents</key>
    <true/>
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UIRequiredDeviceCapabilities</key>
    <array>
        <string>armv7</string>
    </array>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
</dict>
</plist>

Encore une fois, ce ne sera pas nécessaire d’éditer ou d’accéder très souvent à ce code, d’autant que notre bon vieux Xcode nous affiche ce contenu d’une manière bien plus facile de lecture que le code brut. le fichier contient des informations que l’on entre dans les paramètres du projet (l’icône bleu, le premier que nous avons vu), par exemple les informations générales de l’app. Il y a également d’autres informations concernant les capacités de l’app, le sandboxing, etc… On y trouve la propriété UIApplicationSceneManifest qui va indiquer à l’appareil le nom de la classe de la scène gérant nos fenêtres, et ainsi pouvoir la lancer. On remarque aussi la motion “Main storyboard file base name” qui contient la valeur “Main” qui indique donc que le fichier Main.storyboard est le fichier d’interface de base de l’app ou bien UISupportedInterfaceOrientations qui liste dans quelles orientations d’appareil notre app pourra s’adapter.

Résumons

En bref, lorsque l’on clique sur l’icône de notre app pour la lancer sur un iPhone, l’appareil va créer une instance AppDelegate pour gérer toute notre application. Une fois ceci effectué, il va consulter dans info.plist (ce n’est pas vraiment le cas, en fait tout ça est compilé, le fichier info donne juste le chemin pour la compilation mais c’est plus simple expliqué comme ça) quelle Scène doit être lancée, puis quel Storyboard doit y être intégré. Dans ce dernier, l’app va rechercher le point d’entrée puis charger le ViewController associé dans le code. Les interactions utilisateurs sont directement récupérables dans les contrôleurs de vue car le code à exécuter lorsque l’on touche l’écran ne sera pas le même selon le contrôleur affiché, mais ça on le verra une prochaine fois, je vous le donne quand même en exemple pour que vous ayez une vue complète du fonctionnement d’une app de base. Tenez, j’en ai même fait un schéma :

A bien noter que techniquement AppDelegate et SceneDelegate font partie du code de l’application, mais il sont réservé à l’interface avec l’appareil et la gestion globale du fonctionnement de l’app, le contenu n’y figure donc pas.

Je rappelle que cet exemple est donné pour une application extrêmement simple, et même carrément vide, il y a une multitude d’autres choses qui devraient figurer sur ce schéma, mais chaque chose en son temps, il est nécessaire de déjà bien comprendre le fonctionnement de base pour bien organiser son code !

 

J’espère que vous avez tout compris, et si ce n’est pas le cas n’hésitez pas à me le dire, je modifierai en conséquence l’article car ce sujet est assez complexe. Merci à tous pour votre attention et votre fidélité, on se retrouve très bientôt pour de nouvelles aventures !

2 réponses à “Structure d’une app iOS”

  1. Bonjour et merci beaucoup pour votre travail.
    Débutant en programmation je souhaiterai savoir si avec la version de mon Xcode (13.O) j’ai la possibilité de voir dans mon interface Main.storyboard ? Ou la version de votre démonstration et antérieur à ma version ? Si oui comment y accéder ?
    Merci pour votre aide

    1. Bonjour et merci !

      Oui en effet ma version sur cet article est antérieure à la vôtre cependant il vous est toujours possible d’accéder au fichier storyboard si celui-ci a été généré par Xcode lors de la création de l’app. Si vous ne le voyez pas dans la navigateur de fichier (la sidebar à gauche) il est probable que vous ayez créé un nouveau projet avec SwiftUI comme interface. Vous pouvez vérifier qu’il s’agit bien de cela si vous trouvez un fichier ContentView.swift, dans ce cas c’est que vous avez choisi de ne pas vous servir des storyboard lors de la création projet.

      Bien qu’il soit possible de modifier cela une fois le projet créé, il s’agit d’un tâche complexe et précise que je ne saura pas suffisamment vous détailler, je vous conseille dans ce cas là de créer un nouveau projet en vous assurant de bien sélectionner Storyboard lors du choix de l’interface (en dessous du Bundle Identifier) :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *