April 2018
Band 33, Nummer 4
Visual Studio für Mac: Programmieren für watchOS mit Xamarin und Visual Studio für Mac
Von Dawid Borycki
Kleine tragbare Geräte wie persönliche Activity-Tracker und Smartwatches (wie Microsoft Band, Android Wear oder Apple Watch) werden immer beliebter. Diese tragbaren Geräte sind mit verschiedenen Sensoren ausgestattet, die die Gesundheitsparameter des Trägers in Echtzeit überwachen. Viele tragbare Geräte verfügen auch über Kommunikationsschnittstellen, sodass die Sensordaten problemlos an benutzerdefinierte oder dedizierte Clouddienste (z.B. Microsoft Health) zur Speicherung oder erweiterten Verarbeitung übertragen werden können. Dadurch kann das tragbare Gerät als zusätzlicher Endpunkt in einem IoT-Ökosystem (Internet of Things, Internet der Dinge) fungieren. Dies wiederum kann dazu beitragen, die persönliche Gesundheitsfürsorge auf ein neues Niveau zu heben, indem IoT-Vorhersagealgorithmen den Benutzer im Voraus über neu auftretende Gesundheitsprobleme informieren.
Tragbare Geräte können auch benutzerdefinierte Apps ausführen. Entwicklern stehen dedizierte SDKs zur Verfügung. Wie bei vielen mobilen Geräten weist jedoch jede Plattform ihre eigene spezifische API auf, auf die über plattformspezifische Programmiersprachen und Tools zugegriffen werden kann. Um es einfacher zu machen, bietet Xamarin Unterstützung für Android Wear und watchOS innerhalb der Xamarin.Android- bzw. Xamarin.iOS-Bibliotheken. Sie können Apps für tragbare Geräte ähnlich wie mobile Apps entwickeln, indem Sie die gemeinsame .NET-Codebasis verwenden, auf die in den plattformspezifischen Projekten verwiesen wird.
In diesem Artikel zeige ich Ihnen, wie Sie einen solchen Ansatz nutzen können, um die in Abbildung 1 dargestellte watchOS-App zu erstellen. Wenn Sie diese App ausführen, beginnt sie, die Sammlung von Objekten aus dem REST-Webdienst abzurufen. Diese Sammlung besteht aus Platzhalterfotos, von denen jedes einen Titel und eine Bitmap (einfarbiges Bild) aufweist. Zu diesem Zeitpunkt zeigt die App nur eine Schaltfläche mit der Beschriftung „Get list“ (Liste abrufen) an. Diese Schaltfläche ist deaktiviert, bis die Daten heruntergeladen werden, wie in der ersten Zeile von Abbildung 1 gezeigt.
Abbildung 1: Eine Vorschau der watchOS-App
Tippen Sie auf die Schaltfläche, und ein Aktionsblatt (zweite Zeile von Abbildung 1) wird angezeigt, das aus mehreren Schaltflächen besteht, die als Aktionen oder Aktionsschaltflächen definiert sind (bit.ly/2EEUZpL). In diesem Beispiel stellt das Aktionsblatt die Aktionsschaltflächen zur Verfügung, deren Beschriftungen den Bereich der anzuzeigenden Fotos enthalten. Wenn Sie auf eine Aktion tippen, werden die ausgewählten Fotos im Tabellensteuerelement (bit.ly/2Caq0nM) direkt unter der Schaltfläche „Get list“ angezeigt, wie in der letzten Zeile in Abbildung 1 gezeigt. Diese Tabelle ist scrollbar, so dass Sie in der Liste nach unten scrollen können, um alle Fotos in der Gruppe anzuzeigen.
Ich werde die Kommunikation mit dem Webdienst in einer separaten .NET Standard-Klassenbibliothek (bit.ly/2HArfMq) implementieren. Gemäß der Dokumentation, auf die verwiesen wird, ist .NET Standard eine formale Spezifikation der .NET-APIs, die einen einheitlichen Zugriff auf die Programmierschnittstellen aller .NET-Implementierungen ermöglichen soll. Einer der Hauptvorteile dieses Ansatzes besteht darin, dass auf eine gemeinsame Bibliothek in .NET-Projekten leicht verwiesen werden kann, um die bedingte Kompilierung des gemeinsamen Codes zu verringern oder zu eliminieren. Als Ergebnis kann die .NET Standard-Klassenbibliothek ein Mal implementiert und dann in verschiedenen .NET-Projekten für die universelle Windows-Plattform (UWP), .NET Core, ASP.NET Core, Xamarin.iOS, Xamarin.Android, Xamarin.Forms usw. referenziert werden, ohne dass für jede Plattform eine erneute Kompilierung erfolgen muss.
Hier zeige ich, wie gemeinsamer Code in der watchOS-App wiederverwendet werden kann. Für den Webdienst verwende ich den REST-API-Platzhalterserver JSONPlaceholder (jsonplaceholder.typicode.com), der mehrere Ressourcen bereitstellt, einschließlich der in der Beispiel-App verwendeten Fotoressource. Diese Ressource speichert eine Sammlung von Platzhalterbildern, von denen jedes als das folgende JSON-Objekt dargestellt wird:
{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "http://placehold.it/600/92c952",
"thumbnailUrl": "http://placehold.it/150/92c952"
}
Zu jedem Foto gehören eine ID, ein Fotoalbum, ein Titel und zwei URLs, die auf eine Bitmap und ihre Miniaturansicht verweisen. Für diese Übung ist die Bitmap nur ein einfarbiges Bild mit einer Bezeichnung, die die Abmessungen der Bitmap anzeigt.
Alles, was Sie hier sehen, wird mit Visual Studio für Mac erstellt. Den vollständigen Beispielcode für dieses Projekt finden Sie auf GitHub unter github.com/dawidborycki/Photos.
Übergeordnete App and gemeinsamer Code
Die Struktur einer typischen watchOS-Lösung besteht aus drei Projekten (siehe apple.co/2GwXhrn und bit.ly/2EI2dNO). Das erste ist die übergeordnete iOS-App. Zwei weitere Projekte sind für die Watch-App reserviert: das Watch-App Bundle und das WatchKit-Erweiterungsbundle. Die übergeordnete iOS-App wird als Proxy verwendet, um Watch-Bundles an das tragbare Gerät zu übermitteln. Das Watch-App-Bundle enthält Schnittstellenstoryboards. Wie bei iOS verwenden Entwickler Schnittstellenstoryboards, um Szenen und Übergänge zwischen ihnen zu definieren. Schließlich enthält das WatchKit-Erweiterungsbundle Ressourcen und den App-Code.
Beginnen wir mit der Erstellung der übergeordneten iOS-App mithilfe des neuen iOS-Projekts „Einzelansicht“, das sich unter „Neues Projekt“ in Visual Studio für Mac befindet. Ich lege die Projekt- und Projektmappennamen auf „Photos.iOS“ bzw. „Photos“ fest. Nachdem ich das Projekt erstellt habe, ergänze ich die Photos-Projektmappe um ein weiteres Projekt „Photos.Common“, das ich mit der Projektvorlage der .NET Standard-Bibliothek erstelle. Diese Vorlage befindet sich unter der Gruppe „Multi-Plattform“ > „Bibliothek“ von „Neues Projekt“.
Wenn Sie die .NET Standard-Bibliothek erstellen, haben Sie die Möglichkeit, die Version von .NET Standard auszuwählen. Diese Version bestimmt die verfügbare API (je höher die Version, desto mehr Funktionalität können Sie nutzen) und die unterstützten Plattformen (je höher die Version, desto weniger unterstützte Plattformen). Hier lege ich die Version von .NET Standard auf Version 2.0 fest, die bereits die HttpClient-Klasse enthält, um mit dem Webdienst über HTTP zu kommunizieren. Sie können die Liste der APIs für jede .NET Standard-Version mit dem .NET-API-Browser unter bit.ly/2Fl44Fa abrufen.
Nachdem ich das allgemeine Projekt eingerichtet habe, installiere ich ein NuGet-Paket (Newtonsoft.JSON), das zum Deserialisieren von HTTP-Antworten verwendet wird. Um das NuGet-Paket in Visual Studio für Mac zu installieren, gehen Sie wie in Visual Studio für Windows vor. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Knoten „Abhängigkeiten“ > „NuGet“, und wählen Sie dann im Kontextmenü „Pakete hinzufügen“ aus. Visual Studio zeigt ein Fenster an, in dem Sie nach Paketen suchen können. Alternativ können Sie die Paket-Konsole verwenden und das NuGet-Paket über die Befehlszeile installieren.
REST-Client
Jetzt bin ich bereit, die Clientklasse für den Photos-Webdienst zu implementieren. Um die Deserialisierung zu vereinfachen, ordne ich das zuvor gezeigte JSON-Objekt einer C#-Klasse zu. Dies kann manuell oder mit einem speziellen Tool erfolgen. Hier verwende ich JSONUtils (jsonutils.com). Diese Website verfügt über eine intuitive Benutzeroberfläche, die aus den folgenden Elementen besteht:
- Textfeld „Class Name“, in das Sie Ihren Klassennamen eingeben können.
- Textfeld „JSON Text or URL“, in das Sie Ihren JSON-Code oder dessen URL einfügen können.
- Mehreren Optionsfeldern, mit denen Sie Ihre Sprache auswählen können (C#, VB.NET usw.)
- Zwei Kontrollkästchen: „Add Namespace“ und „Pascal Case“
Um die C#-Klasse zu generieren, lege ich „Class Name“ auf „Photo“ fest und füge die folgende URL für das Textfeld „JSON Text or URL“ ein: jsonplaceholder.typicode.com/photos/1. Schließlich aktiviere ich das Kontrollkästchen „Pascal Case“ und klicke auf die Schaltfläche „Submit“. Die generierte Photo-Klasse wird nun unten auf der Seite angezeigt. Die Photo-Klasse (siehe begleitenden Code: Photos.Common/Model/Photo.cs) erfordert keine weiteren Kommentare. Sie besteht nur aus den automatisch implementierten Eigenschaften, die das zuvor gezeigte JSON-Objekt darstellen.
Um den REST-Client zu implementieren, erstelle ich die statische PhotoClient-Klasse (Photos.Common-Projekt). Diese Klasse weist ein Feld vom Typ HttpClient auf. Dieses Feld wird im statischen Konstruktor instanziiert, um die BaseAddress-Eigenschaft so festzulegen, dass sie auf die JSONPlaceholder-URL verweist, wie hier gezeigt:
private static HttpClient httpClient;
static PhotosClient()
{
httpClient = new HttpClient()
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
};
}
Wie Abbildung 2 zeigt, verfügt der PhotosClient über zwei öffentliche Methoden:
- GetByAlbumId: Ruft die Sammlung von Fotos aus dem angegebenen Album mit der GetAsync-Methode der HttpClient-Klasse ab. Nach Prüfung des HTTP-Antwortstatuscodes (CheckStatusCode) wird die sich ergebende Antwort mithilfe einer generischen Hilfsmethode DeserializeResponse in eine Sammlung von C#-Fotoobjekten deserialisiert (Hilfsmethoden werden weiter unten behandelt).
- GetImageData: Ruft das Bytearray ab, das das Foto von der angegebenen URL darstellt. Um die Bilddaten abzurufen, verwende ich GetByteArrayAsync der HttpClient-Klasseninstanz.
Abbildung 2: Öffentliche Methoden der PhotoClient-Klasse
public static async Task<IEnumerable<Photo>> GetByAlbumId(int albumId)
{
var response = await httpClient.GetAsync($"photos?albumId={albumId}");
var photoCollection = new List<Photo>();
try
{
CheckStatusCode(response);
photoCollection = await DeserializeResponse<List<Photo>>(response);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return photoCollection;
}
public static async Task<byte[]> GetImageData(Photo photo)
{
var imageData = new byte[0];
try
{
Check.IsNull(photo);
imageData = await httpClient.GetByteArrayAsync(photo.ThumbnailUrl);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return imageData;
}
PhotoClient implementiert auch zwei private Methoden: CheckStatusCode und DeserializeResponse. Die erste Methode akzeptiert eine Instanz der HttpResponseMessage-Klasse und überprüft den Wert ihrer IsSuccessStatusCode-Eigenschaft, wobei eine Ausnahme ausgelöst wird, wenn der Statuscode ein anderer Code als 200 ist (Erfolgscode).
private static void CheckStatusCode(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Unexpected status code: {response.StatusCode}");
}
}
Die zweite Methode (DeserializeReponse) akzeptiert ebenfalls eine Instanz der HttpResponseMessage-Klasse, liest aber den Nachrichtentext als Zeichenfolge mit der HttpResponseMessage.Content.ReadAsStringAsync-Methode. Der sich ergebende Wert wird dann an die statische DeserializeObject-Methode der Newtonsoft.Json.JsonConvert-Klasse übergeben. Letztere gibt das C#-Objekt des angegebenen Typs zurück, wie hier gezeigt:
private static async Task<T> DeserializeResponse<T>(HttpResponseMessage response)
{
var jsonString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(jsonString);
}
In Abbildung 2 verwende ich auch die IsNull-Methode der benutzerdefinierten Check-Klasse. Die IsNull-Methode führt eine einfache Argumentüberprüfung aus, um zu prüfen, ob das Argument NULL ist. Wenn dies der Fall ist, wird eine Ausnahme vom Typ ArgumentNullException ausgelöst (siehe begleitenden Code: Photos.Common/Helpers/Check.cs).
Die gemeinsame Funktionalität ist nun fertig, sodass ich mit der Implementierung der watchOS-App fortfahren kann.
Die watchOS-App und ihre Struktur
Um die eigentliche watchOS-App zu erstellen, klicke ich zuerst im Projektmappen-Explorer mit der rechten Maustaste auf den Namen der Projektmappe (in diesem Fall „Photos“) und wähle dann „Hinzufügen“ > „Neues Projekt hinzufügen“ aus dem Kontextmenü aus. Ein Dialogfeld „Neues Projekt“ wird angezeigt, in dem ich auf die folgende Registerkarte klicke: „watchOS“ > „App“ (stellen Sie sicher, dass Sie keine Registerkarte „Erweiterung“ oder „Bibliothek“ verwenden). Es wird eine Liste mit Projektvorlagen angezeigt, aus der ich die Projektvorlage „WatchKit App C#“ auswähle. Danach wird die Liste der konfigurierbaren Optionen angezeigt, wie in Abbildung 3 gezeigt.
Abbildung 3: Konfigurieren der watchOS-App
Wie bereits erwähnt, besitzt jede watchOS-App eine zugehörige übergeordnete iOS-App. In diesem Fall ist es die Photos.iOS-App. Anschließend gebe ich im Textfeld „App-Name“ die Angabe „WatchKit“ ein, lege das Ziel auf watchOS 4.2 fest (beachten Sie, dass die einzelnen Elemente in dieser Liste von der installierten SDK-Version abhängen) und deaktiviere dann alle Kontrollkästchen unter der Gruppe „Szenen“.
Nachdem Sie auf die Schaltfläche „Weiter“ geklickt haben, erstellt Visual Studio zwei weitere Projekte: Photos.iOS.WatchKit und Photos.iOS.WatchKitExtension.
Das erste Projekt (WatchKit) enthält das Storyboard, mit dem Sie Szenen und Übergänge zwischen ihnen definieren. Das zweite Projekt (WatchKitExtension) enthält die zugehörige Logik, einschließlich der Ansichtscontroller, die in watchOS als Schnittstellencontroller bezeichnet werden. Sie ändern also typischerweise die Benutzeroberfläche der Watch-App mithilfe des Storyboard-Designers (in Abbildung 4 gezeigt), der durch Doppelklicken auf die Datei „Interface.storyboard“ der WatchKit-App aktiviert wird.
Abbildung 4: Entwerfen der Benutzeroberfläche der watchOS-App
Mit dem Storyboard-Designer können Sie Steuerelemente aus der Toolbox mithilfe von Drag & Drop auf die Szenen ziehen. In Abbildung 4 ist nur eine Szene, die der InterfaceController-Klasse zugeordnet ist, unter dem Projekt „Photos.iOS.WatchKitExtension“ definiert. Diese Klasse ist wie der UIViewController für eine iOS-App, d.h. sie präsentiert und verwaltet Inhalte auf dem Bildschirm und implementiert Methoden, die Benutzerinteraktionen verarbeiten. Beachten Sie, dass Sie nach dem Hinzufügen von Steuerelementen zur Szene deren Eigenschaften im Eigenschaftenpad ändern und dann über die InterfaceController-Klasse auf sie zugreifen können.
Bevor wir die Benutzeroberfläche bearbeiten, sollten wir kurz die Struktur dieser Klasse untersuchen, die aus dem WatchKit.WKInterfaceController (der Basisklasse für alle Schnittstellencontroller) abgeleitet wird. Eine Standardimplementierung von InterfaceController setzt drei Methoden außer Kraft, die sich auf den Lebenszyklus der Ansicht beziehen (siehe apple.co/2GwXhrn):
- Awake: Wird vom System direkt nach der Initialisierung von InterfaceController aufgerufen. Sie verwenden diese Methode in der Regel, um Daten zu laden und die Benutzeroberfläche vorzubereiten.
- WillActivate: Wird aufgerufen, wenn die zugehörige Ansicht im Begriff ist, aktiv zu werden. Mit dieser Methode bereiten Sie letzte Aktualisierungen vor, bevor die Benutzeroberfläche angezeigt wird.
- DidDeactivate: Wird aufgerufen, wenn die Ansicht inaktiv wird. Diese Methode wird typischerweise verwendet, um dynamische Ressourcen freizugeben, die nicht mehr benötigt werden.
Diese Methoden werden verwendet, um Ihre Ansicht abhängig von ihrer Sichtbarkeit zu konfigurieren. Um Ihnen ein einfaches Beispiel zu zeigen, werde ich die folgende Awake-Methode analysieren:
public override void Awake(
NSObject context)
{
base.Awake(context);
SetTitle("Hello, watch!");
}
Dieser Code ruft zunächst die Awake-Methode der Basisklasse (WKInterfaceController) auf und ruft dann die SetTitle-Methode auf, die die in der oberen linken Ecke der Ansicht angezeigte Zeichenfolge ändert. Dieser Titel ist ein Standardelement jedes Schnittstellencontrollers.
Um diese Änderung zu testen, können Sie die Photos.iOS.WatchKit-App im Simulator ausführen. Zunächst müssen Sie diese App (nicht das Erweiterungsbundle) als Startprojekt festlegen, indem Sie ein Dropdownmenü aus der Symbolleiste von Visual Studio für Mac verwenden. Neben dieser Liste gibt es zwei weitere Dropdownlisten: eine Liste zur Auswahl der Konfiguration (Debug oder Release) und eine andere Liste zur Auswahl aus einer Liste von Simulatoren. Hier verwende ich die Debugkonfiguration und den Emulator „Apple Watch-Serie 3 - 42 mm - watchOS 4.2“. Nachdem Sie den Simulator ausgewählt und auf das Symbol „Wiedergabe“ geklickt haben, wird die App kompiliert und bereitgestellt. Beachten Sie, dass zwei Simulatoren gestartet werden: der iOS-Simulator und der zugehörige watchOS-Simulator (siehe linker Bildschirm in Abbildung 1).
Aktionsblatt
Nun kann ich die eigentliche Benutzeroberfläche der Photos.iOS.WatchKit-App implementieren. Wie in Abbildung 1 gezeigt, besteht die Benutzeroberfläche der App aus drei Elementen: einer Schaltfläche, einem Aktionsblatt und einer Tabellenansicht. Wenn der Benutzer auf die Schaltfläche tippt, wird ein Aktionsblatt aktiviert. Es bietet mehrere Optionen, mit denen der Benutzer die Gruppe der Fotos auswählen kann, die in der Tabellenansicht angezeigt werden sollen. Ich habe diese Fotogruppierung implementiert, um der Empfehlung von Apple zu entsprechen, die Anzahl der Zeilen in der Tabellenansicht einzuschränken, um die Leistung der App zu verbessern (apple.co/2Cecrnt).
Ich beginne mit der Erstellung der Liste der Schaltflächen, die auf dem Aktionsblatt angezeigt werden. Die Liste dieser Schaltflächen wird auf der Grundlage der Fotosammlung (Feld „photos“) erstellt, die wie in Abbildung 5 gezeigt aus dem Webdienst abgerufen wird. Jede Aktionsschaltfläche wird als Instanz der WatchKit.WKAction-Klasse dargestellt. Diese Klasse enthält keine öffentlichen Konstruktoren, implementiert aber die statische Create-Methode, mit der Sie Aktionen erstellen. Wie in Abbildung 5 gezeigt, akzeptiert die Create-Methode drei Argumente:
- „Title“ definiert die Beschriftung der Aktionsschaltfläche.
- „Style“ gibt den Stil der Aktionsschaltfläche an.
- „Handler“ gibt eine Methode an, die ausgeführt werden soll, wenn der Benutzer auf die Aktionsschaltfläche tippt.
Abbildung 5: Partitionieren von Fototiteln
private const int rowsPerGroup = 10;
private IEnumerable<Photo> photos;
private WKAlertAction[] alertActions;
private void CreateAlertActions()
{
var actionsCount = photos.Count() / rowsPerGroup;
alertActions = new WKAlertAction[actionsCount];
for (var i = 0; i < actionsCount; i++)
{
var rowSelection = new RowSelection(
i, rowsPerGroup, photos.Count());
var alertAction = WKAlertAction.Create(
rowSelection.Title,
WKAlertActionStyle.Default,
async () => { await DisplaySelectedPhotos(rowSelection); });
alertActions[i] = alertAction;
}
}
In Abbildung 5 weisen alle Aktionen einen Standardstil auf, der als Standardwert aus der WatchKit.WKAlertStyle-Enumeration dargestellt wird. Diese Enumeration definiert zwei weitere Werte (apple.co/2EHCAZr): „Cancel“ (Abbrechen) und „Destructive“ (destruktiv). Mit dem ersten Wert erstellen Sie eine Aktion, die einen Vorgang ohne Änderungen abbricht. Der destruktive Stil sollte auf Aktionen angewendet werden, die irreversible Änderungen hervorrufen.
Die CreateAlertActions-Methode aus Abbildung 5 partitioniert die Fotos in Blöcke, von denen jeder 10 Elemente enthält (rowsPerGroup-Konstante). Um eine ausgewählte Gruppe von Fotos aus der Sammlung abzurufen, benötige ich zwei Indizes: einen, an dem die Gruppe beginnt (beginIndex-Variable) und einen, an dem sie endet (endIndex). Um diese Indizes zu berechnen, verwende ich die RowSelection-Klasse, die auch zum Erstellen von Titeln für die auf dem Aktionsblatt angezeigten Elemente verwendet wird. Die RowSelection-Klasse wird im Projekt Photos.iOS.WatchKitExtension („RowSelection.cs“) implementiert. Ihre wichtigsten Komponenten werden in Abbildung 6 dargestellt.
Abbildung 6: Berechnen von Indizes mit der RowSelection-Klasse
public int BeginIndex { get; private set; }
public int EndIndex { get; private set; }
public int RowCount { get; private set; }
public string Title { get; private set; }
private static string titlePrefix = "Elements to show:";
public RowSelection(int groupIndex, int rowsPerGroup, int elementCount)
{
BeginIndex = groupIndex * rowsPerGroup;
EndIndex = Math.Min((groupIndex + 1) * rowsPerGroup, elementCount) - 1;
RowCount = EndIndex - BeginIndex + 1;
Title = $"{titlePrefix} {BeginIndex}-{EndIndex}";
}
Schließlich verfügt jede Schaltfläche des Aktionsblatts über einen zugehörigen Handler, der die DisplaySelectedPhotos-Methode aufruft. Diese Methode ist für die Darstellung der Tabelle der ausgewählten Fotos verantwortlich und wird später beschrieben.
Um das Aktionsblatt zu aktivieren, verweise ich zunächst auf das Projekt Photos.Common. Dazu klicke ich im Projektmappen-Explorer mit der rechten Maustaste auf „Verweise“ der Photos.iOS.WatchKitExtension, wähle „Verweise bearbeiten“ aus, wähle die Registerkarte „Projekt“ aus und dann „Photos.Common“. Im Verweis-Manager muss ich auch auf die Bibliothek „Newtonsoft.Json.dll“ verweisen, um sicherzustellen, dass sie in das Ausgabeverzeichnis kopiert wird. Dabei verwende ich die Registerkarte „.NET Assembly“, klicke auf die Schaltfläche „Durchsuchen“ und wähle dann „Newtonsoft.Json.dll“ aus dem Ordner „packages/Newtonsoft.Json/lib/netstandard20“ aus. Dieser Ordner wird nach der Installation des NuGet-Pakets Newtonsoft.Json erstellt.
Diese Schritte sind erforderlich, um von der watchOS-Anwendung aus auf eine gemeinsame Codebasis (einschließlich des zuvor implementierten PhotosClient) zuzugreifen. Dann ändere ich die Benutzeroberfläche mithilfe des Storyboards. Eine ausführliche Beschreibung dazu, wie das Layout in watchOS funktioniert, finden Sie in der Apple- (apple.co/2FlzADj) und Xamarin-Dokumentation (bit.ly/2EKjCRM).
Nachdem ich den Storyboard-Designer geöffnet habe, ziehe ich das Button-Steuerelement aus der Toolbox auf die Szene. Mithilfe des Eigenschaftenpads lege ich die Eigenschaften der Schaltfläche „Name“ und „Title“ auf ButtonDisplayPhotoList bzw. Get list fest. Dann erstelle ich den Ereignishandler, der immer dann ausgeführt wird, wenn der Benutzer auf die Schaltfläche klickt. Um einen Ereignishandler zu erstellen, verwende ich das Eigenschaftenpad, klicke auf die Registerkarte „Ereignisse“ und gebe dann ButtonDisplayPhotoList_Activated in das Suchfeld „Aktion“ ein. Nach Drücken der EINGABETASTE deklariert Visual Studio die neue Methode in der InterfaceController-Klasse. Schließlich wird ButtonDisplayPhotoList_Activated wie folgt definiert:
partial void ButtonDisplayPhotoList_Activated()
{
PresentAlertController(string.Empty,
string.Empty,
WKAlertControllerStyle.ActionSheet,
alertActions);
}
Um ein Aktionsblatt zu erstellen und anzuzeigen, verwende ich PresentAlertController. Diese Methode akzeptiert vier Argumente:
- „Title“ gibt den Titel der Warnung an.
- „Message“ gibt den Text an, der im Textkörper der Warnung angezeigt werden soll.
- „PreferredStyle“ gibt den Stil des Warnungscontrollers an. „Style“ wird durch einen der in der WatchKit.WKAlertControllerStyle-Enumeration definierten Werte dargestellt: Alert, SideBySideButtonsAlert oder ActionSheet. Die Unterschiede zwischen ihnen werden unter apple.co/2GA57Rp zusammengefasst.
- „Actions“ sind eine Sammlung von Aktionsschaltflächen, die in die Warnung aufgenommen werden. Beachten Sie, dass die Anzahl der Aktionen vom Warnungsstil abhängt. Weitere Informationen finden Sie in der Dokumentation, auf die verwiesen wird.
Hier werden sowohl der Titel als auch die Meldung auf string.empty festgelegt, während der Warnungsstil auf ActionSheet festgelegt wird. Als Ergebnis werden nur Aktionsschaltflächen angezeigt (siehe Abbildung 1). Um sicherzustellen, dass alertActions bereit sind, bevor der Benutzer auf die Schaltfläche „Get list“ tippt, rufe ich Fotos und Titel in der Awake-Methode ab (wie in Abbildung 7 gezeigt):
Abbildung 7: Abrufen von Fotos und Titeln
public async override void Awake(NSObject context)
{
base.Awake(context);
SetTitle("Hello, watch!");
// Disable button until the photos are downloaded
ButtonDisplayPhotoList.SetEnabled(false);
// Get photos from the web service (first album only)
photos = await PhotosClient.GetByAlbumId(1);
// Create actions for the alert
CreateAlertActions();
ButtonDisplayPhotoList.SetEnabled(true);
}
Sie können die App jetzt ausführen. Wenn das Fotoalbum vom Remoteserver abgerufen wird, wird die Schaltfläche aktiviert. Klicken Sie darauf, um das Aktionsblatt zu aktivieren (im mittleren Teil von Abbildung 1 gezeigt).
Tabellenansicht
Um diese Implementierung abzuschließen, muss ich die Tabellenansicht erstellen, die die ausgewählte Gruppe von Fotos anzeigt. Dazu öffne ich den Storyboard-Designer und ziehe das Table-Steuerelement aus der Toolbox auf die Szene (ich platziere es direkt unter der Schaltfläche „Get list“).
Im nächsten Schritt muss ich das Zellenlayout definieren. Standardmäßig verfügt dieses Layout über ein einziges Steuerelement: „Group“. Ich kann dieses als übergeordnetes Steuerelement für andere Steuerelemente verwenden, aber zuerst muss ich sicherstellen, dass das Gruppensteuerelement aktiv ist. Ich klicke also auf das Dokumentgliederungspad (sichtbar im unteren Teil von Abbildung 4) und dann auf das Gruppensteuerelement unter der Tabelle/Tabellenzeile. Als nächstes ziehe ich die Image- und Label-Steuerelemente auf die Tabelle. Image und Label werden in der Tabellenansicht und auch in der Dokumentgliederung angezeigt. Ich konfiguriere alle Steuerelementeigenschaften wie folgt: Image (Name: ImagePhotoPreview, Größe: feste Breite und Höhe 50 Pixel), Label (Name: LabelPhotoTitle), Table (Name: TablePhotos), Group (Größe: feste Höhe von 50 Pixeln).
Es ist wichtig, Steuerelementnamen explizit festzulegen, damit Sie sie einfach aus dem Code heraus aufrufen können. Nachdem Sie den Steuerelementnamen angegeben haben, nimmt Visual Studio eine entsprechende Deklaration unter der Datei „InterfaceController.designer.cs“ vor.
In watchOS weist jede Zeile einen eigenen Zeilencontroller auf, mit dem Sie das Aussehen der Zeile steuern können. Hier verwende ich diesen Controller, um den Inhalt für das Bild und die Beschriftung jeder Zeile anzugeben. Um den Zeilencontroller zu erstellen, wählen Sie die Tabellenzeile im Dokumentgliederungspad aus, und öffnen Sie dann die Registerkarte „Eigenschaften“, auf der Sie „PhotoRowController“ in das Textfeld „Klasse“ eingeben. Eine neue Datei („PhotoRowController.cs“) wird hinzugefügt. Sie enthält die gleichnamige Klasse. Ich ergänze dann die Definition dieser Klasse wie folgt um eine weitere Methode:
public async Task SetElement(Photo photo)
{
Check.IsNull(photo);
// Retrieve image data and use it to create UIImage
var imageData = await PhotosClient.GetImageData(photo);
var image = UIImage.LoadFromData(NSData.FromArray(imageData));
// Set image and title
ImagePhotoPreview.SetImage(image);
LabelPhotoTitle.SetText(photo.Title);
}
Die SetElement-Funktion akzeptiert ein Argument vom Typ „Photo“, um eine Fotominiaturansicht zusammen mit dem Fototitel in der entsprechenden Zeile der Tabellenansicht anzuzeigen. Um dann Tabellenzeilen tatsächlich zu laden und zu konfigurieren, erweitere ich die Definition von InterfaceController mit der folgenden Methode:
private async Task DisplaySelectedPhotos(RowSelection rowSelection)
{
TablePhotos.SetNumberOfRows(rowSelection.RowCount, "default");
for (int i = rowSelection.BeginIndex, j = 0;
i <= rowSelection.EndIndex; i++, j++)
{
var elementRow = (PhotoRowController)TablePhotos.GetRowController(j);
await elementRow.SetElement(photos.ElementAt(i));
}
}
RowSelection wird an die DisplaySelectedPhotos-Methode übergeben, um die erforderlichen Informationen zu den anzuzeigenden Zeilen bereitzustellen. Genauer gesagt, wird die RowCount-Eigenschaft verwendet, um die Anzahl der Zeilen festzulegen, die der Tabelle hinzugefügt werden sollen (TablePhotos.SetNumberOfRows). Anschließend iteriert DisplaySelectedPhotos durch Tabellenzeilen, um den Inhalt für jede Zeile festzulegen. Bei jeder Iteration rufe ich zunächst einen Verweis auf den PhotoRowController ab, der der aktuellen Zeile zugeordnet ist. Mit diesem Verweis rufe ich die PhotoRowController.SetElement-Methode auf, um Bilddaten und Titel abzurufen, die in der Tabellenzelle angezeigt werden.
Nachdem Sie die Anwendung ausgeführt haben, erhalten Sie schließlich die weiter oben in Abbildung 1 gezeigten Ergebnisse.
Zusammenfassung
In diesem Artikel habe ich gezeigt, wie Sie watchOS-Apps mit Xamarin, Visual Studio für Mac und einer gemeinsamen C# .NET-Codebasis entwickeln, die in der .NET Standard-Klassenbibliothek implementiert ist. Dabei habe ich einige der wichtigsten Elemente von watchOS-Apps untersucht, darunter die App-Struktur, Schnittstellencontroller und ausgewählte Steuerelemente der Benutzeroberfläche (Schaltfläche, Aktionsblatt, Tabellenansicht). Da die gemeinsame Codebasis mit dem gleichen Ansatz wie bei mobilen Anwendungen implementiert wird, können Sie Ihre mobile Lösung problemlos auf tragbare Smartgeräte erweitern. Weitere Informationen zu watchOS erhalten Sie von Apple unter apple.co/2EFLeaL. Eine ausführliche Xamarin-Dokumentation finden Sie unter bit.ly/2ohSwLU.
Dawid Borycki ist Software Engineer und biomedizinischer Forscher, Autor und spricht häufig auf Konferenzen. Er beschäftigt sich gern mit neuen Technologien für Softwareexperimente und -prototypen.
Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Brad Umbaugh
Brad Umbaugh erstellt iOS- und zugehörige Dokumentation für das Xamarin-Team bei Microsoft. Folgen Sie ihm auf Twitter @bradumbaugh, wo er vor allem schlechte Wortspiele retweetet.