Windows Phone

Erstellen einer App für Windows Phone und iOS

Andrew Whitechapel

Dokumentation über die App-Portierung von iOS zu Windows Phone ist schon reichlich vorhanden. In diesem Artikel gehe ich allerdings von der Annahme aus, dass Sie von Grund auf eine neue App für beide Plattformen erstellen möchten. Ich werde keinerlei Bewertung darüber abgeben, welche der Plattformen besser ist. Stattdessen werde ich bei der Erstellung der App einem praktischen Ansatz folgen und die Unterschiede bzw. die Ähnlichkeiten beider Plattformen, die einem dabei begegnen, beschreiben.

Als ein Mitglied des Windows Phone-Teams liegt mir die Windows Phone-Plattform zwar besonders am Herzen, der springende Punkt hier soll aber nicht sein, dass eine Plattform der anderen überlegen ist, sondern dass die Plattformen unterschiedlich sind und unterschiedliche Programmieransätze erfordern. Obwohl Sie iOS-Apps mithilfe des MonoTouch-Systems in C# entwickeln können, handelt es sich dabei doch um eine selten genutzte Umgebung. Für diesen Artikel verwende ich daher Standard-Xcode und Objective-C für iOS sowie C# für Windows Phone.

Ziel-UX

Ich möchte in beiden Versionen der App die gleiche Benutzerfreundlichkeit (UX) erreichen und dabei aber sicherstellen, dass jede Version dem Modell und der Philosophie der Zielplattform treu bleibt. Zur Verdeutlichung beachten Sie, dass die Windows Phone-Version der App die Hauptbenutzeroberfläche mit einem vertikalen Listenfeld für den Bildlauf implementiert, die iOS-Version die gleiche Benutzeroberfläche hingegen mit einem horizontalen ScrollViewer-Element umsetzt. Offensichtliche handelt es sich hierbei nur um softwarebedingte Unterschiede. Ich könnte also in iOS eine vertikale Bildlaufliste erstellen oder eine horizontale in Windows Phone. Diese Eigenschaften zu erzwingen würde allerdings bedeuten, der jeweiligen Designphilosophie weniger treu zu bleiben, und ich möchte derartige „Verfremdungen“ gerne vermeiden.

Die App „SeaVan“ zeigt die vier Grenzübergänge an der Landesgrenze zwischen Seattle in den Vereinigten Staaten und Vancouver, British Columbia, in Kanada mit den Wartezeiten an den jeweiligen Übergängen an. Die App ruft die Daten mittels HTTP von den Websites der Regierungen der USA und Kanada ab. Die Aktualisierung der Daten erfolgt entweder manuell über eine Schaltfläche oder automatisch über einen Timer.

Abbildung 1 veranschaulicht die beiden Implementierungen. Ein Unterschied, den Sie bemerken werden, ist der, dass die Windows Phone-Version designabhängig ist und die aktuelle Akzentfarbe verwendet. Im Gegensatz dazu verfügt die iOS-Version über kein Design- oder Farbkonzept.

The Main UI Screen for the SeaVan App on an iPhone and a Windows Phone Device
Abbildung 1: Der Bildschirm der Hauptbenutzeroberfläche der App „SeaVan“ auf dem iPhone und einem Windows Phone-Gerät

Das Windows Phone-Gerät folgt einem strikt linearen, seitenbasierten Navigationsmodell. Die wesentlichen Bildschirmelemente der Benutzeroberfläche werden seitenweise angezeigt, und der Benutzer navigiert vorwärts und rückwärts durch den Seitenstapel. Sie können die gleiche lineare Navigation auf dem iPhone umsetzen. Das iPhone ist jedoch nicht an dieses Modell gebunden und ermöglicht Ihnen, jedes Bildschirmmodell zu nutzen, das Ihnen gefällt. Bei der iOS-Version von „SeaVan“ handelt es sich bei Zusatzbildschirmen wie „About“ um modale View Controller-Elemente. Vom technologischen Standpunkt aus betrachtet, entsprechen diese annähernd den modalen Popups von Windows Phone.

Abbildung 2 zeigt eine schematische Darstellung der generalisierten Benutzeroberfläche, auf der interne Elemente der Benutzeroberfläche weiß, und externe Elemente (Startprogramme/Auswahl in Windows Phone, freigegebene Anwendungen in iOS) in orange dargestellt werden. Die Benutzeroberfläche für Einstellungen (in hellgrün) stellt eine Ausnahme dar, auf die ich später im Artikel noch eingehen werde.

Generalized Application UI
Abbildung 2: Generalisierte Benutzeroberfläche der Anwendung

Ein weiterer Unterschied besteht bei den Benutzeroberflächen darin, dass Windows Phone ein ApplicationBar-Objekt als standardisiertes Element der Benutzeroberfläche verwendet. In „SeaVan“ findet der Benutzer in dieser Leiste die Schaltflächen zum Aufrufen von App-Zusatzfeatures (wie den Seiten „About“ oder „Einstellungen“) sowie zur manuellen Datenaktualisierung. Es ist kein direktes iOS-Äquivalent zum ApplicationBar-Objekt vorhanden. Daher vermittelt in der iOS-Version von „SeaVan“ eine einfache Symbolleiste die entsprechende Benutzerfreundlichkeit.

