Swift Full-stack : SwiftUI et Vapor

Par ybenkhayat, 22 mars, 2025
logo swift

Plan

Dans le blogue précédent, nous avons comment vu Vapor, sa comparaison avec HummingBird et comment créer un projet Vapor et configurer une API Rest. Dans le blogue suivant, il sera question de connecter l’API Vapor que nous avons créé au blogue précédent avec l’application SwiftUI des blogues d’avant. Comme dans les blogues précédents, le code produit dans le cadre de ce blogue est disponible dans ce dépôt GitHub.

Dans le blogue de la semaine 5, nous avons vu différents types de vues, comme NavigationView et NavigationStack, et avons entre autres affiché une liste d’animaux. Nous allons réutiliser ce même modèle (Animal), et donc l’afficher dans l’application SwiftUI avec une requête GET dans l’API Vapor, qui récupèrera la liste des animaux.

Expérimentation

Du côté de l’API

Nous allons d’abord ajouter le même modèle (animal) qui se trouve dans l’application SwiftUI, dans un nouveau fichier Animal.swift :

import Vapor

struct Animal: Content {
    let id: Int
    let nom: String
    let couleur: String
    let nomImage: String
}

Ensuite, dans le fichier routes.swift, nous ajouterons le nouveau point d’accès /animaux qui retournera toute la liste d’animaux, ainsi que animal/{id} qui retournera l’animal qui correspond à l’id donné en paramètre :

Voici le fichier routes.swift que vous devriez donc avoir :

import Fluent
import Vapor

func routes(_ app: Application) throws {
    app.get { req async in
        "It works!"
    }

    app.get("hello") { req async -> String in
        "Hello, world!"
    }
    
    app.get("hello", ":nom") { req async throws -> String in
        
        let nom = try req.parameters.require("nom")
        
        return "Hello, \(nom.capitalized)!"
    }
    
    app.get("animaux") { req async -> [Animal] in
        return [
            Animal(id: 1, nom: "Chaton", couleur: "blue", nomImage: "image1"),
            Animal(id: 2, nom: "Chien", couleur: "yellow", nomImage: "image2"),
            Animal(id: 3, nom: "Hamster", couleur: "gray", nomImage: "image3")
        ]
    }
    
    app.get("animaux", ":id") { req async throws -> Animal in
        guard let idString = req.parameters.get("id"),
              let id = Int(idString) else {
            throw Abort(.badRequest, reason: "id invalide")
        }
        
        let animals = [
            Animal(id: 1, nom: "Chaton", couleur: "blue", nomImage: "image1"),
            Animal(id: 2, nom: "Chien", couleur: "yellow", nomImage: "image2"),
            Animal(id: 3, nom: "Hamster", couleur: "gray", nomImage: "image3")
        ]
        
        if let animal = animals.first(where: { $0.id == id }) {
            return animal
        } else {
            throw Abort(.notFound, reason: "Animal avec l'id \(id) non existant")
        }
    }

    try app.register(collection: TodoController())
}

Lorsqu’on lance la requête curl http://localhost:8080/animaux, on devrait obtenir tous les animaux, et avec curl http://localhost:8080/animaux/1, on devrait obtenir l’animal avec l’id 1.

Résultat :

Alt Text

Du côté de l’application iOS

Nous allons d’abord ajouter un nouveau fichier AnimalViewModel.swift qui permettra de lancer les requêtes avec URLSession, un objet permettant de lancer des requête avec des urls fournis en paramètre.³

Dans ce même fichier, on décodra les JSON retournés des requêtes avec le décodeur JSON JSONDecoder puis ensuite récupérer les animaux dans une liste animaux qu’on utilisera dans notre vue principale : ²

import Foundation

class AnimalViewModel: ObservableObject {
    @Published var animaux: [Animal] = []
    
    func fetchAnimals() {
        guard let url = URL(string: "http://localhost:8080/animaux") else {
            print("URL invalide")
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Erreur lors de la récupération des animaux: \(error)")
                return
            }
            guard let data = data else {
                print("Aucun donnée reçu")
                return
            }
            do {
                let decodedAnimals = try JSONDecoder().decode([Animal].self, from: data)
                DispatchQueue.main.async {
                    self.animaux = decodedAnimals
                }
            } catch {
                print("Erreur décodage: \(error)")
            }
        }.resume()
    }
}

Puisque dans le JSON on ne peut pas avoir de type Color, et qu’on utilise String à la place dans le projet Vapor, on modifiera le modèle animal pour garder la notion de Color tout en changeant l’attribut couleur au type String pour ne pas avoir de problème lors du décodage JSON. On obtient donc le code suivant pour le model Animal.swift :

