Widgets IOS 14 - Fonctionnalités et limitations





Cette année, il existe plusieurs opportunités intéressantes pour les développeurs iOS de vider la batterie de l'iPhone pour améliorer l'expérience utilisateur, dont les nouveaux widgets. En attendant la sortie de la version de l'OS, j'aimerais partager mon expérience d'écriture d'un widget pour l'application "Wallet" et vous dire quelles opportunités et limitations notre équipe a rencontrées sur les versions beta de Xcode.



Commençons par la définition - les widgets sont des vues qui affichent des informations pertinentes sans lancer l'application mobile principale et sont toujours à portée de main de l'utilisateur. La possibilité de les utiliser existe déjà dans iOS ( Today Extension ), à commencer par iOS 8, mais mon expérience purement personnelle de les utiliser est plutôt triste - bien qu'un bureau spécial avec des widgets leur soit alloué, j'y arrive encore rarement, l'habitude ne s'est pas développée.



Du coup, dans iOS 14, on assiste à une résurgence des widgets, plus profondément intégrés dans l'écosystème, et plus conviviaux (en théorie).







Travailler avec des cartes de fidélité est l'une des principales fonctions de notre application Wallet. De temps en temps, des suggestions d'utilisateurs sur la possibilité d'ajouter un widget à Aujourd'hui apparaissent dans les avis de l'App Store. Les utilisateurs, étant à la caisse, aimeraient montrer la carte dès que possible, obtenir une réduction et s'enfuir pour leur entreprise, car le retard pour toute tranche de temps provoque ces regards très reproches dans la file d'attente. Dans notre cas, le widget peut enregistrer plusieurs actions de l'utilisateur pour ouvrir une carte, accélérant ainsi le paiement des marchandises à la caisse. Les magasins seront également reconnaissants - moins de files d'attente à la caisse.



Cette année, Apple a publié de manière inattendue une version iOS presque immédiatement après la présentation, laissant aux développeurs une journée pour finaliser leurs applications sur Xcode GM, mais nous étions prêts pour la sortie, puisque notre équipe iOS a commencé à créer sa propre version du widget sur les versions bêta de Xcode ... Le widget est actuellement en cours de révision dans l'App Store. Selon les statistiques , la mise à jour des appareils vers le nouvel iOS est assez rapide ; très probablement, les utilisateurs iront vérifier quelles applications ont déjà des widgets, trouveront les nôtres et seront heureux.



À l'avenir, nous aimerions ajouter des informations encore plus pertinentes - par exemple, le solde, le code-barres, les derniers messages non lus des partenaires et les notifications (par exemple, que les utilisateurs doivent prendre une action - pour confirmer ou activer la carte). Pour le moment, le résultat ressemble à ceci:







Ajouter un widget Ă  un projet



Comme d'autres fonctionnalités supplémentaires similaires, le widget est ajouté en tant qu'extension au projet principal. Une fois ajouté, Xcode a généreusement généré le code du widget et d'autres classes de base. C'est là que nous attendait la première fonctionnalité intéressante - pour notre projet, ce code n'a pas été compilé, car dans l'un des fichiers un préfixe était automatiquement inséré dans les noms de classe (oui, ces mêmes préfixes Obj-C!), Mais pas dans les fichiers générés. Comme le dit l'adage, ce ne sont pas les dieux qui brûlent les pots, apparemment, les différentes équipes au sein d'Apple ne se sont pas mises d'accord entre elles. Espérons qu'ils le corrigeront pour la version de sortie. Afin de personnaliser le préfixe de votre projet, dans l' inspecteur de fichiers de la cible principale de l'application, remplissez le champ Préfixe de classe .



Pour ceux qui ont suivi l'actualité de la WWDC, ce n'est un secret pour personne que l'implémentation de widgets n'est possible qu'en utilisant SwiftUI. Un point intéressant est que de cette manière, Apple impose une mise à jour de ses technologies: même si l'application principale est écrite en utilisant UIKit, alors, si vous voulez, seulement SwiftUI. D'un autre côté, c'est une bonne occasion d'essayer un nouveau cadre pour écrire une fonctionnalité, dans ce cas, il s'intègre confortablement dans le processus - pas de changement d'état, pas de navigation, il vous suffit de déclarer une interface utilisateur statique. Autrement dit, avec le nouveau cadre, de nouvelles restrictions sont également apparues, car les anciens widgets dans Today peuvent contenir plus de logique et d'animation.