Umgekehrt verfügt die iOS-Version über ein PageControl-Element; die schwarze Leiste mit vier Positionspunktanzeigen am unteren Rand des Bildschirms. Der Benutzer kann einen horizontalen Bildlauf durch die vier Grenzübergänge durchführen, indem er entweder eine Streifbewegung ausführt oder auf das PageControl-Element tippt. Bei „SeaVan“ von Windows Phone gibt es kein Äquivalent zum PageControl-Element. Stattdessen führt der Windows Phone-Benutzer bei „SeaVan“ einen Bildlauf durch die vier Grenzübergänge durch, indem er den Inhalt mit einer Streifbewegung direkt verschiebt. Eine Folge bei der Verwendung eines PageControl-Elements ist die einfache Konfiguration, sodass jede Seite angedockt wird und vollständig sichtbar ist. Beim Listenfeld für den Bildlauf von Windows Phone gibt es dafür keine standardmäßige Unterstützung und der Benutzer kann sich mit Teilansichten zweier Grenzübergänge konfrontiert sehen. Das ApplicationBar-Objekt und das PageControl-Element sind Beispiele für Bereiche, an denen ich nicht versucht habe, die Benutzerfreundlichkeit beider Versionen stärker anzugleichen, als sie bei Verwendung des Standardverhaltens sein können.

Architekturentscheidungen

Die Verwendung der Model-View-ViewModel-Architektur (MVVM) wird in beiden Plattformen angeregt. Ein Unterschied besteht darin, dass Visual Studio Code generiert, der einen Verweis auf das Haupt-ViewModel im Anwendungsobjekt enthält. Bei Xcode geschieht das nicht. Hier steht es Ihnen frei, Ihr ViewModel an die App anzukoppeln, wo sie es wünschen. Es ist auf beiden Plattformen sinnvoll, das ViewModel in das Anwendungsobjekt zu binden.

Ein weitaus bedeutsamerer Unterschied besteht im Mechanismus, mit dem die Modelldaten durch das ViewModel zur Ansicht fließen. Bei Windows Phone wird das durch Datenbindung erreicht, mit der Sie in XAML angeben können, wie die Elemente der Benutzeroberfläche mit den Daten des ViewModels verknüpft werden können. Die eigentliche Werteverteilung erfolgt dann zur Laufzeit. In den Standardbibliotheken bei iOS gibt es kein Äquivalent zur Datenbindung, obwohl Drittanbieterbibliotheken vorhanden sind, die (auf Grundlage des Key-Value Observer-Musters) ein ähnliches Verhalten bereitstellen. Stattdessen muss die Verteilung der Datenwerte zwischen dem ViewModel und der Ansicht bei der App manuell erfolgen. Abbildung 3 veranschaulicht die generalisierte Architektur und die Komponenten von „SeaVan“ mit ViewModels in rosa und Ansichten in blau.

Generalized SeaVan Architecture
Abbildung 3: Generalisierte Architektur von „SeaVan“

Objective-C und C#

Ein detaillierter Vergleich zwischen Objective-C und C# sprengt offensichtlich den Rahmen dieses kurzen Artikels. Abbildung 4 stellt jedoch eine ungefähre Zuordnung der Schlüsselkonstrukte bereit.

Abbildung 4: Schlüsselkonstrukte in Objective-C und ihre Äquivalente in C+#

Objective-C Begriff C#-Gegenstück
@interface Foo : Bar {} Klassendeklaration, einschließlich Vererbung Klasse Foo : Bar {}

@implementation Foo

@end

Klassenimplementierung Klasse Foo : Bar {}
Foo* f = [[Foo alloc] init] Klasseninstanziierung und -initialisierung Foo f = new Foo();
-(void) doSomething {} Deklaration von Instanzenmethoden void doSomething() {}
+(void) doOther {} Deklaration von Klassenmethoden static void doOther() {}

[myObject doSomething];

oder

myObject.doSomething;

