Avec iOSSystème d'exploitation des appareils Apple. 14, Apple revoit sa copie concernant les Widgets. Dans les versions antérieures d’iOS, ils étaient tous situés dans une page dédiée, à gauche de l’écran d’accueil. Maintenant, ils peuvent être placés n’importe où sur l’écran d’accueil ! Ce qui laisse aux développeurs, la possibilité de revoir leur copie afin de prendre en compte ce nouveau comportement. Dans ce tuto, nous allons voir comment faire pour implémenter un widget dans notre application iOS.
Nous allons créer un Widget qui va nous permettre de récupérer la dernière release de Swift via l’APIUne API est un programme permettant à deux applications distinctes de communiquer entre elles et d’échanger des données. publique de GitHub.
Avant de démarrer, sachez qu’il existe 2 types de Widgets sur iOS 14 :
StaticConfiguration
qui sont des Widgets statiques, c’est-à-dire qu’il ne peut y avoir aucune configuration utilisateur directement depuis le Widget. Cependant, vous pouvez récupérer des informations depuis l’application parente.IntentConfiguration
qui eux, à l’inverse, sont dynamiques. Ils peuvent récupérer des informations depuis un ’Intent’, ce qui permet de rendre le widget configurable (par exemple, une localisation précise pour une application de météo).Dans ce tutoriel, nous allons voir les deux types ! A vos claviers, c’est parti !
Ouvrez Xcode (Il faut xCode 12 minimum), créez un nouveau projet App
. Je vous laisse donner le nom que vous voulez à votre projet, de mon côté, je l’appellerai tout simplement GitWidget
.
Le projet maintenant créé, nous allons directement ajouter à l’application une extension qui va nous permettre de créer un widget.
Pour cela, allez dans le fichier de configuration du projet, et ajoutez une target. Il faut ensuite chercher Widget
et ajouter l’extension éponyme. Cliquez sur Next
.
Il faut ensuite nommer votre extension, de mon côté WidgetExtension
. Je vous invite à décocher Include Configuration Intent
, on va s’en occuper manuellement plus tard. Cliquez sur Finish
.
Une popup vous demandant si vous souhaitez activer le scheme qui a été créé pour l’extension s’affiche, cliquez sur Activate
.
Vous pouvez essayer de lancer l’extension, un widget va s’ajouter sur votre écran d’accueil qui contient... l’heure !
Au niveau du projet, un dossier au nom de votre extension a été créé. A l’intérieur de celui-ci vous trouverez 3 fichiers :
WidgetExtension.swift
-> C’est le fichier principal qui contient les structures nécessaires au lancement de notre widget.Assets.xcassets
-> Ce sont les assets de notre extension.Info.plist
-> C’est le fichier de préférence de notre extension.Pour la suite du tutoriel, et pour qu’on comprenne bien ce que font les classes, supprimez le fichier WidgetExtension.swift
.
Afin de mieux nous organiser pour la suite, je vous invite à créer 2 dossiers à l’intérieur du dossier de votre extension :
Class
Views
Un widget n’ayant pas besoin d’être actualisé dès qu’il s’affiche, Apple passe par un système de Timeline. Cela veut dire que c’est à vous de demander à iOS de mettre à jour votre Widget, en donnant une date d’expiration à l’instance du Widget.
Cela peut également être utile pour une application météo (pour ne citer qu’elle), car ce genre d’application peut anticiper l’affichage de son widget (avec certes, plus ou moins de fiabilité...).
Dans notre cas, nous allons utiliser un délai d’expiration fixe (toutes les 2 minutes pour le développement), car nos données peuvent changer.
Pour créer la timeline, nous devons créer une classe qui hérite du protocole TimelineEntry
. Cette classe a besoin d’une date (la fameuse date d’expiration), on lui passe également les informations nécessaires à la configuration de notre Widget.
Il faut sélectionner la target de l’extension pour les nouveaux fichiers.
Nouveau fichier Class/ReleaseTimelineEntry.swift
:
import WidgetKit
struct ReleaseTimelineEntry: TimelineEntry { var date: Date var gitRelease: GitRelease }
Nouveau fichier Class/GitRelease.swift
:
struct GitRelease: Codable {
let id: Int
let tagName: String
let targetCommitish: String
let name: String
let author: GitAuthor
let createdAt: String
let publishedAt: String
enum CodingKeys: String, CodingKey {
case id, name, author
case tagName = "tag_name"
case targetCommitish = "target_commitish"
case createdAt = "created_at"
case publishedAt = "published_at"
}
}
Nouveau fichier Class/GitAuthor.swift
:
struct GitAuthor: Codable {
let id: Int
let login: String
}
Il faut également que nous récupérions les informations depuis l’API publique de GitHub. Nous allons donc créer une structure qui fait cela :
Nouveau fichier GitHubServices.swift
:
struct GitHubServices {
static func getAppleSwiftLatestRelease(completion: @escaping ((Result<GitRelease, Error>)?) -> Void) {
if let lUrl = URL(string: "https://api.github.com/repos/apple/swift/releases/latest") {
let lTask = URLSession.shared.dataTask(with: lUrl) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
guard let lData = data, let lGitRelease = try? JSONDecoder().decode(GitRelease.self, from: lData) else {
completion(nil)
return
}
completion(.success(lGitRelease))
}
lTask.resume()
} else {
completion(nil)
}
}
}
Le TimelineProvider
est un protocole qui va nous permettre de gérer l’affichage de notre Widget. Il contient 3 méthodes :
func placeholder(in context: Context)
: Méthode qui fournit une instance de la timeline qui représente le placeholder du Widget.func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void)
: Méthode qui fournit une instance de la timeline qui représente le Widget. Utilisé notamment pour l’affichage du Widget dans la galerie.func getTimeline(in context: Context, completion: @escaping (Timeline<LastCommitEntry>) -> Void)
: Méthode qui fournit une instance de la timeline qui représente le Widget à une date précise et optionnellement, des dates futures.Nous allons donc déclarer une structure dans un fichier nommé LastRealeaseTimeline.swift
avec les méthodes vues précédemment :
import WidgetKit
struct LastReleaseTimeline: TimelineProvider {
typealias Entry = ReleaseTimelineEntry
func placeholder(in context: Context) -> ReleaseTimelineEntry {
}
func getSnapshot(in context: Context, completion: @escaping (ReleaseTimelineEntry) -> Void) {
}
func getTimeline(in context: Context, completion: @escaping (Timeline<ReleaseTimelineEntry>) -> Void) {
}
}
Dans la fonction placeholder
, nous allons créer de fausses données pour montrer que le Widget charge :
func placeholder(in context: Context) -> ReleaseTimelineEntry {
let lPlaceholderGitAuthor = GitAuthor(id: 0, login: "Chargement...")
let lPlaceholderGitRelease = GitRelease(id: 0,
tagName: "Chargement...",
targetCommitish: "Chargement...",
name: "Chargement...",
author: lPlaceholderGitAuthor,
createdAt: "Chargement...",
publishedAt: LastReleaseWidgetView.format(date: Date()))
return ReleaseTimelineEntry(date: Date(), gitRelease: lPlaceholderGitRelease)
}
Dans la fonction getSnapshot
, nous allons créer des données afin de montrer à quoi le widget pourrait ressembler :
func getSnapshot(in context: Context, completion: @escaping (ReleaseTimelineEntry) -> Void) {
let lSnapshotGitAuthor = GitAuthor(id: 0, login: "Tim Cook")
let lSnapshotGitRelease = GitRelease(id: 0,
tagName: "Swift-6.0",
targetCommitish: "master",
name: "Swift 6.0 Release",
author: lSnapshotGitAuthor,
createdAt: "2020-09-08T09:41:00Z",
publishedAt: LastReleaseWidgetView.format(date: Date()))
completion(ReleaseTimelineEntry(date: Date(), gitRelease: lSnapshotGitRelease))
}
Et enfin, la fonction principale : getTimeline
. Ici, nous allons donc récupérer les informations via GitHub. À noter que comme nous ne pouvons pas anticiper la sortie de la prochaine release de SwiftLangage de programmation créé par Apple, pour le développement sur leur différents périphériques., nous allons mettre une date d’expiration du Widget toutes les 2 minutes (pour la démo, sinon 1x par jour suffirait amplement :)). Voici le rendu :
func getTimeline(in context: Context, completion: @escaping (Timeline<ReleaseTimelineEntry>) -> Void) {
GitHubServices.getAppleSwiftLatestRelease { (lResult) in
let lGitRelease: GitRelease
switch lResult {
case .success(let lRetreivedGitRelease):
lGitRelease = lRetreivedGitRelease
default:
let lSnapshotGitAuthor = GitAuthor(id: 0, login: "Tim Cook")
lGitRelease = GitRelease(id: 0,
tagName: "Swift-6.0",
targetCommitish: "master",
name: "Swift 6.0 Release",
author: lSnapshotGitAuthor,
createdAt: "2020-09-08T09:41:00Z",
publishedAt: LastReleaseWidgetView.format(date: Date()))
}
let lCurrentDate = Date()
let lExpireDate = Calendar.current.date(byAdding: .minute, value: 2, to: lCurrentDate)!
let lReleaseTimelineEntry = ReleaseTimelineEntry(date: Date(), gitRelease: lGitRelease)
let lTimeline = Timeline(entries: [lReleaseTimelineEntry], policy: .after(lExpireDate))
completion(lTimeline)
}
}
A noter : Pour la création de la variable lTimeline
, au niveau du paramètre Policy, on a accès à plusieurs types :
.after(date)
: Le widget se réactualise après une date donnée..atEnd
: Le widget se réactualise après la dernière date de la timeline..never
: Le widget se réactualise dès qu’une nouvelle timeline est disponible.Maintenant que notre Widget est fonctionnel, il est temps de passer à l’interface !
A noter, les widgets fonctionnent uniquement avec SwiftUI.
Voici un exemple de ce qu’on pourrait utiliser (Fichier Views/LastReleaseWidgetView.swift
) :
import SwiftUI
import WidgetKit
struct LastReleaseWidgetView: View {
let entry: LastReleaseTimeline.Entry
var body: some View {
VStack(alignment: .leading) {
VStack(spacing: 4, content: {
Text("apple/swift")
.underline()
.bold()
.italic()
.font(.system(.title))
.foregroundColor(.white)
.baselineOffset(1)
.frame(maxWidth: .infinity, alignment: .center)
})
Spacer()
HStack(spacing: 10, content: {
VStack(content: {
Text("Dernière release :")
.underline()
.baselineOffset(1)
.font(.system(.caption))
.foregroundColor(.white)
.bold()
Text(entry.gitRelease.name)
.font(.system(.title3))
.foregroundColor(.white)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
})
VStack(content: {
Text("par \(entry.gitRelease.author.login) le \(Self.format(string: entry.gitRelease.publishedAt))")
.font(.system(.caption))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
})
}).frame(maxWidth: .infinity, maxHeight: .infinity)
Text("Mise à jour le \(Self.format(date:entry.date))")
.font(.system(.caption2))
.italic()
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 5)
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.padding(8)
.background(LinearGradient(gradient: Gradient(colors: [Color(red: 0.93, green: 0.27, blue: 0.24), Color(red: 0.89, green: 0.66, blue: 0.30)]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
static func format(date pDate: Date) -> String {
let lDateFormatter = DateFormatter()
lDateFormatter.dateFormat = "dd-MM-yyyy HH:mm"
return lDateFormatter.string(from: pDate)
}
static func format(string pString: String) -> String {
let lDateFormatter = DateFormatter()
lDateFormatter.locale = Locale(identifier: "en_US_POSIX")
lDateFormatter.dateFormat = "yyyy-MM-dd’T’HH:mm:ssZ"
if let lDate = lDateFormatter.date(from: pString) {
return Self.format(date: lDate)
} else {
return pString
}
}
}
struct LastReleaseWidgetView_Previews: PreviewProvider {
static var previews: some View {
let lSnapshotGitAuthor = GitAuthor(id: 0, login: "Tim Cook")
let lSnapshotGitRelease = GitRelease(id: 0,
tagName: "Swift-6.0",
targetCommitish: "master",
name: "Swift 6.0 Release",
author: lSnapshotGitAuthor,
createdAt: "2020-09-08T09:41:00Z",
publishedAt: LastReleaseWidgetView.format(date: Date()))
let lLastReleaseWidgetView = LastReleaseWidgetView(entry: ReleaseTimelineEntry(date: Date(), gitRelease: lSnapshotGitRelease))
lLastReleaseWidgetView.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}
Vous êtes bien entendu libre pour le design ! Néanmoins, je vous invite à utiliser cette vue si vous voulez juste tester les Widgets iOS 14 :)
Nous avons tout fait ! Il ne nous reste plus qu’a déclarer le Widget afin qu’iOS le detecte (fichier LastReleaseWidget.swift
) :
@main
struct LastReleaseWidget: Widget {
private let kind: String = "LastReleaseWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: LastReleaseTimeline()) { entry in
LastReleaseWidgetView(entry: entry)
}
.configurationDisplayName("Swift : Dernière release")
.description("Affiche la dernière release du repo Swift.")
}
}
Et voilà notre widget !
Maintenant, on pourrait imaginer rendre notre Widget personnalisable afin que l’utilisateur puisse consulter les releases du projet GitHub qu’il souhaite.
Pour cela, nous allons rajouter un Siri Intent
. Faites un clic droit sur votre dossier WidgetExtension
> New File...
et ajoutez un fichier SiriKit Intent Definition File
.
ATTENTION : Il faut bien sélectionner les deux targets de votre application (ici GitWidget
& WidgetExtensionExtension
), sinon l’intent ne fonctionnera pas.
Ce fichier va nous permettre de créer des paramètres pour notre Widget en utilisant SiriKit.
Pour cela, créez un nouvel intent LastRelease
dans le fichier que l’on vient de créer, changez sa catégorie en Information/View
et cochez uniquement la check-box Intent is eligible for widgets
. Ajoutez ensuite un paramètre nommé GitRepo
de type String
et ajoutez lui comme Default Value
"apple/swift".
Et voilà, notre Widget est désormais paramétrable. Nous allons maintenant mettre à jour le code de notre Widget afin que l’on récupère le paramètre saisi. Afin de bien séparer les choses et de conserver notre Widget de base, nous allons recréer les fichiers avec la dénomination Intent
à la fin.
Créez un fichier Class/ReleaseTimelineEntryIntent.swift
afin de mettre à jour notre timeline pour qu’elle prenne en compte le paramètre :
import WidgetKit
struct ReleaseTimelineEntryIntent: TimelineEntry {
var date: Date
var gitRelease: GitRelease
var gitRepo: String
}
Nous allons ensuite modifier notre fonction getAppleSwiftLatestRelease()
afin de la rendre paramétrable (fichier GitHubService.swift
) :
static func getGitRepoLatestRelease(forGitRepo pGitRepo: String, completion: @escaping ((Result<GitRelease, Error>)?) -> Void) {
if let lUrl = URL(string: "https://api.github.com/repos/\(pGitRepo)/releases/latest") {
Il va ensuite falloir modifier le TimelineProvider
afin que notre Widget build :
func getTimeline(in context: Context, completion: @escaping (Timeline<ReleaseTimelineEntry>) -> Void) {
GitHubServices.getGitRepoLatestRelease(forGitRepo: "apple/swift") { (lResult) in
...
}
Nous allons aussi cependant quand même en créer un nouveau afin de le rentre compatible avec l’intent. Pour cela, créez un nouveau fichier LastReleaseTimelineIntent.swift
:
import WidgetKit
struct LastReleaseTimelineIntent: IntentTimelineProvider {
typealias Entry = ReleaseTimelineEntryIntent
typealias Intent = LastReleaseIntent
func placeholder(in context: Context) -> ReleaseTimelineEntryIntent {
let lPlaceholderGitAuthor = GitAuthor(id: 0, login: "Chargement...")
let lPlaceholderGitRelease = GitRelease(id: 0,
tagName: "Chargement...",
targetCommitish: "Chargement...",
name: "Chargement...",
author: lPlaceholderGitAuthor,
createdAt: "Chargement...",
publishedAt: LastReleaseWidgetView.format(date: Date()))
return ReleaseTimelineEntryIntent(date: Date(), gitRelease: lPlaceholderGitRelease, gitRepo: "apple/swift")
}
func getSnapshot(for configuration: LastReleaseIntent, in context: Context, completion: @escaping (ReleaseTimelineEntryIntent) -> Void) {
let lSnapshotGitAuthor = GitAuthor(id: 0, login: "Tim Cook")
let lSnapshotGitRelease = GitRelease(id: 0,
tagName: "Swift-6.0",
targetCommitish: "master",
name: "Swift 6.0 Release",
author: lSnapshotGitAuthor,
createdAt: "2020-09-08T09:41:00Z",
publishedAt: LastReleaseWidgetView.format(date: Date()))
completion(ReleaseTimelineEntryIntent(date: Date(), gitRelease: lSnapshotGitRelease, gitRepo: configuration.GitRepo ?? "apple/swift"))
}
func getTimeline(for configuration: LastReleaseIntent, in context: Context, completion: @escaping (Timeline<ReleaseTimelineEntryIntent>) -> Void) {
GitHubServices.getGitRepoLatestRelease(forGitRepo: configuration.GitRepo ?? "apple/swift") { (lResult) in
let lGitRelease: GitRelease
switch lResult {
case .success(let lRetreivedGitRelease):
lGitRelease = lRetreivedGitRelease
default:
let lSnapshotGitAuthor = GitAuthor(id: 0, login: "Tim Cook")
lGitRelease = GitRelease(id: 0,
tagName: "Swift-6.0",
targetCommitish: "master",
name: "Swift 6.0 Release",
author: lSnapshotGitAuthor,
createdAt: "2020-09-08T09:41:00Z",
publishedAt: LastReleaseWidgetView.format(date: Date()))
}
let lCurrentDate = Date()
let lExpireDate = Calendar.current.date(byAdding: .minute, value: 2, to: lCurrentDate)!
let lReleaseTimelineEntry = ReleaseTimelineEntryIntent(date: Date(), gitRelease: lGitRelease, gitRepo: configuration.GitRepo ?? "apple/swift")
let lTimeline = Timeline(entries: [lReleaseTimelineEntry], policy: .after(lExpireDate))
completion(lTimeline)
}
}
}
Il faut aussi mettre à jour la vue afin qu’elle affiche le repo que l’utilisateur aura choisi. Pour cela, créez un fichier Views/LastReleaseWidgetViewIntent.swift
:
import SwiftUI
import WidgetKit
struct LastReleaseWidgetViewIntent: View {
let entry: LastReleaseTimelineIntent.Entry
var body: some View {
VStack(alignment: .leading) {
VStack(spacing: 4, content: {
Text(entry.gitRepo)
.underline()
.bold()
.italic()
.font(.system(.title))
.foregroundColor(.white)
.baselineOffset(1)
.frame(maxWidth: .infinity, alignment: .center)
})
Spacer()
HStack(spacing: 10, content: {
VStack(content: {
Text("Dernière release :")
.underline()
.baselineOffset(1)
.font(.system(.caption))
.foregroundColor(.white)
.bold()
Text(entry.gitRelease.name)
.font(.system(.title3))
.foregroundColor(.white)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
})
VStack(content: {
Text("par \(entry.gitRelease.author.login) le \(Self.format(string: entry.gitRelease.publishedAt))")
.font(.system(.caption))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
})
}).frame(maxWidth: .infinity, maxHeight: .infinity)
Text("Mise à jour le \(Self.format(date:entry.date))")
.font(.system(.caption2))
.italic()
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 5)
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.padding(8)
.background(LinearGradient(gradient: Gradient(colors: [Color(red: 0.93, green: 0.27, blue: 0.24), Color(red: 0.89, green: 0.66, blue: 0.30)]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
static func format(date pDate: Date) -> String {
let lDateFormatter = DateFormatter()
lDateFormatter.dateFormat = "dd-MM-yyyy HH:mm"
return lDateFormatter.string(from: pDate)
}
static func format(string pString: String) -> String {
let lDateFormatter = DateFormatter()
lDateFormatter.locale = Locale(identifier: "en_US_POSIX")
lDateFormatter.dateFormat = "yyyy-MM-dd’T’HH:mm:ssZ"
if let lDate = lDateFormatter.date(from: pString) {
return Self.format(date: lDate)
} else {
return pString
}
}
}
struct LastReleaseWidgetViewIntent_Previews: PreviewProvider {
static var previews: some View {
let lSnapshotGitAuthor = GitAuthor(id: 0, login: "Tim Cook")
let lSnapshotGitRelease = GitRelease(id: 0,
tagName: "Swift-6.0",
targetCommitish: "master",
name: "Swift 6.0 Release",
author: lSnapshotGitAuthor,
createdAt: "2020-09-08T09:41:00Z",
publishedAt: LastReleaseWidgetView.format(date: Date()))
let lLastReleaseWidgetViewIntent = LastReleaseWidgetViewIntent(entry: ReleaseTimelineEntryIntent(date: Date(), gitRelease: lSnapshotGitRelease, gitRepo: "apple/swift"))
lLastReleaseWidgetViewIntent.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}
Et enfin, il faut créer l’instance du Widget. Pour cela, nous allons créer une classe LastReleaseWidgetIntent
dans un nouveau fichier LastReleaseWidgetIntent.swift
:
import WidgetKit
import SwiftUI
@main
struct LastReleaseWidgetIntent: Widget {
private let kind: String = "LastReleaseWidgetIntent"
public var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: LastReleaseIntent.self, provider: LastReleaseTimelineIntent()) { entry in
LastReleaseWidgetViewIntent(entry: entry)
}
.configurationDisplayName("GitHubRepo : Dernière release")
.description("Affiche la dernière release d’un repo GitHub.")
}
}
Avant de build, il va falloir commenter le @main
du fichier LastReleaseWidget.swift
, sinon, on ne pourra pas build car il y aura deux instances du Widget, ce qui n’est pas possible pour le moment. Une fois cela fait, vous pouvez lancer le Widget sur votre téléphone. Le rendu est le même, mais si vous restez appuyer sur le Widget, vous pouvez modifier le Widget et vous aurez ceci :
Si je modifie pour un autre repo GitHub (par microsoft/vscode par exemple), mon Widget se mettra à jour
Il est possible, pour la même application, et la même extension, d’avoir plusieurs Widgets ! Et cela est assez simple, il suffit de créer un WidgetBundle
. Pour cela, créez un fichier SwiftWidgetsBundle.swift
:
import WidgetKit
import SwiftUI
@main
struct SwiftWidgetsBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
LastReleaseWidget()
LastReleaseWidgetIntent()
}
}
Il faut aussi que vous commentiez le @main
du fichier LastReleaseWidgetIntent.swift
.
Et maintenant, vous aurez les 2 widgets que nous avons créé dans l’écran de configuration de notre Widget sur iOS : le Widget classique qui check sur le GitHub apple/swift et le Widget personnalisable
Vous savez maintenant comment ajouter un Widget dans votre application !
C’est un moyen assez pratique pour diffuser des informations à vos utilisateurs via le HomeScreen d’iOS. Vous pouvez également aller plus loin en ajoutant des liens sur les vues de votre Widget, qui ouvrent directement votre application au bon endroit.
Android, iOS, agilité, UX... On vous donne nos grandes convictions concernant le développement mobile
Avoir des applications, des sites web et des applications mobiles dernier cri est un point essentiel pour gagner et fidéliser ses clients et utilisateurs. Mais, à partir de deux applications, il devient difficile d’avoir un suivi réel de leur comportement
La pérennité des applications en développement : définition et enjeux. On en parle sur AXOTalks, le blog des experts tech & IT !