L'une des principales innovations de SwiftUI est la possibilité de prévisualiser sans le lancer sur un simulateur ou un appareil ( aperçu ). Une chose cool, mais, malheureusement, sur les grands projets (dans le nôtre - ~ 400K lignes de code), cela fonctionne extrêmement lentement, même sur les meilleurs MacBook, il est plus rapide de fonctionner sur un appareil. Une alternative à cela est d'avoir un projet vide ou une aire de jeux sous la main pour le prototypage rapide.



Le débogage est également disponible avec un schéma Xcode dédié. Sur le simulateur, le débogage est instable même jusqu'à Xcode 12 beta 6, il est donc préférable de faire don de l'un des appareils de test, de passer à iOS 14 et de le tester. Soyez prêt que cette partie ne fonctionnera pas comme prévu sur les versions de publication.



Interface



L'utilisateur peut choisir parmi différents types ( WidgetFamily ) de widgets de trois tailles - petit, moyen, grand .







Pour vous inscrire, vous devez spécifier explicitement les éléments pris en charge:

struct CardListWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: “CardListWidgetKind”,
                            intent: DynamicMultiSelectionIntent.self,
                            provider: CardListProvider()) { entry in
            CardListEntryView(entry: entry)
        }
        .configurationDisplayName(" ")
        .description(",     ")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}


Mon équipe et moi avons décidé de nous en tenir aux petits et moyens - afficher une carte préférée pour un petit widget ou 4 pour un moyen.



Le widget est ajouté au bureau à partir du centre de contrôle, où l'utilisateur choisit le type dont il a besoin:







Personnalisez la couleur du bouton "Ajouter un widget" en utilisant Assets.xcassets -> AccentColor , le nom du widget avec une description Ă©galement (exemple de code ci-dessus).



Si vous rencontrez la limitation du nombre de vues prises en charge, vous pouvez l'Ă©tendre Ă  l'aide du WidgetBundle :



@main
struct WalletBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        CardListWidget()
        MySecondWidget()
    }
}


Puisque le widget montre un instantané d'un état, la seule possibilité d'interaction de l'utilisateur est de basculer vers l'application principale en cliquant sur un élément ou sur le widget entier. Aucune animation, navigation ou transitions vers d'autres vues . Mais il est possible de déposer un lien profond dans l'application principale. Dans ce cas, pour un petit widget, la zone de clic est la zone entière, et dans ce cas nous utilisons la méthode widgetURL (_ :) . Pour les moyennes et grandes, les clics sur les vues sont disponibles , et la structure Link de SwiftUI nous aidera dans ce domaine .



Link(destination: card.url) {
  CardView(card: card)
}


La vue finale du widget de deux tailles s'est avérée comme suit:







Lors de la conception de l'interface du widget, les règles et exigences suivantes peuvent aider (selon les directives Apple):

  1. Concentrez le widget sur une idée et un problème, n'essayez pas de répéter toutes les fonctionnalités de l'application.
  2. Affichez plus d'informations en fonction de la taille, plutĂ´t que de simplement mettre Ă  l'Ă©chelle le contenu.
  3. Affichez des informations dynamiques qui peuvent changer au cours de la journée. Les extrêmes sous la forme d'informations complètement statiques et d'informations qui changent à chaque minute ne sont pas les bienvenus.
  4. Le widget doit fournir des informations pertinentes aux utilisateurs, pas simplement une autre façon d'ouvrir l'application.