Eine Nachricht an ein Objekt übermitteln (eine Methode zu einem Objekt aufrufen) myObject.doSomething();
[self doSomething] Eine Nachricht an das aktuelle Objekt übermitteln (eine Methode des aktuellen Objekts aufrufen) this.doSomething();
-(id)init {} Initialisierer (Konstruktor) Foo() {}
-(id)initWithName:(NSString*)n price:(int)p {} Initialisierer (Konstruktor) mit Parametern Foo(String n, int p) {}
@property NSString *name; Eigenschaftsdeklaration public String Name { get; set; }
@interface Foo: NSObject <UIAlertViewDelegate> Foo implementiert die Unterklasse „NSObjekt“ und das Protokoll „UIAlertViewDelegate“ (ungefähr mit einer C#-Schnittstelle gleichzusetzen) Klasse Foo: IAnother

Kernanwendungskomponenten

Um mit der App „SeaVan“ zu beginnen, erstelle ich in Xcode eine neue Single-View-Anwendung und eine Windows Phone-Anwendung in Visual Studio. Beide Tools erstellen ein Projekt mit einem Satz Anfangsdateien, einschließlich der Klassen des Anwendungsobjekts und der Hauptseite bzw. -ansicht.

Bei iOS entsprechen Präfixe aus zwei Buchstaben der Konvention. Daher werden alle benutzerdefinierten Klassen in „SeaVan“ mit dem Präfix „SV.“ versehen. Eine iOS-App beginnt mit der in C üblichen Main-Methode, die einen App-Delegaten erstellt. In „SeaVan“ ist das eine Instanz der Klasse „SVAppDelegate“. Der App-Delegat entspricht dem App-Objekt bei Windows Phone. Ich habe das Projekt in Xcode mit eingeschalteter automatischer Verweiszählung (Automatic Reference Counting, ARC) erstellt. Wie im folgenden gezeigt wird dadurch eine den gesamten Code in Main umschließende @autoreleasepool scope-Deklaration hinzugefügt:

int main(int argc, char *argv[])
{
  @autoreleasepool {
    return UIApplicationMain(
    argc, argv, nil, 
    NSStringFromClass([SVAppDelegate class]));
  }
}

Das System führt nun eine automatische Verweiszählung der von mir erstellten Objekte durch und gibt sie automatisch aus, wenn die Zählung null erreicht. Der @autoreleasepool erledigt die meisten der üblichen C/C++-Speicherverwaltungsaufgaben sauber und bringt die Codierungserfahrung deutlich näher an C# heran.

In der Schnittstellendeklaration für SVAppDelegate spezifiziere ich diesen dann als <UIApplicationDelegate>. Damit reagiert er auf Standard-App-Delegatnachrichten, wie „application:didFinishLaunchingWith­Options“. Ich deklariere zudem eine SVContentController-Eigenschaft. Da entspricht bei „SeaVan“ der Standard-MainPage-Klasse von Windows Phone. Die letzte Eigenschaft ist ein SVBorderCrossings-Zeiger. Das ist mein Haupt-ViewModel mit einer Aufzählung von SVBorderCrossing-Elementen, die jeweils einen Grenzübergang darstellen:

@interface SVAppDelegate : UIResponder <UIApplicationDelegate>{}
@property SVContentController *contentController;
@property SVBorderCrossings *border;
@end

Sobald „Main“ startet, wird der App-Delegat initialisiert und das System übermittelt ihm die Anwendungsnachricht mit der Auswahl „didFinishLaunching­WithOptions“. Vergleichen Sie das mit Windows Phone, wo der „Anwendungsstart“ oder der „Aktivierte Ereignishandler“ die logischen Äquivalente stellen würden. An dieser Stelle lade ich eine XIB-Datei (Xcode Interface Builder-Datei) namens SVContent und verwende Sie zum Initialisieren meines Hauptfensters. Das Gegenstück zu einer XIB-Datei ist bei Windows Phone die XAML-Datei. XIB-Dateien sind eigentlich XML-Dateien, obwohl sie normalerweise indirekt mit dem grafischen XIB-Editor von Xcode bearbeitet werden, der dem grafischen XAML-Editor von Visual Studio ähnlich ist. Meine SVContentController-Kasse wird auf die gleiche Weise mit der Datei „SVContent.xib“ verknüpft, in der die Windows Phone MainPage-Klasse mit der Datei MainPage.xaml“ verknüpft wird.

Schließlich instanziiere ich das ViewModel „SVBorderCrossings“ und rufe dessen Initialisierer auf. Bei iOS führen Sie Zuordnung und Initialisierung üblicherweise in einer Anweisung aus, um die potenziellen Fallstricke bei der Verwendung nicht initialisierter Objekte zu vermeiden:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [[NSBundle mainBundle] loadNibNamed:@"SVContent" 
    owner:self options:nil];
  [self.window addSubview:self.contentController.view];
  border = [[SVBorderCrossings alloc] init];
  return YES;
}

Bei Windows Phone erfolgt das Gegenstück zum XIB-Ladevorgang normalerweise für Sie im Hintergrund. Das Build erstellt diesen Teil des Codes unter Verwendung Ihrer XAML-Datei. Wenn Sie zum Beispiel versteckte Dateien wieder in Ihrem Obj-Ordner einblenden, können Sie die InitializeComponent-Methode, die das XAML des Objekts lädt, in der MainPage.g.cs sehen:

public partial class MainPage : Microsoft.Phone.Controls.PhoneApplicationPage
{
  public void InitializeComponent()
  {
    System.Windows.Application.LoadComponent(this,
      new System.Uri("/SeaVan;component/MainPage.xaml",
      System.UriKind.Relative));
  }
}

Die SVContentController-Klasse wird als meine Hauptseite eine Bildlaufansicht hosten, die ihrerseits vier View Controller-Elemente hosten wird. Jedes View Controller-Element wird letztlich mit Daten von einem der vier Grenzübergänge zwischen Seattle und Vancouver aufgefüllt. Ich deklariere die Klasse als <UIScrollViewDelegate> und definiere drei Eigenschaften: ein UIScrollView-Element, ein UIPageControl-Element und ein NSMutableArray von View Controller-Elementen. ScrollView und pageControl werden beide als IBOutlet-Eigenschaften deklariert. Dadurch kann ich sie mit den UI-Artefakten im XIB-Editor verbinden. In Windows Phone erfolgt das entsprechend, wenn ein Element in XAML mit x:Name deklariert und so ein Klassenfeld erstellt wird. Bei iOS können Sie die Elemente Ihrer XIB-Benutzeroberfläche auch mit IBAction-Eigenschaften verbinden. Damit können Sie UI-Ereignisse ankoppeln. Bei Silverlight erfolgt das, wenn Sie, sagen wir, einen Klick-Handler in XAML hinzufügen, um das Ereignis anzukoppeln und Stubcode für den Ereignis-Handler in der Klasse bereitstellen. Interessanterweise bildet mein SVContentController-Element keine Unterklassen aus irgend einer UI-Klasse. Stattdessen wird die Basisklasse „NSObject“ als Unterklasse implementiert. Sie übernimmt in „SeaVan“ die Funktion eines Elements der Benutzeroberfläche, da sie das <UIScrollViewDelegate>-Protokoll implementiert. Das bedeutet, sie reagiert auf scrollView-Nachrichten:

