IOS 14 Widgets - Funktionen und Einschränkungen





In diesem Jahr gibt es für iOS-Entwickler mehrere interessante Möglichkeiten, den iPhone-Akku zu entladen , um die Benutzererfahrung zu verbessern. Eines davon sind neue Widgets. Während wir alle auf die Release-Version des Betriebssystems warten, möchte ich meine Erfahrungen beim Schreiben eines Widgets für die "Wallet" -Anwendung mitteilen und Ihnen mitteilen, auf welche Möglichkeiten und Einschränkungen unser Team bei Beta-Versionen von Xcode gestoßen ist.



Beginnen wir mit der Definition: Widgets sind Ansichten, die relevante Informationen anzeigen, ohne die mobile Hauptanwendung zu starten, und die dem Benutzer immer zur Verfügung stehen. Die Möglichkeit, sie zu verwenden, ist bereits in iOS ( Today Extension ) vorhanden, beginnend mit iOS 8, aber meine rein persönliche Erfahrung mit ihnen ist ziemlich traurig - obwohl ihnen ein spezieller Desktop mit Widgets zugewiesen wurde, komme ich immer noch selten dorthin, die Gewohnheit hat sich nicht entwickelt.



Infolgedessen sehen wir in iOS 14 ein Wiederaufleben von Widgets, die stärker in das Ökosystem integriert und (theoretisch) benutzerfreundlicher sind.







Das Arbeiten mit Kundenkarten ist eine der Hauptfunktionen unserer Wallet-Anwendung. Von Zeit zu Zeit finden sich in den Bewertungen im App Store Vorschläge von Benutzern zur Möglichkeit, ein Widget zu Today hinzuzufügen. Benutzer, die an der Kasse sind, möchten die Karte so schnell wie möglich vorzeigen, einen Rabatt erhalten und über ihr Geschäft davonlaufen, da die Verzögerung für jede Zeitscheibe diese sehr vorwurfsvollen Blicke in der Warteschlange verursacht. In unserem Fall kann das Widget mehrere Benutzeraktionen zum Öffnen einer Karte speichern und so die Zahlung für Waren an der Kasse beschleunigen. Die Geschäfte werden auch dankbar sein - weniger Warteschlangen an der Kasse.



In diesem Jahr veröffentlichte Apple fast unmittelbar nach der Präsentation unerwartet eine iOS-Version, sodass Entwickler einen Tag Zeit hatten, um ihre Anwendungen auf Xcode GM fertigzustellen. Wir waren jedoch bereit für die Veröffentlichung, da unser iOS-Team damit begann, eine eigene Version des Widgets für Beta-Versionen von Xcode zu erstellen ... Das Widget wird derzeit im App Store überprüft. Laut Statistik ist das Aktualisieren von Geräten auf das neue iOS ziemlich schnell . Höchstwahrscheinlich werden Benutzer überprüfen, welche Anwendungen bereits über Widgets verfügen, unsere finden und glücklich sein.



In Zukunft möchten wir noch relevantere Informationen hinzufügen - zum Beispiel Guthaben, Barcode, letzte ungelesene Nachrichten von Partnern und Benachrichtigungen (zum Beispiel, dass Benutzer eine Aktion ausführen müssen - um die Karte zu bestätigen oder zu aktivieren). Im Moment sieht das Ergebnis folgendermaßen aus:







Hinzufügen eines Widgets zu einem Projekt



Wie andere ähnliche zusätzliche Funktionen wird das Widget als Erweiterung zum Hauptprojekt hinzugefügt . Nach dem Hinzufügen hat Xcode freundlicherweise den Code für das Widget und andere Kernklassen generiert. Hier erwartete uns das erste interessante Feature - für unser Projekt wurde dieser Code nicht kompiliert, da in einer der Dateien automatisch ein Präfix in die Klassennamen eingefügt wurde (ja, dieselben Obj-C-Präfixe!), Aber nicht in die generierten Dateien. Wie das Sprichwort sagt, sind es nicht die Götter, die die Töpfe verbrennen, anscheinend waren sich die verschiedenen Teams innerhalb von Apple nicht einig. Hoffen wir, dass sie es für die Release-Version beheben. Um das Präfix Ihres Projekts anzupassen , füllen Sie im Dateiinspektor des Hauptziels der Anwendung das Feld Klassenpräfix aus .