L'apparence a été personnalisée. L'étape suivante consiste à choisir les cartes à montrer à l'utilisateur et comment. Il peut y avoir clairement plus de quatre cartes. Considérons plusieurs options:

  1. Autorisez l'utilisateur Ă  choisir des cartes. Qui, sinon lui, sait quelles cartes sont les plus importantes!
  2. Afficher les dernières cartes utilisées.
  3. Créez un algorithme plus intelligent, en vous concentrant, par exemple, sur l'heure et le jour de la semaine et les statistiques (si un utilisateur se rend dans un magasin de fruits près de chez lui en semaine le soir et se rend dans un hypermarché le week-end, vous pouvez aider l'utilisateur en ce moment et lui montrer la carte souhaitée)


Dans le cadre du prototype, nous avons opté pour la première option afin d'essayer en même temps la possibilité d'ajuster les paramètres directement sur le widget. Pas besoin de faire un écran spécial à l'intérieur de l'application. Est-il vrai que les utilisateurs, comme on dit, sont suffisamment expérimentés pour trouver ces paramètres?



Paramètres de widget personnalisés



Les paramètres sont générés à l'aide d'intents (bonjour les développeurs Android) - lors de la création d'un nouveau widget, le fichier d'intention est automatiquement ajouté au projet. Le générateur de code préparera une classe héritant d' INIntent , qui fait partie du framework SiriKit . Les paramètres d'intention contiennent l'option magique «L'intention est éligible pour les widgets» . Plusieurs types de paramètres sont disponibles, vous pouvez personnaliser vos sous-types. Puisque les données dans notre cas sont une liste dynamique, nous définissons également l'élément «Les options sont fournies dynamiquement» .



Pour différents types de widget, définissez le nombre maximum d'éléments dans la liste - pour petit 1, pour moyen 4.

Ce type d'intention est utilisé par le widget comme source de données.







Ensuite, la classe d'intention configurée doit être placée dans la configuration IntentConfiguration .

struct CardListWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: WidgetConstants.widgetKind,
                            intent: DynamicMultiSelectionIntent.self,
                            provider: CardListProvider()) { entry in
            CardListEntryView(entry: entry)
        }
        .configurationDisplayName(" ")
        .description(",     .")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}


Si les paramètres utilisateur ne sont pas requis, il existe une alternative sous la forme de la classe StaticConfiguration, qui fonctionne sans spécifier d'intention.



Le titre et la description sont modifiables sur l'écran des paramètres.

Le nom du widget doit tenir sur une seule ligne, sinon il est coupé. Dans le même temps, la longueur autorisée pour l'écran d'ajout et les paramètres du widget sont différentes.

Exemples de longueur de nom maximale pour certains appareils:



iPhone 11 Pro Max
28  
21   

iPhone 11 Pro
25  
19   

iPhone SE
24  
19   


La description est sur plusieurs lignes. Dans le cas d'un texte très long dans les paramètres, le contenu peut faire défiler. Mais sur l'écran d'ajout, l'aperçu du widget est d'abord compressé, puis quelque chose de terrible arrive à la mise en page.







Vous pouvez également modifier la couleur d'arrière-plan et les valeurs des paramètres WidgetBackground et AccentColor - par défaut, ils sont déjà dans Assets . Si nécessaire, ils peuvent être renommés dans la configuration du widget dans Paramètres de construction du groupe Compilateur de catalogue d'actifs - Options dans les champs Nom de couleur d' arrière-plan du widget et Nom de couleur d'accent global , respectivement.







Certains paramètres peuvent être masqués (ou affichés) en fonction de la valeur sélectionnée dans un autre paramètre via le paramètre Relation .

Il est à noter que l'interface utilisateur de modification d'un paramètre dépend de son type. Par exemple, si nous spécifions Boolean , nous verrons UISwitch , et si Integer , nous avons déjà le choix entre deux options: entrée via UITextfield ou changement pas à pas via UIStepper .







Interaction avec l'application principale.



Le bundle a été configuré, il reste à déterminer où l'intention elle-même prendra les données réelles. Le pont avec l'application principale dans ce cas est un fichier dans le groupe général ( App Groups ). L'application principale écrit, le widget lit.

La méthode suivante est utilisée pour obtenir l'URL du groupe général:

FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: “group.ru.yourcompany.yourawesomeapp”)


Nous sauvegardons tous les candidats, car ils seront utilisés par l'utilisateur dans les paramètres comme dictionnaire de sélection.