@interface SVContentController : NSObject <UIScrollViewDelegate>{}
@property IBOutlet UIScrollView *scrollView;
@property IBOutlet UIPageControl *pageControl;
@property NSMutableArray *viewControllers;
@end

Bei der Implementierung des SVContentController-Elements, ist (die von NSObject geerbte) „awakeFromNib“ die erste aufzurufende Methode. An diesem Punkt erstelle ich das Array von SVViewController-Objekten und füge dem scrollView-Element die Ansicht jeder Seite hinzu:

- (void)awakeFromNib
{   
  self.viewControllers = [[NSMutableArray alloc] init];
  for (unsigned i = 0; i < 4; i++)
  {
    SVViewController *controller = [[SVViewController alloc] init];
    [controllers addObject:controller];
    [scrollView addSubview:controller.view];
  }
}

Schließlich erhalte ich die scrollViewDidScroll-Nachricht, wenn ein Benutzer eine Streifbewegung auf des scrollView-Elements ausführt oder auf das Seiten-Steuerelement tippt. In dieser Methode wechsle ich den PageControl-Indikator, sobald mehr als die Hälfte der „vorherige/nächste Seite“-Anzeige sichtbar ist. Dann lade ich die sichtbare Seite sowie die beiden angrenzenden Seiten (dadurch vermeide ich ein Flackern, wenn der Benutzer mit einem Bildlauf beginnt). Als Letztes rufe ich noch eine private Methode auf: „updateViewFromData“. Die ruft die Daten des ViewModels ab und fügt sie manuell in jedes Feld der Benutzeroberfläche ein.

- (void)scrollViewDidScroll:(UIScrollView *)sender
{
  CGFloat pageWidth = scrollView.frame.size.width;
  int page = floor((scrollView.contentOffset.x - 
    pageWidth / 2) / pageWidth) + 1;
  pageControl.currentPage = page;
  [self loadScrollViewWithPage:page - 1];
  [self loadScrollViewWithPage:page];
  [self loadScrollViewWithPage:page + 1];
  [self updateViewFromData];
}

Bei Windows Phone wird die entsprechende Funktionalität deklarativ in der XAML in der MainPage implementiert. Ich zeige die Grenzübergangszeiten mithilfe von TextBlock-Steuerelementen innerhalb der Datenvorlage eines Listenfelds an. Das Listenfeld führt für jeden Datensatz automatisch einen Bildlauf in den Sichtbereich durch. Daher gibt es für das „SeaVan“-App bei Windows Phone auch keinen benutzerdefinierten Code zur Behandlung von Bildlaufgesten. Es ist kein Gegenstück zur updateViewFromData-Methode vorhanden, da dieser Vorgang durch die Datenbindung erledigt wird.

Beziehen und Parsen von Webdaten

Die SVAppDelegate-Klasse deklariert , zusätzlich zu ihrer Funktion als App-Delegat, Felder und Eigenschaften, um das Abrufen und Parsen der Übergangsdaten der Websites aus den USA und aus Kanada zu unterstützen. Ich deklariere zwei NSURLConnection-Felder für die HTTP-Verbindung mit den beiden Websites. Außerdem deklariere ich auch zwei NSMutableData-Felder. Das sind Puffer, die ich zum Anhängen jedes Datenstücks verwende, sobald es ankommt. Ich aktualisiere die Klasse, um das <NSXMLParserDelegate>-Protokoll zu implementieren. Damit wird sowohl ein Standard-App-Delegat, als auch ein XML-Parser-Delegat daraus. Sobald XML-Daten empfangen werden, wird zum Parsen der Daten zunächst diese Klasse aufgerufen. Da mir bekannt ist, dass ich es mit zwei vollständig unterschiedlichen XML-Datensätzen zu tun bekommen, übergebe ich die Arbeit sofort an einen von zwei untergeordneten Parserdelegaten. Dafür deklariere Ich ein Paar benutzerdefinierter SVXMLParserUs/­SVXMLParserCa-Felder. Über die Klasse wird auch einen Timer für das automatische Aktualisierungs-Feature deklariert. Für jedes Timerereignis rufe ich, wie in Abbildung 5 gezeigt, die refreshData-Methode auf.

Abbildung 5: Schnittstellendeklaration für „SVAppDelegate“

@interface SVAppDelegate : 
  UIResponder <UIApplicationDelegate, NSXMLParserDelegate>
{
  NSURLConnection *connectionUs;
  NSURLConnection *connectionCa;
  NSMutableData *rawDataUs;
  NSMutableData *rawDataCa;
  SVXMLParserUs *xmlParserUs;
  SVXMLParserCa *xmlParserCa;
  NSTimer *timer;
}
@property SVContentController *contentController;
@property SVBorderCrossings *border;
- (void)refreshData;
@end