Für diejenigen, die die WWDC-Nachrichten verfolgt haben, ist es kein Geheimnis, dass die Implementierung von Widgets nur mit SwiftUI möglich ist. Ein interessanter Punkt ist, dass Apple auf diese Weise ein Update seiner Technologien erzwingt: Selbst wenn die Hauptanwendung mit UIKit geschrieben wird, dann bitte nur SwiftUI. Auf der anderen Seite ist dies eine gute Gelegenheit, ein neues Framework zum Schreiben eines Features auszuprobieren. In diesem Fall passt es problemlos in den Prozess - keine Statusänderungen, keine Navigation, Sie müssen lediglich eine statische Benutzeroberfläche deklarieren. Das heißt, neben dem neuen Framework sind auch neue Einschränkungen aufgetreten, da alte Widgets in Today mehr Logik und Animation enthalten können.



Eine der Hauptinnovationen von SwiftUI ist die Möglichkeit der Vorschau, ohne sie auf einem Simulator oder Gerät zu starten ( Vorschau ). Es ist eine coole Sache, aber leider funktioniert es bei großen Projekten (in unseren - ~ 400K Codezeilen) extrem langsam, selbst auf Top-MacBooks. Es ist schneller, auf einem Gerät ausgeführt zu werden. Eine Alternative dazu besteht darin, ein leeres Projekt oder einen leeren Spielplatz für Rapid Prototyping zur Verfügung zu haben.



Das Debuggen ist auch mit einem dedizierten Xcode-Schema möglich. Auf dem Simulator ist das Debuggen sogar bis zu Xcode 12 Beta 6 instabil. Es ist daher besser, eines der Testgeräte zu spenden, auf iOS 14 zu aktualisieren und darauf zu testen. Seien Sie darauf vorbereitet, dass dieser Teil in Release-Versionen nicht wie erwartet funktioniert.



Schnittstelle



Der Benutzer kann aus verschiedenen Arten ( WidgetFamily ) von Widgets in drei Größen auswählen - klein, mittel, groß .







Um sich zu registrieren, müssen Sie explizit Folgendes angeben:

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])
    }
}


Mein Team und ich haben beschlossen, bei klein und mittel zu bleiben - zeigen Sie eine Lieblingskarte für ein kleines Widget oder 4 für mittel an.



Dem Desktop wird vom Kontrollzentrum aus ein Widget hinzugefügt, in dem der Benutzer den gewünschten Typ auswählt:







Passen Sie die Farbe der Schaltfläche "Widget hinzufügen " mit Assets.xcassets -> AccentColor an , dem Namen des Widgets mit einer Beschreibung (Beispielcode oben).



Wenn Sie auf die Beschränkung der Anzahl der unterstützten Ansichten stoßen , können Sie diese mithilfe des WidgetBundle erweitern :



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


Da das Widget eine Momentaufnahme eines bestimmten Status anzeigt, besteht die einzige Möglichkeit für die Benutzerinteraktion darin, zur Hauptanwendung zu wechseln, indem Sie auf ein Element oder das gesamte Widget klicken. Keine Animation, Navigation oder Übergänge zu anderen Ansichten . Es ist jedoch möglich, einen Deep Link in die Hauptanwendung einzufügen. In diesem Fall ist für ein kleines Widget die Klickzone der gesamte Bereich, und in diesem Fall verwenden wir die Methode widgetURL (_ :) . Für mittlere und große Klicks stehen Ansichtsklicks zur Verfügung , und die Link- Struktur von SwiftUI hilft uns dabei .



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


Die endgültige Ansicht des Widgets in zwei Größen stellte sich wie folgt heraus:







Beim Entwerfen der Benutzeroberfläche des Widgets können die folgenden Regeln und Anforderungen hilfreich sein (gemäß den Apple-Richtlinien):

  1. Konzentrieren Sie das Widget auf eine Idee und ein Problem. Versuchen Sie nicht, alle Funktionen der Anwendung zu wiederholen.
  2. Zeigen Sie je nach Größe mehr Informationen an, anstatt nur den Inhalt zu skalieren.
  3. Zeigen Sie dynamische Informationen an, die sich im Laufe des Tages ändern können. Extreme in Form von vollständig statischen Informationen und Informationen, die sich jede Minute ändern, sind nicht erwünscht.
  4. Das Widget sollte den Benutzern relevante Informationen bereitstellen und keine andere Möglichkeit sein, die Anwendung zu öffnen.


