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 :
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 :
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.
-
Developer Apple, « NavigationStack », s.d., https://developer.apple.com/documentation/swiftui/navigationlink
-
Developer Apple, « JSONDecoder », s.d., https://developer.apple.com/documentation/foundation/jsondecoder
-
Developer Apple, « URLSession », s.d., https://developer.apple.com/documentation/foundation/urlsession
-
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/
-
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
-
Hacking with Swift, « Parsing JSON using the Codable protocol », s.d., https://www.hackingwithswift.com/read/7/3/parsing-json-using-the-codable-protocol
Commentaires3
Super intéressant de voir…
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 !
Commentaire
Merci, c'était super intéressant. Je voulais savoir comment tu gérais les erreurs pour afficher les messages d'erreur à l'utilisateur
Article super intéressant !…
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.