Die refreshData-Methode ordnet jedem ankommenden Datensatz einen veränderlichen Datenpuffer zu und stellt die beiden HTTP-Verbindungen her. Ich verwende eine benutzerdefinierte SVURLConnectionWithTag-Klasse, die „NSURLConnection“ als Unterklasse implementiert. Denn das iOS-Parserdelegatmodell erfordert das Anstoßen beider Anfragen aus dem selben Objekt heraus. Darüber hinaus kehren die gesamten Daten auch wieder in dieses Objekt zurück. Daher benötige ich eine Möglichkeit, zwischen den ankommenden Daten aus den USA und denen aus Kanada zu unterscheiden. Dazu füge ich schlicht einen Tag an jede Verbindung an und speichere beide Verbindungen in einem NSMutableDictionary-Objekt zwischen. Bei der Initialisierung jeder Verbindung, bestimme ich „self“ als den Delegaten. Jedes Mal, wenn ein Datenabschnitt empfangen wird, wird die connectionDidReceiveData-Methode aufgerufen, und ich implementiere sie, um die Daten an den entsprechenden Puffer für diesen Tag anzuhängen (siehe Abbildung 6).

Abbildung 6: Einstellung der HTTP-Verbindungen

static NSString *UrlCa = @"http://apps.cbp.gov/bwt/bwt.xml";
static NSString *UrlUs = @"http://wsdot.wa.gov/traffic/rssfeeds/CanadianBorderTrafficData/Default.aspx";
NSMutableDictionary *urlConnectionsByTag;
- (void)refreshData
{
  rawDataUs = [[NSMutableData alloc] init];
  NSURL *url = [NSURL URLWithString:UrlUs];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];   
  connectionUs =
  [[SVURLConnectionWithTag alloc]
    initWithRequest:request
    delegate:self
    startImmediately:YES
    tag:[NSNumber numberWithInt:ConnectionUs]];
    // ... Code omitted: set up the Canadian connection in the same way
}

Ich muss auch „connectionDidFinishLoading“ implementieren. Wenn die gesamten Daten (für die eine oder andere der beiden Verbindungen) empfangen wurden, bestimme diesen App-Delegaten als ersten Parser. Die Parse-Nachricht ist ein blockierender Aufruf. Wenn sie zurückgegeben wird, kann ich „updateViewFromData“ zur Aktualisierung der Benutzeroberfläche der geparsten Daten in meinem Inhalts-Steuerelement aufrufen:

- (void)connectionDidFinishLoading:(SVURLConnectionWithTag *)connection
{
  NSXMLParser *parser = 
    [[NSXMLParser alloc] initWithData:
  [urlConnectionsByTag objectForKey:connection.tag]];
  [parser setDelegate:self];
  [parser parse];
  [_contentController updateViewFromData];
}

Im Allgemeinen gibt es zwei Arten von XML-Parsern:

  • Einfache API-Parser für XML (SAX), bei denen Ihr Code benachrichtigt wird, sowie der Parser die XML-Struktur durchläuft.
  • DOM-Parser (Document Object Model-Parser), die das gesamte Dokument lesen und im Speicher eine Darstellung davon erstellen, die Sie nach unterschiedlichen Elementen abfragen können.

Der standardmäßige NSXMLParser in iOS ist ein SAX-Parser. DOM-Parser von Drittanbietern sind für die Nutzung in iOS zwar erhältlich, ich wollte jedoch Standardplattformen vergleichen, ohne auf Bibliotheken von Drittanbietern zurückzugreifen. Der Standardparser arbeitet sich nacheinander durch jedes Element ohne die Position des aktuelle Elements im gesamten XML-Dokument zu beachten. Daher behandelt der aktuelle Parser bei „SeaVan“ die äußersten Blöcke, um die er sich kümmert und gibt diese an einen untergeordneten Delegatparser weiter, der den nächsten, weiter innen liegenden Block behandelt.

In der Parserdelegatmethode führe ich zur Unterscheidung zwischen den US-XML-Daten und den kanadischen XML-Daten einen einfachen Test durch. Außerdem instanziiere ich den entsprechenden untergeordneten Parser und bestimme das untergeordnete Element von diesem Moment an als aktuellen Parser. Darüber hinaus stelle ich das übergeordnete Element dieses untergeordneten Elementes auf „self“ ein. Damit kann das untergeordnete Element die Parsing-Steuerung wieder an das übergeordnete Element zurückgeben, sobald das Ende der XML-Daten, die es behandeln kann, erreicht ist (siehe Abbildung 7).

Abbildung 7: Die Parserdelegatmethode

- (void)connection:(SVURLConnectionWithTag *)connection didReceiveData:(NSData *)data
{
  [[urlConnectionsByTag objectForKey:connection.tag] appendData:data];
}
- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
  qualifiedName:(NSString *)qName
  attributes:(NSDictionary *)attributeDict
{
  if ([elementName isEqual:@"rss"]) // start of US data
  {
    xmlParserUs = [[SVXMLParserUs alloc] init];
    [xmlParserUs setParentParserDelegate:self];
    [parser setDelegate:xmlParserUs];
  }
  else if ([elementName isEqual:@"border_wait_time"]) // start of Canadian data
  {
    xmlParserCa = [[SVXMLParserCa alloc] init];
    [xmlParserCa setParentParserDelegate:self];
    [parser setDelegate:xmlParserCa];
  }
}