Ensuite, le système d'exploitation doit découvrir que les données ont été mises à jour, pour cela, nous appelons:

WidgetCenter.shared.reloadAllTimelines()
//  WidgetCenter.shared.reloadTimelines(ofKind: "kind")


Puisque l'appel de méthode rechargera le contenu du widget et la chronologie entière, utilisez-le lorsque les données ont été réellement mises à jour afin de ne pas surcharger le système.



Mise à jour des données



Afin de prendre soin de la batterie d'un appareil utilisateur, Apple a pensé à un mécanisme de mise à jour des données sur un widget à l'aide d'une chronologie - un mécanisme de génération d' instantanés . Le développeur ne met pas à jour ni ne gère directement la vue , mais fournit à la place un calendrier, guidé par lequel, le système d'exploitation coupera les instantanés en arrière-plan.

La mise à jour a lieu sur les événements suivants:

  1. Appel du WidgetCenter.shared.reloadAllTimelines () précédemment utilisé
  2. Lorsqu'un utilisateur ajoute un widget au bureau
  3. Lors de la modification des paramètres.


En outre, le développeur dispose de trois types de politiques pour mettre à jour les délais (TimelineReloadPolicy):

atEnd - mise à jour après avoir montré le dernier instantané

jamais - mise à jour uniquement en cas d'appel forcé

après (_ :) - mise à jour après une certaine période de temps.



Dans notre cas, il suffit de demander au système de prendre un instantané jusqu'à ce que les données de la carte soient mises à jour dans l'application principale:



struct CardListProvider: IntentTimelineProvider {
    public typealias Intent = DynamicMultiSelectionIntent
    public typealias Entry = CardListEntry

    public func placeholder(in context: Context) -> Self.Entry {
        return CardListEntry(date: Date(), cards: testData)
    }