Das Erscheinungsbild wurde angepasst. Der nächste Schritt besteht darin, auszuwählen, welche Karten dem Benutzer wie gezeigt werden sollen. Es kann eindeutig mehr als vier Karten geben. Betrachten wir verschiedene Optionen:

  1. Ermöglichen Sie dem Benutzer, Karten auszuwählen. Wer, wenn nicht er, weiß, welche Karten wichtiger sind!
  2. Zuletzt verwendete Karten anzeigen.
  3. Erstellen Sie einen intelligenteren Algorithmus, der sich beispielsweise auf die Uhrzeit und den Wochentag sowie die Statistiken konzentriert (wenn ein Benutzer an Wochentagen in einen Obstladen in der Nähe seines Hauses und an Wochenenden in einen Hypermarkt geht, können Sie dem Benutzer in diesem Moment helfen und die gewünschte Karte zeigen).


Als Teil des Prototyps haben wir uns für die erste Option entschieden, um gleichzeitig die Möglichkeit zu versuchen, die Parameter direkt im Widget anzupassen. Es ist nicht erforderlich, einen speziellen Bildschirm in der Anwendung zu erstellen. Sind die Benutzer jedoch, wie sie sagen, erfahren genug, um diese Einstellungen zu finden?



Benutzerdefinierte Widget-Einstellungen



Einstellungen werden mit Absichten generiert (Hallo Android-Entwickler). Beim Erstellen eines neuen Widgets wird die Absichtsdatei automatisch zum Projekt hinzugefügt. Der Codegenerator bereitet eine Klasse vor , die von INIntent erbt , das Teil des SiriKit- Frameworks ist . Die Intent-Parameter enthalten die magische Option "Intent ist für Widgets geeignet" . Es stehen verschiedene Arten von Parametern zur Verfügung. Sie können Ihre Untertypen anpassen. Da es sich bei den Daten in unserem Fall um eine dynamische Liste handelt, setzen wir auch den Punkt "Optionen werden dynamisch bereitgestellt" .



Legen Sie für verschiedene Widget-Typen die maximale Anzahl von Elementen in der Liste fest - für kleine 1 für mittlere 4.

Diese Art von Absicht wird vom Widget als Datenquelle verwendet.







Als nächstes muss die konfigurierte Intent-Klasse in die IntentConfiguration- Konfiguration eingefügt werden .

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])
    }
}


Wenn keine Benutzereinstellungen erforderlich sind, gibt es eine Alternative in Form der StaticConfiguration-Klasse, die ohne Angabe einer Absicht funktioniert.



Titel und Beschreibung können auf dem Einstellungsbildschirm bearbeitet werden.

Der Name des Widgets muss in eine Zeile passen, sonst wird es abgeschnitten. Gleichzeitig unterscheiden sich die zulässigen Längen für die Einstellungen zum Hinzufügen des Bildschirms und zum Widget.

Beispiele für die maximale Namenslänge für einige Geräte:



iPhone 11 Pro Max
28  
21   

iPhone 11 Pro
25  
19   

iPhone SE
24  
19   


Die Beschreibung ist mehrzeilig. Bei sehr langem Text in den Einstellungen kann der Inhalt gescrollt werden. Auf dem Bildschirm zum Hinzufügen wird jedoch zuerst die Vorschau des Widgets komprimiert, und dann passiert etwas Schreckliches mit dem Layout.







Sie können auch die Hintergrundfarbe und die Werte der Parameter WidgetBackground und AccentColor ändern - standardmäßig befinden sie sich bereits in Assets . Bei Bedarf können sie in der Widget-Konfiguration unter Build-Einstellungen in der Gruppe Asset Catalog Compiler - Optionen in den Feldern Widget-Hintergrundfarbname bzw. Globaler Akzentfarbname umbenannt werden.







Einige Parameter können abhängig vom ausgewählten Wert in einem anderen Parameter über die Einstellung Beziehung ausgeblendet (oder angezeigt) werden .