Für den entsprechenden Windows Phone-Code setze ich zunächst eine Webanfrage an die US-Website und dann eine an die kanadische Website auf. Dabei verwende ich ein WebClient-Element, obwohl ein HttpWebRequest hinsichtlich optimaler Leistung und Ansprechbarkeit meistens angebrachter ist. Ich richte einen Handler für das OpenReadCompleted-Ereignis ein und öffne die Anfrage dann asynchron:

public static void RefreshData()
{
  WebClient webClientUsa = new WebClient();
  webClientUsa.OpenReadCompleted += webClientUs_OpenReadCompleted;
  webClientUsa.OpenReadAsync(new Uri(UrlUs));
  // ... Code omitted: set up the Canadian WebClient in the same way
}

Im OpenReadCompleted-Ereignishandler der jeweiligen Anfrage entnehme ich die als Streamobjekt zurückgegebenen Daten und gebe sie zum Parsen an ein Helferobjekt weiter. Da mir zwei unabhängige Webanfragen und zwei unabhängige OpenReadCompleted-Ereignishandler vorliegen, muss ich die Anfragen nicht mit einem Tag versehen und keinen Test zur Identifikation der Daten durchzuführen. Zur Erstellung des gesamten XML-Dokuments muss ich die ankommenden Datenabschnitte nicht weiter behandeln. Stattdessen kann ich mich zurücklehnen und warten, bis die Daten empfangen wurden:

private static void webClientUs_OpenReadCompleted(object sender, 
  OpenReadCompletedEventArgs e)
{
  using (Stream result = e.Result)
  {
    CrossingXmlParser.ParseXmlUs(result);
  }
}

Zum Parsern der XML enthält Silverlight, im Gegensatz zu iOS, standardmäßig einen durch die XDocument-Klasse dargestellten DOM-Parser. Anstelle einer Parserhierarchie kann ich das XDocument-Element also direkt für die gesamt Parsingarbeit nutzen:

internal static void ParseXmlUs(Stream result)
{
  XDocument xdoc = XDocument.Load(result);
  XElement lastUpdateElement = 
    xdoc.Descendants("last_update").First();
  // ... Etc.
}

Ansichten und Dienste unterstützen

Bei Windows Phone ist das App-Objekt statisch und für jede andere Komponente in der Anwendung verfügbar. Dem entsprechend, ist bei iOS ein UIApplication-Delegattyp in der App verfügbar. Um die Dinge etwas zu vereinfachen, definiere ich ein Makro, das ich in der gesamten App zum Erreichen des App-Delegaten verwenden kann, der angemessen zu einem bestimmten SVAppDelegate-Typ ausgeworfen wird:

#define appDelegate ((SVAppDelegate *) [[UIApplication sharedApplication] delegate])

Dadurch kann ich beispielsweise die refreshData-Methode des App-Delegaten aufzurufen, sobald der Benutzer die Schaltfläche „Aktualisieren“ antippt, bei der es sich um eine dem View Controller-Element zugehörige Schaltfläche handelt:

- (IBAction)refreshClicked:(id)sender
{
  [appDelegate refreshData];
}

Wenn der Benutzer die Schaltfläche „About“ antippt, soll ein wie in Abbildung 8 dargestellter „About“-Bildschirm, angezeigt werden. In iOS instanziiere ich dazu ein SVAboutViewController-Element. Dieses verfügt über einen zugeordneten XIB mit einem Bildlauftextelement für die Bedienungsanleitung und drei zusätzlichen Schaltflächen in einer Symbolleiste.

The SeaVan About Screen in iOS and Windows Phone
Abbildung 8: Der „About“-Bildschirm von „SeaVan“ in iOS und Windows Phone

Zur Darstellung des View Controller-Elements instanziiere ich es und übermittle dem aktuellen Objekt (self) eine presentModalViewController-Nachricht.

- (IBAction)aboutClicked:(id)sender
{
  SVAboutViewController *aboutView =
    [[SVAboutViewController alloc] init];
  [self presentModalViewController:aboutView animated:YES];
}

Dann implementiere ich in der SVAboutViewController-Klasse eine Schaltfläche „Abbrechen“ zum Entfernen des View Controller-Elements und kehre die Steuerung auf das aufrufende View Controller-Element um:

- (IBAction) cancelClicked:(id)sender
{
  [self dismissModalViewControllerAnimated:YES];
}

Beide Plattformen bieten standardmäßig die Möglichkeit, mit einer App die Funktionalität integrierter Apps, wie Email, Telefon und SMS, aufzurufen. Der Hauptunterschied besteht darin, ob die Steuerung zur App zurückgegeben wird, nachdem eine Rückgabe durch die integrierte Funktionalität erfolgt ist. Das geschieht in Windows Phone immer. In iOS geschieht das bei einigen Features, doch nicht bei allen.

Sobald der Benutzer die Schaltfläche „Support“ antippt, möchte ich im SVAboutViewController-Element eine Email entwerfen, die der Benutzer an das Entwicklerteam senden kann. Das erneut in Modalansicht dargestellte MFMailComposeViewController-Element funktioniert für diesen Zweck sehr gut. Das Standard-View Controller-Element implementiert zudem eine Schaltfläche „Abbrechen“, die genau die gleiche Funktion erfüllt: sich selbst zu entfernen und die Steuerung auf seine aufrufende Ansicht umzukehren.