    public func getSnapshot(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Self.Entry) -> Void) {
        let entry = CardListEntry(date: Date(), cards: testData)
        completion(entry)
    }

    public func getTimeline(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) {
        let cards: [WidgetCard]? = configuration.cards?.compactMap { card in
            let id = card.identifier
            let storedCards = SharedStorage.widgetRepository.restore()
            return storedCards.first(where: { widgetCard in widgetCard.id == id })
        }

        let entry = CardListEntry(date: Date(), cards: cards ?? [])
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

struct CardListEntry: TimelineEntry {
    public let date: Date
    public let cards: [WidgetCard]
}


Une option plus flexible serait utile si vous utilisez un algorithme automatique pour sélectionner les cartes en fonction du jour de la semaine et de l'heure.



Par ailleurs, il convient de noter l'affichage d'un widget s'il se trouve dans une pile de widgets ( Smart Stack ). Dans ce cas, nous pouvons utiliser deux options pour gérer les priorités: Suggestions Siri ou en définissant la valeur de pertinence d'un TimelineEntry avec le type TimelineEntryRelevance . TimelineEntryRelevance contient deux paramètres:

score - la priorité de l'instantané actuel par rapport aux autres instantanés;

la durée est le temps pendant lequel le widget reste pertinent et le système peut le placer en haut de la pile.



Les deux méthodes, ainsi que les options de configuration du widget, ont été discutées en détail lors de la session WWDC .



Vous devez également expliquer comment maintenir à jour l'affichage de la date et de l'heure. Comme nous ne pouvons pas mettre à jour régulièrement le contenu du widget, plusieurs styles ont été ajoutés pour le composant Texte. Lors de l'utilisation d'un style, le système met automatiquement à jour le contenu du composant lorsque le widget est à l'écran. Peut-être qu'à l'avenir, la même approche sera étendue à d'autres composants SwiftUI.



Le texte prend en charge les styles suivants:

relatif- le décalage horaire entre la date actuelle et la date spécifiée. Il convient de noter ici: si la date est spécifiée dans le futur, le compte à rebours commence, puis la date à partir du moment où elle atteint zéro est affichée. Le même comportement sera pour les deux styles suivants;

offset - similaire au précédent, mais il y a une indication sous la forme d'un préfixe avec ±;

minuterie - analogique d'une minuterie;

date - affichage de la date ;

heure - affichage de l' heure .



De plus, il est possible d'afficher l'intervalle de temps entre les dates en spécifiant simplement l'intervalle.



let components = DateComponents(minute: 10, second: 0)
 let futureDate = Calendar.current.date(byAdding: components, to: Date())!
 VStack {
   Text(futureDate, style: .relative)
      .multilineTextAlignment(.center)
   Text(futureDate, style: .offset)
      .multilineTextAlignment(.center)
   Text(futureDate, style: .timer)
      .multilineTextAlignment(.center)
   Text(Date(), style: .date) 
      .multilineTextAlignment(.center)
   Text(Date(), style: .time)
      .multilineTextAlignment(.center)
   Text(Date() ... futureDate)
      .multilineTextAlignment(.center)
}






Aperçu du widget



Lorsqu'il est affiché pour la première fois, le widget sera ouvert en mode aperçu, pour cela nous devons renvoyer le TimeLineEntry dans l'espace réservé (dans :). Dans notre cas, cela ressemble à ceci:

func placeholder(in context: Context) -> Self.Entry {
        return CardListEntry(date: Date(), cards: testData)
 }


Après cela, le modificateur expurgé (raison :) avec le paramètre d' espace réservé est appliqué à la vue . Dans ce cas, les éléments du widget sont affichés flous.







Nous pouvons supprimer cet effet de certains éléments en utilisant le modificateur non rédigé () .

La documentation indique également que l'appel à la méthode d' espace réservé (in :) est synchrone et que le résultat doit revenir le plus rapidement possible, contrairement à getSnapshot (in: completion :) et getTimeline (in: completion :)



Éléments d'arrondi



Dans les directives, il est recommandé de faire correspondre l'arrondi des éléments avec l'arrondi du widget; pour cela , la structure ContainerRelativeShape a été ajoutée dans iOS 14 , ce qui vous permet d'appliquer une forme de conteneur à une vue.



.clipShape(ContainerRelativeShape()) 


Prise en charge d'Objective-C



Si vous avez besoin d'ajouter du code Objective-C au widget (par exemple, nous y avons écrit la génération d'images de codes-barres), tout se passe de manière standard en ajoutant l'en-tête de pont Objective-C. Le seul problème que nous avons rencontré était que lors de la construction, Xcode ne voyait plus les fichiers d'intention générés automatiquement, nous les avons donc également ajoutés à l'en-tête de pontage :



#import "DynamicCardSelectionIntent.h"
#import "CardSelectionIntent.h"
#import "DynamicMultiSelectionIntent.h"


Taille de l'application



Les tests ont été réalisés sur Xcode 12 beta 6

Sans widget: 61,6 Mo

Avec un widget: 62,2 Mo Je vais



résumer les principaux points qui ont été abordés dans l'article:

  1. Les widgets sont un excellent moyen de se faire une idée de SwiftUI dans la pratique. Ajoutez-les à votre projet même si la version minimale prise en charge est inférieure à iOS 14.
  2. WidgetBundle est utilisé pour augmenter le nombre de widgets disponibles, voici un excellent exemple du nombre de widgets différents d'ApolloReddit.
  3. IntentConfiguration ou StaticConfiguration aidera à ajouter des paramètres personnalisés sur le widget lui-même si des paramètres personnalisés ne sont pas nécessaires.
  4. Un dossier partagé sur le système de fichiers dans les groupes d'applications partagés aidera à synchroniser les données avec l'application principale.
  5. Le développeur peut choisir parmi plusieurs politiques de mise à jour de la chronologie (à la fin, jamais, après (_ :)).


Sur ce point, le chemin épineux du développement d'un widget sur les versions bêta de Xcode peut être considéré comme complet, il ne reste plus qu'une étape simple - passer par un examen dans l'App Store.



PS La version avec le widget a passé la modération et est maintenant disponible en téléchargement dans l'App Store!



Merci d'avoir lu jusqu'au bout, je serai heureux de vos suggestions et commentaires. Veuillez répondre à une courte enquête pour voir à quel point les widgets sont populaires parmi les utilisateurs et les développeurs.



All Articles