Es ist zu beachten, dass die Benutzeroberfläche zum Bearbeiten eines Parameters von seinem Typ abhängt. Wenn wir beispielsweise Boolean angeben , sehen wir UISwitch , und wenn Integer , haben wir bereits die Wahl zwischen zwei Optionen: Eingabe über UITextfield oder schrittweise Änderung über UIStepper .







Interaktion mit der Hauptanwendung.



Das Bundle wurde konfiguriert. Es bleibt zu bestimmen, woher die Absicht selbst die realen Daten bezieht. Die Brücke zur Hauptanwendung ist in diesem Fall eine Datei in der allgemeinen Gruppe ( App-Gruppen ). Die Hauptanwendung schreibt, das Widget liest.

Die folgende Methode wird verwendet, um die URL zur allgemeinen Gruppe abzurufen:

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


Wir speichern alle Kandidaten, da diese vom Benutzer in den Einstellungen als Wörterbuch zur Auswahl verwendet werden.

Als nächstes muss das Betriebssystem herausfinden, dass die Daten aktualisiert wurden. Dazu nennen wir:

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


Da der Methodenaufruf den Inhalt des Widgets und die gesamte Zeitleiste neu lädt, verwenden Sie ihn, wenn die Daten tatsächlich aktualisiert wurden, um das System nicht zu überlasten.



Daten aktualisieren



Um den Akku des Geräts des Benutzers zu schonen, hat Apple einen Mechanismus zum Aktualisieren von Daten in einem Widget mithilfe einer Zeitleiste entwickelt - einen Mechanismus zum Generieren von Snapshots . Der Entwickler aktualisiert oder verwaltet die Ansicht nicht direkt , sondern stellt stattdessen einen Zeitplan bereit, anhand dessen das Betriebssystem Snapshots im Hintergrund schneidet.

Das Update erfolgt bei folgenden Ereignissen:

  1. Aufrufen des zuvor verwendeten WidgetCenter.shared.reloadAllTimelines ()
  2. Wenn ein Benutzer dem Desktop ein Widget hinzufügt
  3. Beim Bearbeiten von Einstellungen.


Außerdem verfügt der Entwickler über drei Arten von Richtlinien zum Aktualisieren von Zeitleisten (TimelineReloadPolicy):

atEnd - Aktualisierung nach Anzeige des letzten Snapshots

nie - Aktualisierung nur bei einem erzwungenen Aufruf

nach (_ :) - Aktualisierung nach einer bestimmten Zeitspanne.



In unserem Fall reicht es aus, das System zu bitten, einen Schnappschuss zu erstellen, bis die Kartendaten in der Hauptanwendung aktualisiert sind:



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]
}


Eine flexiblere Option wäre nützlich, wenn ein automatischer Algorithmus zum Auswählen von Karten je nach Wochentag und Uhrzeit verwendet wird.



Unabhängig davon ist die Anzeige eines Widgets zu beachten, wenn es sich in einem Stapel von Widgets befindet ( Smart Stack ). In diesem Fall können wir zwei Optionen zum Verwalten von Prioritäten verwenden: Siri-Vorschläge oder durch Festlegen des Relevanzwerts eines TimelineEntry mit dem Typ TimelineEntryRelevance . TimelineEntryRelevance enthält zwei Parameter:

score - die Priorität des aktuellen Snapshots im Vergleich zu anderen Snapshots;

Die Dauer ist die Zeit, bis das Widget relevant bleibt und das System es an die oberste Position im Stapel setzen kann.



Beide Methoden sowie die Konfigurationsoptionen für das Widget wurden in der WWDC-Sitzung ausführlich erläutert .



Sie müssen auch darüber sprechen, wie Sie die Anzeige von Datum und Uhrzeit auf dem neuesten Stand halten können. Da wir den Inhalt des Widgets nicht regelmäßig aktualisieren können, wurden für die Textkomponente mehrere Stile hinzugefügt. Bei Verwendung eines Stils aktualisiert das System automatisch den Inhalt der Komponente, während sich das Widget auf dem Bildschirm befindet. Vielleicht wird der gleiche Ansatz in Zukunft auf andere SwiftUI-Komponenten ausgedehnt.



Text unterstützt die folgenden Stile:

relativ- die Zeitdifferenz zwischen dem aktuellen und dem angegebenen Datum. Hier ist zu beachten: Wenn das Datum in der Zukunft festgelegt wird, beginnt der Countdown, und danach wird das Datum ab dem Zeitpunkt angezeigt, an dem es Null erreicht. Das gleiche Verhalten gilt für die nächsten beiden Stile.

Offset - ähnlich dem vorherigen, aber es gibt eine Angabe in Form eines Präfixes mit ±;

Timer - analog zu einem Timer;

Datum - Datumsanzeige ;

Zeit - Zeitanzeige.



Darüber hinaus ist es möglich, das Zeitintervall zwischen Datumsangaben durch einfaches Angeben des Intervalls anzuzeigen.



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)
}






Widget-Vorschau



Bei der erstmaligen Anzeige wird das Widget im Vorschaumodus geöffnet. Dazu müssen wir den TimeLineEntry in der Methode placeholder (in :) zurückgeben. In unserem Fall sieht es so aus:

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


Danach wird der redigierte Modifikator (Grund :) mit dem Platzhalterparameter auf die Ansicht angewendet . In diesem Fall werden die Elemente im Widget verschwommen angezeigt.







Wir können diesen Effekt von einigen Elementen entfernen, indem wir den Modifikator unredacted () verwenden .

Die Dokumentation besagt auch , dass der Aufruf der Platzhaltermethode (in :) synchron ist und das Ergebnis im Gegensatz zu getSnapshot (in: Vervollständigung :) und getTimeline (in: Vervollständigung :) so schnell wie möglich zurückgegeben werden sollte



Rundungselemente



In den Richtlinien wird empfohlen, die Rundung von Elementen mit der Rundung des Widgets abzugleichen. Dazu wurde in iOS 14 die ContainerRelativeShape- Struktur hinzugefügt , mit der Sie die Form eines Containers auf eine Ansicht anwenden können.



.clipShape(ContainerRelativeShape()) 


Objective-C-Unterstützung



Wenn Sie dem Widget Objective-C-Code hinzufügen müssen (wir haben beispielsweise die Generierung von Barcode-Bildern darauf geschrieben), geschieht alles auf standardmäßige Weise, indem Sie den Objective-C-Bridging-Header hinzufügen. Das einzige Problem, auf das wir stießen, war, dass Xcode beim Erstellen die automatisch generierten Absichtsdateien nicht mehr sah. Deshalb haben wir sie auch dem Bridging-Header hinzugefügt :



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


Anwendungsgröße



Die Tests wurden mit Xcode 12 Beta 6

ohne Widget durchgeführt: 61,6 MB

Mit einem Widget: 62,2 MB fasse ich



die wichtigsten Punkte zusammen, die im Artikel behandelt wurden:

  1. Widgets sind eine großartige Möglichkeit, SwiftUI in der Praxis kennenzulernen. Fügen Sie sie Ihrem Projekt hinzu, auch wenn die unterstützte Mindestversion niedriger als iOS 14 ist.
  2. WidgetBundle wird verwendet, um die Anzahl der verfügbaren Widgets zu erhöhen. Hier ist ein gutes Beispiel dafür, wie viele verschiedene Widgets ApolloReddit hat.
  3. IntentConfiguration oder StaticConfiguration helfen dabei, benutzerdefinierte Einstellungen im Widget selbst hinzuzufügen, wenn keine benutzerdefinierten Einstellungen erforderlich sind.
  4. Ein freigegebener Ordner im Dateisystem in den freigegebenen App-Gruppen hilft beim Synchronisieren von Daten mit der Hauptanwendung.
  5. Dem Entwickler stehen verschiedene Richtlinien zum Aktualisieren der Zeitleiste zur Verfügung (atEnd, never, after (_ :)).


In diesem Zusammenhang kann der schwierige Weg zur Entwicklung eines Widgets für Beta-Versionen von Xcode als abgeschlossen angesehen werden. Es bleibt nur ein einfacher Schritt - eine Überprüfung im App Store durchzuführen.



PS Die Version mit dem Widget hat die Moderation bestanden und steht jetzt im App Store zum Download zur Verfügung!



Vielen Dank für das Lesen bis zum Ende, ich freue mich über Vorschläge und Kommentare. Machen Sie eine kurze Umfrage, um zu sehen, wie beliebt Widgets bei Benutzern und Entwicklern sind.



All Articles