import SwiftUI

struct Animal: Codable, Identifiable, Hashable{
    let id: Int
    let nom: String
    let couleur : String
    let nomImage : String
    
    var swiftUIColor: Color {
        switch couleur.lowercased() {
        case "blue": return .blue
        case "yellow": return .yellow
        case "gray": return .gray
        default: return .primary
        }
    }
}

Finalement, nous mettrons à jour le fichier de la vue afin d’utiliser le AnimalViewModel que nous avons créé pour récupérer les animaux de l’API :

import SwiftUI

struct ChatonVue: View {
    @StateObject var viewModel = AnimalViewModel()
    
    var body: some View {
        NavigationStack {
            List {
                Section("Liste d'animaux") {
                    ForEach(viewModel.animaux, id: \.id) { animal in
                        NavigationLink(value: animal) {
                            HStack {
                                Image(animal.nomImage)
                                    .resizable()
                                    .scaledToFit()
                                    .frame(width: 50, height: 50)
                                    .clipShape(Circle())
                                    .overlay {
                                        Circle().stroke(animal.swiftUIColor, lineWidth: 2)
                                    }
                                Text(animal.nom)
                                    .offset(x: 5)
                                    .foregroundColor(animal.swiftUIColor)
                            }
                        }
                    }
                }
            }
            .navigationTitle("Mes animaux")
            .navigationDestination(for: Animal.self) { animal in
                ZStack {
                    animal.swiftUIColor.ignoresSafeArea()
                    HStack {
                        Image(animal.nomImage)
                            .resizable()
                            .scaledToFit()
                            .frame(width: 100, height: 100)
                            .clipShape(Circle())
                            .overlay {
                                Circle().stroke(.black, lineWidth: 2)
                            }
                        Text(animal.nom)
                            .offset(x: 5)
                            .foregroundColor(.black)
                            .font(.largeTitle)
                            .bold()
                    }
                }
            }
            .onAppear {
                viewModel.fetchAnimals()
            }
        }
        .accentColor(Color(.label))
    }
}

#Preview {
    ChatonVue()
}

On obtient ainsi le résultat suivant :

Alt Text

Conclusion

Dans ce blogue qui achève la série des blogues d'apprentissage de Swift en développement iOS, nous avons pu mettre en pratique la majorité de ce qu'on a appris ensemble lors des sept dernières semaines. J'espère que vous avez, comme moi, appris davantage sur Swift, et que vous continuerez à en apprendre par vous-même, même si c'est la fin de ces blogues.


  1. Developer Apple, « NavigationStack », s.d., https://developer.apple.com/documentation/swiftui/navigationlink

  2. Developer Apple, « JSONDecoder », s.d., https://developer.apple.com/documentation/foundation/jsondecoder

  3. Developer Apple, « URLSession », s.d., https://developer.apple.com/documentation/foundation/urlsession

  4. Antoine VAN DER LEE, « JSON Parsing in Swift explained with code examples », dans SwiftLee, 13 février 2025, https://www.avanderlee.com/swift/json-parsing-decoding/

  5. ElAmir MANSOUR, « Mastering URLSession in Swift: A Comprehensive Guide », dans Medium, 13 janvier 2024, https://elamir.medium.com/mastering-urlsession-in-swift-a-comprehensive-guide-d3a3aa740f6e

  6. Hacking with Swift, « Parsing JSON using the Codable protocol », s.d., https://www.hackingwithswift.com/read/7/3/parsing-json-using-the-codable-protocol

Étiquettes

Commentaires3

adelaa

il y a 3 semaines 3 jours

Super intéressant de voir comment tu fais le lien entre Vapor et SwiftUI de façon aussi fluide ! J’aime bien la clarté du modèle Animal et l’intégration avec la vue. Petite question : tu envisages aussi de gérer la création ou modification d’un animal depuis SwiftUI ? Ce serait cool de voir comment tu gères les requêtes POST ou PUT !

mboudemagh

il y a 3 semaines 3 jours

Merci, c'était super intéressant. Je voulais savoir comment tu gérais les erreurs pour afficher les messages d'erreur à l'utilisateur

rkassan

il y a 3 semaines 2 jours

Article super intéressant ! est-ce qu’on pourrait utiliser une base de données avec Fluent pour stocker les animaux au lieu de les coder en dur dans routes.swift ? Ça permettrait de rendre l’API plus dynamique.