- (IBAction)supportClicked:(id)sender
{
  if ([MFMailComposeViewController canSendMail])
  {
    MFMailComposeViewController *mailComposer =
      [[MFMailComposeViewController alloc] init];
    [mailComposer setToRecipients:
      [NSArray arrayWithObject:@"tensecondapps@live.com"]];
    [mailComposer setSubject:@"Feedback for SeaVan"];
    [self presentModalViewController:mailComposer animated:YES];
}

Üblicherweise werden Routenbeschreibungen in iOS durch den Aufruf von Google Maps bereitgestellt. Die Kehrseite dieses Ansatzes ist, dass der Benutzer zu einer von Safari freigegebenen Anwendung (einer integrierten App) geleitet wird und es keine Möglichkeit gibt, die Steuerung mithilfe der Programmierung an die App zurückzugeben. Ich möchte die Stellen, an denen der Benutzer die App verlässt möglichst gering halten. Daher präsentiere ich anstelle einer Routenbeschreibung eine Karte der Zielgrenzübergänge. Dazu verwende ich ein benutzerdefiniertes SVMapViewController-Element, das ein Standard-MKMapView-Steuerelement hostet.

- (IBAction)mapClicked:(id)sender
{   
  SVBorderCrossing *crossing =
    [appDelegate.border.crossings
    objectAtIndex:parentController.pageControl.currentPage];
  CLLocationCoordinate2D target = crossing.coordinatesUs;
  SVMapViewController *mapView =
    [[SVMapViewController alloc]
    initWithCoordinate:target title:crossing.portName];
  [self presentModalViewController:mapView animated:YES];
}

Damit der Benutzer die Möglichkeit zur Eingabe einer Bewertung erhält, kann ich einen Link zur App im iTunes App Store erstellen. (Bei der neunstelligen ID im folgenden Code handelt es sich um die App Store-ID der App.) Das Ergebnis gebe ich dann an den Safaribrowser (eine freigegebene Anwendung) weiter. Ich habe hier keine andere Wahl, als die App zu verlassen:

- (IBAction)appStoreClicked:(id)sender
{
  NSString *appStoreURL =
    @"http://itunes.apple.com/us/app/id123456789?mt=8";
  [[UIApplication sharedApplication]
    openURL:[NSURL URLWithString:appStoreURL]];
}

Das Äquivalent der Schaltfläche „About“ bei Windows Phone ist eine Schaltfläche auf dem ApplicationBar-Objekt. Sobald der Benutzer die Schaltfläche antippt, rufe ich das NavigationService-Element für die Navigation zum AboutPage-Element auf:

private void appBarAbout_Click(object sender, EventArgs e)
{
  NavigationService.Navigate(new Uri("/AboutPage.xaml", 
    UriKind.Relative));
}

Genau wie in der iOS-Version zeigt das AboutPage-Element eine einfache Bedienungsanleitung mit einem Bildlauftext an. Eine Schaltfläche „Abbrechen“ gibt es nicht, da der Benutzer die hardwareseitige Zurück-Schaltfläche antippen kann, um von der Seite zurück zu navigieren. Anstelle der Schaltflächen „Support“ und „App Store“ stehen mit Hyperlink­Button-Steuerelemente zu Verfügung. Für die Support-Email kann ich das Verhalten deklarativ implementieren, indem ich einen NavigateUri zur Angabe des mailto:-Protokolls verwende. Das reicht aus, um den EmailComposeTask aufzurufen:

<HyperlinkButton 
  Content="tensecondapps@live.com" 
  Margin="-12,0,0,0" HorizontalAlignment="Left"
  NavigateUri="mailto:tensecondapps@live.com" 
  TargetName="_blank" />

Ich habe den Bewertungslink mit einem Klick-Handler im Code aufgesetzt und rufe das Startprogramm für „MarketplaceReviewTask“ auf:

private void ratingLink_Click(object sender, 
  RoutedEventArgs e)
{
  MarketplaceReviewTask reviewTask = 
    new MarketplaceReviewTask();
  reviewTask.Show();
}

Zurück auf der „MainPage“, implementiere ich das SelectionChanged-Ereignis lieber im Listenfeld, als eine separate Schaltfläche für das Karte/Routenbeschreibungs-Feature anzubieten, sodass der Benutzer zum Aufrufen des Features den Inhalt antippen kann. Dieser Ansatz stimmt mit dem bei Windows Store-Apps überein. Denn der Benutzer soll direkt mit dem Inhalt interagieren, anstatt dies indirekt über Chrome-Elemente zu tun. In diesem Handler starte ich ein Startprogramm für „BingMapsDirectionsTask“:

private void CrossingsList_SelectionChanged(
  object sender, SelectionChangedEventArgs e)
{
  BorderCrossing crossing = (BorderCrossing)CrossingsList.SelectedItem;
  BingMapsDirectionsTask directions = new BingMapsDirectionsTask();
  directions.End =
    new LabeledMapLocation(crossing.PortName, crossing.Coordinates);
  directions.Show();
}

Anwendungseinstellungen

Auf der iOS-Plattform werden App-Einstellungen durch die integrierte Einstellungsapp zentral verwaltet. Diese App stellt die Benutzeroberfläche bereit, damit Benutzer Einstellungen für integrierte Apps und Apps von Drittanbietern ändern können. Abbildung 9 zeigt die Hauptbenutzeroberfläche für Einstellungen und die spezielle „SeaVan“-Einstellungsansicht in iOS sowie die Windows Phone Einstellungsseite. Es gibt bei „SeaVan“ nur eine Einstellung: einen Schalter für das Autoaktualisierungs-Feature.

Standard Settings and SeaVan-Specific Settings on iOS and the Windows Phone Settings Page
Abbildung 9: Standardeinstellungen und spezifische „SeaVan“-Einstellungen bei iOS und der Windows Phone Einstellungsseite

Um Einstellungen in eine App einzubeziehen, verwende ich Xcode zur Erstellung eines speziellen Ressourcentyps, der als „Einstellungsbündel“ bekannt ist. Dann konfiguriere ich die Einstellungswerte mit dem Xcode-Einstellungseditor. So ist kein Code erforderlich.

Bei der in Abbildung 10 dargestellten Application-Methode stelle ich sicher, dass die Einstellungen synchron sind und beziehe die aktuellen Werte aus dem Store. Wenn die Einstellung der automatischen Aktualisierung den Wert „wahr“ angenommen hat, starte ich den Timer. Die APIs unterstützen das Abrufen und Einstellen der Werte innerhalb der App. So könnte ich zusätzlich zu der Ansicht der App in der App „Einstellungen“ optional auch eine Einstellungsansicht anbieten.

Abbildung 10: Die Application-Methode

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSUserDefaults *defaults =
    [NSUserDefaults standardUserDefaults];
  [defaults synchronize];
  boolean_t isAutoRefreshOn =
    [defaults boolForKey:@"autorefresh"];
  if (isAutoRefreshOn)
  {
    [timer invalidate];
    timer =
      [NSTimer scheduledTimerWithTimeInterval:kRefreshIntervalInSeconds
        target:self
        selector:@selector(onTimer)
        userInfo:nil
        repeats:YES];
  }
  // ... Code omitted for brevity
  return YES;
}

In Windows Phone kann ich die Appeinstellungen den globalen Einstellungsapp nicht hinzufügen. Stattdessen stelle ich meine eigene Einstellungsbenutzeroberfläche innerhalb der App bereit. In „SeaVan“ handelt es sich beim SettingsPage-Element, wie schon beim AboutPage-Element einfach um eine andere Seite. Ich stelle auf dem ApplicationBar-Objekt eine Schaltfläche für die Navigation zu dieser Seite bereit:

private void appBarSettings_Click(object sender, 
  EventArgs e)
{
  NavigationService.Navigate(new Uri("/SettingsPage.xaml", 
    UriKind.Relative));
}

In der Datei „SettingsPage.xaml“ definiere ich einen ToggleSwitch für das Autoaktualisierungsfeature:

<StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
  <toolkit:ToggleSwitch
    x:Name="autoRefreshSetting" Header="auto-refresh"
    IsChecked="{Binding Source={StaticResource appSettings},
    Path=AutoRefreshSetting, Mode=TwoWay}"/>
</StackPanel>

Ich muss innerhalb der App ein Einstellungsverhalten bereitstellen. Allerdings kann ich kann diesen Umstand zu meinem Vorteil nutzen und ein AppSettings-ViewModel dafür implementieren, das ich mittels Datenbindung an die Ansicht ankopple. Genau wie mit jedem anderen Datenmodell. In der MainPage-Klasse starte ich den Timer auf Grundlage der Einstellungswerte.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  if (App.AppSettings.AutoRefreshSetting)
  {
    timer.Tick += timer_Tick;
    timer.Start();
  }
}

Versionshinweise und Beispielapp

Plattformversionen:

  • Windows Phone SDK 7.1 und Silverlight für Windows Phone Toolkit
  • iOS 5 und Xcode 4

„SeaVan“ wird auf dem Windows Phone Marketplace und im iTunes App Store veröffentlicht.

Gar nicht so schwer

Eine App für iOS und Windows Phone zu erstellen ist gar nicht so schwer: Die Ähnlichkeiten sind größer als die Unterschiede. Beide Plattformen nutzen MVVM mit einem App-Objekt und einem oder mehreren Seiten/Ansicht-Objekten. Die Klassen der Benutzeroberfläche werden mit der XML (XAML bzw. XIB) verknüpft, die Sie mit einem grafischen Editor bearbeiten können. In iOS übermitteln Sie Nachrichten an ein Objekt und in Windows Phone rufen Sie die Methode eines Objekts auf. Der Unterschied ist hier jedoch nahezu akademisch, und Sie können in iOS sogar Punktnotation verwenden, wenn Ihnen die „[Nachrichten]“-Notation nicht gefällt. Beide Plattformen verfügen über Ereignis/Delegat-Mechanismen, Instanzen und statische Methoden, private und öffentliche Member sowie Eigenschaften mit „get“ und „set“-Zugriffsmethoden. Auf beiden Plattformen können Sie integrierte Appfunktionalität aufrufen und Benutzereinstellungen unterstützen. Offensichtlich ist die zu beachtende Codebasis beider Plattformen unterschiedlich. Doch Ihre Apparchitektur, das Hauptkomponentendesign und die Benutzerfreundlichkeit können über die Plattformen hinweg bewahrt werden. Versuchen Sie es. Sie werden angenehm überrascht sein!

Andrew Whitechapel ist seit über 20 Jahren als Entwickler tätig und arbeitet zurzeit als Programmmanager im Windows Phone-Team, wo er für wesentliche Bestandteile der Anwendungsplattform verantwortlich ist. Sein neues Buch ist „Windows Phone 7 Development Internals“ (Microsoft Press, 2012).

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Chung Webster und Jeff Wilcox