Compartir a través de


Windows Phone

Creación de una aplicación para Windows Phone e iOS

Andrew Whitechapel

Descargar el ejemplo de código

Existe abundante información sobre cómo portar las aplicaciones de iOS a Windows Phone, pero en este artículo partiré del supuesto queremos crear una aplicación nueva a partir de cero, para ambas plataformas. No juzgaré cuál de las dos es mejor. Por el contrario, asumiré una postura práctica para crear la aplicación y describiré las diferencias y similitudes de ambas plataformas sobre la marcha.

Como miembro del equipo de Windows Phone, me apasiona la plataforma Windows Phone, pero lo importante aquí no es si una plataforma es superior a la otra, sino que son diferentes y que, por lo tanto, requieren de enfoques de programación diferentes. Aunque podemos desarrollar aplicaciones para iOS en C# con el sistema MonoTouch, este es un entorno minoritario. En este artículo usaré Xcode y Objective-C corriente para iOS, y Visual Studio con C# para Windows Phone.

Experiencia de usuario de destino

Mi intención es conseguir la misma experiencia de usuario en ambas versiones de la aplicación y garantizar al mismo tiempo que cada versión siga siendo fiel al modelo y la filosofía de la plataforma de destino. Para ilustrar esta idea, pensemos por ejemplo en la interfaz de usuario principal, que en la versión para Windows Phone de la aplicación se implementa con un elemento ListBox de desplazamiento vertical, mientras en que la versión para iOS se implementa con un ScrollViewer horizontal. Evidentemente, estas diferencias no son más que software; es decir, podríamos crear una lista que se desplaza verticalmente en iOS o una lista que se desplaza horizontalmente en Windows Phone. Pero al forzar estas preferencias nos alejaríamos de las filosofías de diseño respectivas y quiero evitar estos actos “contra natura”.

La aplicación, SeaVan, muestra los cuatro pasos fronterizos entre Seattle en los Estados Unidos y Vancouver, Columbia Británica en Canadá, con los tiempos de espera en cada uno de los diferentes carriles para cruzar. La aplicación captura la información mediante HTTP de los sitios web gubernamentales de los EE. UU. y Canadá y actualiza los datos manualmente con un botón o en forma automática mediante un temporizador.

En la Ilustración 1 se presentan ambas implementaciones. Una diferencia que llama la atención es que la versión para Windows Phone reconoce el tema actual y usa el color de acento adecuado. La versión para iOS, en cambio, no conoce los conceptos de temas ni de un color de acento.

The Main UI Screen for the SeaVan App on an iPhone and a Windows Phone DeviceIlustración 1 Pantalla principal de la interfaz de usuario de la aplicación SeaVan en un dispositivo iPhone y uno Windows Phone

El dispositivo con Windows cuenta con un modelo de navegación estrictamente lineal basado en páginas. Toda la interfaz de usuario de importancia se presenta en forma de página y el usuario se desplaza hacia delante y atrás por una pila de páginas. Es posible obtener la misma navegación lineal en el iPhone, pero como el iPhone no se limita a este modelo, tenemos toda la libertad del mundo para emplear el modelo de pantalla que más nos guste. En la versión para iOS de SeaVan, las pantallas subordinadas, como About, son controladores de vista modales. Desde el punto de vista tecnológico, estos equivalen más o menos a las ventanas emergentes modales de Windows Phone.

En la Ilustración 2 se presenta un esquema de la interfaz de usuario generalizada, donde los elementos internos de la interfaz de usuario aparecen en blanco y los externos (iniciadores y selectores en Windows Phone, aplicaciones compartidas en iOS) en naranja. La interfaz de usuario de configuración (en verde claro) es una anomalía que describiré más adelante.

Generalized Application UIIlustración 2 Interfaz de usuario generalizada de la aplicación

Otra diferencia en la interfaz de usuario es que Windows Phone emplea ApplicationBar como un elemento estandarizado de la interfaz de usuario. En SeaVan, esta barra es el lugar donde el usuario puede encontrar los botones para invocar las características subordinadas de la aplicación (la página About y la página Settings) y para actualizar los datos manualmente. En iOS no existe ningún equivalente directo a ApplicationBar, así que en la versión para iOS de SeaVan, la experiencia de usuario equivalente se proporciona con una simple Toolbar.

La versión para iOS, por el contrario, tiene un PageControl: la barra negra que aparece en el extremo inferior de la pantalla, con cuatro indicadores de posición en forma de puntos. El usuario puede desplazarse horizontalmente por los cuatro pasos fronterizos, ya sea al deslizar el dedo rápidamente por el contenido mismo o al pulsar el PageControl. En la versión para Windows Phone de SeaVan, no existe ningún equivalente a PageControl. En su lugar, el usuario de Windows Phone SeaVan desliza rápidamente el dedo directamente por el contenido para desplazarse por los diferentes pasos fronterizos. Una consecuencia del uso de PageControl es que este se puede configurar fácilmente para acoplar todas las páginas y dejarlas completamente visibles. El ListBox de desplazamiento de Windows Phone no cuenta con esta funcionalidad, así que el usuario puede acabar con las vistas parciales de dos pasos fronterizos. ApplicationBar y PageControl son ejemplos donde no intenté uniformar la experiencia de usuario de las dos versiones más allá de lo posible, al ajustar únicamente el comportamiento estándar.

Decisiones arquitectónicas

Se recomienda el uso de la arquitectura Model-View-ViewModel (MVVM) en ambas plataformas. Una diferencia es que Visual Studio genera código que incluye una referencia al modelo de vista principal en el objeto de la aplicación. No así en Xcode, donde podemos conectar el modelo de vista en el lugar que queramos dentro de la aplicación. En ambas plataformas, conviene conectar el modelo de vista en el objeto de la aplicación.

Una diferencia más importante es el mecanismo por medio del cual los datos del modelo fluyen a través del modelo de vista hasta la vista. En Windows Phone, esto se logra mediante el enlace de datos, que nos permite especificar en XAML la forma en que se asocian los elementos de la interfaz de usuario con los datos del modelo de vista; el tiempo de ejecución se encarga de propagar los valores. En iOS, donde existen bibliotecas independientes que entregan un comportamiento similar (basado en el patrón Observador de clave-valor), no existe ningún equivalente al enlace de datos en las bibliotecas estándar de iOS. En su lugar, la aplicación debe propagar los valores de los datos manualmente entre el modelo de vista y la vista. En la Ilustración 3 se aprecia la arquitectura generalizada y los componentes de SeaVan, con los modelos de vista en rosado y las vistas en azul.

Generalized SeaVan ArchitectureIlustración 3 Arquitectura generalizada de SeaVan

Objective-C y C#

Evidentemente, una comparación minuciosa entre Objective-C y C# está fuera del alcance de un artículo breve como este, pero en la Ilustración 4 se entrega una concordancia aproximada de las construcciones claves.

Ilustración 4 Construcciones claves en Objective-C y sus equivalentes en C#

Objective-C Concepto Contraparte en C#
@interface Foo : Bar {} Declaración de clase, incluye herencia class Foo : Bar {}

@implementation Foo

@end

Implementación de clase class Foo : Bar {}
Foo* f = [[Foo alloc] init] Creación de instancia de clase e inicialización Foo f = new Foo();
-(void) hacerAlgo {} Declaración de método de instancia void hacerAlgo() {}
+(void) hacerOtraCosa {} Declaración de método de clase static void hacerOtraCosa() {}

[miObjeto hacerAlgo];

o

miObjeto.hacerAlgo;

Enviar un mensaje a (invocar un método en) un objeto miObjeto.hacerAlgo();
[self hacerAlgo] Enviar un mensaje a (invocar un método en) el objeto actual this.hacerAlgo();
-(id)init {} Inicializador (constructor) Foo() {}
-(id)initWithName:(NSString*)n price:(int)p {} Inicializador (constructor) con parámetros Foo(String n, int p) {}
@property NSString *nombre; Declaración de propiedad public String Nombre { get; set; }
@interface Foo : NSObject <UIAlertViewDelegate> Foo se deriva de NSObject e implementa el protocolo UIAlertViewDelegate (aproximadamente equivalente a una interfaz en C#) class Foo : IDiferente

Componentes centrales de la aplicación

Para comenzar con la aplicación SeaVan, creo un nuevo proyecto Aplicación de vista única en Xcode y Aplicación para Windows Phone en Visual Studio. Ambas herramientas generarán un proyecto con un conjunto de archivos iniciales que contienen clases que representan el objeto de la aplicación y la página o vista principal.

Como la convención en iOS manda el uso de prefijos de dos letras en los nombres de las clases, todas las clases personalizadas de SeaVan comienzan con “SV”. Las aplicaciones para iOS comienzan con el método main habitual de C, que crea un delegado de la aplicación. En SeaVan, esto es una instancia de la clase SVAppDelegate. El delegado de la aplicación equivale al objeto App en Windows Phone. Creé el proyecto en Xcode con la función de Recuento automático de referencias (ARC) activada. Esto agrega una declaración de ámbito @autoreleasepool alrededor de todo el código de main, como se aprecia en las siguientes líneas:

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

El sistema ahora contará automáticamente las referencias a los objetos que creo y los liberará automáticamente cuando el contador caiga a cero. @autoreleasepool se encarga a la perfección de los problemas usuales de administración de memoria de C/C++ y permite programar en forma más parecida a C#.

En la declaración de la interfaz para SVAppDelegate, especifico que sea un <UIApplicationDelegate>. Esto significa que responde a los mensajes corrientes de un delegado de aplicación, tales como application:didFinishLaunchingWith­Options. También declaro una propiedad SVContentController. En SeaVan, esto corresponde a la clase MainPage estándar de Windows Phone. La última propiedad es el puntero SVBorderCrossings: este es el modelo de vista principal que contendrá una colección de elementos SVBorderCrossing, donde cada uno representa un paso fronterizo:

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

Cuando se inicia main, se inicializa el delegado de la aplicación y el sistema le envía el mensaje de la aplicación con el selector didFinishLaunching­WithOptions. Compare esto con Windows Phone, donde el equivalente lógico serían los controladores de eventos Launching o Activated de la aplicación. Aquí cargo un archivo Xcode Interface Builder (XIB), llamado SVContent, y lo empleo para inicializar la ventana principal. En Windows Phone, el equivalente de un archivo XIB es el archivo XAML. De hecho, los archivos XIB son archivos XML, aunque normalmente los editamos en forma indirecta con el editor gráfico para XIB de Xcode: parecido al editor gráfico de XAML en Visual Studio. Mi clase SVContentController está asociada con el archivo SVContent.xib, igual que la clase MainPage para Phone que está asociada con el archivo MainPage.xaml.

Por último, creo una instancia del modelo de vista SVBorderCrossings e invoco su inicializador. En iOS, por lo general realizamos las operaciones alloc e init en una misma instrucción para evitar que los objetos queden sin inicializar:

- (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;
}

En Windows Phone, la contraparte a la operación de carga de XIB normalmente ocurre tras bambalinas. La compilación genera esta parte del código mediante los archivos XAML. Por ejemplo, si desoculta los archivos ocultos de la carpeta Obj, en MainPage.g.cs podrá verá el método InitializeComponent, que carga el XAML del objeto:

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

En su función de página principal, la clase SVContentController albergará un visor desplazable que, a su vez, albergará cuatro controladores de vista. Cada controlador de vista se rellenará finalmente con los datos de uno de los cuatro pasos fronterizos entre Seattle y Vancouver. Declaro la clase como <UIScrollViewDelegate> y defino tres propiedades: una UIScrollView, un UIPageControl y una matriz de controladores de vista NSMutableArray. La scrollView y el pageControl se declaran como propiedades IBOutlet, lo que me permite conectarlos con los artefactos de la interfaz de usuario en el editor de XIB. El equivalente en Windows Phone es cuando se declara un elemento en XAML con un x:Name, lo que genera el campo class. En iOS, también podemos conectar los elementos de la interfaz de usuario en XIB a las propiedades de IBAction en la clase, lo que nos permite enlazar los eventos de la interfaz de usuario. El equivalente en Silverlight es cuando agregamos, por decir, un controlador de Click en XAML para enlazar el evento y proporcionar el código auxiliar para el controlador del evento en la clase. Es interesante que mi SVContentController no se derive de ninguna clase de la interfaz de usuario. En su lugar, se deriva de la clase base NSObject. Funciona como un elemento de la interfaz de usuario en SeaVan, ya que implementa el protocolo <UIScrollViewDelegate>; es decir, responde a los mensajes de scrollView:

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

En la implementación de SVContentController, el primer método que se invoca es awakeFromNib (que se hereda de NSObject). Aquí, creo la matriz de objetos SVViewController y agrego la vista de cada página al scrollView:

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

Por último, cuando el usuario desliza rápidamente el dedo por la scrollView o pulsa el control de la página, recibo el mensaje scrollViewDidScroll. En este método, cambio el indicador PageControl cuando se ve más de la mitad de la página siguiente o anterior. Luego cargo la página visible, más las páginas a ambos lados (para evitar parpadeos cuando el usuario se desplaza). Lo último que me queda por hacer es invocar un método privado, updateViewFromData, que captura los datos del modelo de vista y los establece manualmente en todos los campos de la interfaz de usuario:

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

En Windows Phone, la funcionalidad equivalente se implementa en MainPage, en forma declarativa en el archivo XAML. Muestro los tiempos que demora el paso fronterizo mediante controles TextBlock dentro del DataTemplate de un elemento ListBox. El cuadro de lista automáticamente desplaza cada conjunto de datos para visualizarlos, así que Windows Phone SeaVan no contiene código personalizado para controlar los gestos de desplazamiento. No existe ninguna contraparte para el método updateViewFromData, ya que el enlace de datos se hace cargo de esta operación.

Captura y análisis de los datos de la web

Aparte de funcionar como un delegado de la aplicación, la clase SVAppDelegate declara los campos y propiedades que permiten capturar y analizar los datos de los pasos fronterizos de los sitios web de los EE. UU. y Canadá. Declaro dos campos NSURLConnection, para las conexiones HTTP con estos sitios web. También declaro dos campos del tipo NSMutableData: unos búferes que emplearé para anexar cada fragmento de datos, a medida que estos van llegando. Actualizo la clase para implementar el protocolo <NSXMLParserDelegate>, de modo que aparte de ser un delegado de aplicación corriente, también es un delegado del analizador XML. Cuando se reciban datos XML, primero se llamará esta clase para analizarlos. Como sé que me enfrento a dos conjuntos completamente diferentes de datos XML, cedo el trabajo inmediatamente a uno de los dos delegados secundarios del analizador. Para esto declaro los campos personalizados SVXMLParserUs y SVXMLParserCa. La clase también declara un temporizador para la característica de actualización automática. Para cada evento del temporizador, invoco el método refreshData, tal como se aprecia en la Ilustración 5.

Ilustración 5 Declaración de la interfaz para 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

El método refreshData asigna un búfer de datos mutable para cada conjunto de datos entrantes y establece las dos conexiones HTTP. Empleo una clase SVURLConnectionWithTag personalizada que se deriva de NSURLConnection, ya que el modelo del delegado del analizador de iOS tiene que activar ambas solicitudes desde el mismo objeto y todos los datos vuelven a este objeto. Así que necesito un método que me permita discernir entre los datos entrantes de los Estados Unidos y de Canadá. Para esto, simplemente agrego una etiqueta a cada conexión y almaceno ambas conexiones en caché en un NSMutableDictionary. Al inicializar cada conexión, especifico self como el delegado. Cada vez que se recibe un fragmento de datos, se invoca el método connectionDidReceiveData y lo implemento para anexar los datos al búfer que corresponde a esta etiqueta (ver Ilustración 6).

Ilustración 6 Configuración de las conexiones HTTP

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
}

También tengo que implementar connectionDidFinishLoading. Cuando se recibieron todos los datos (para cualquiera de las dos conexiones), establezco este delegado de aplicación como el primer analizador. El mensaje parse es una llamada bloqueante, así que cuando se devuelve, puedo invocar updateViewFromData en el controlador de contenido para actualizar la interfaz de usuario a partir de los datos analizados:

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

Por lo general, existen dos tipos de analizadores XML:

  • Una API sencilla para analizadores XML (SAX), donde el código recibe notificaciones a medida que el analizador recorre el árbol de XML.
  • Analizadores Document Object Model (DOM), que leen el documento completo y construyen una representación en memoria, que nos permite realizar consultas para buscar diferentes elementos.

El analizador NSXMLParser predeterminado de iOS es un analizador SAX. También se dispone de analizadores DOM de terceros en iOS, pero quería comparar las plataformas estándar sin recurrir a bibliotecas externas. El analizador estándar recorre cada elemento uno por uno y no tiene conocimiento del contexto en el que se enmarca el elemento actual dentro del documento XML completo. Por esta razón, el analizador primario en SeaVan controla los bloques más exteriores en los que está interesado y luego los entrega a un analizador delegado secundario para que manipule el siguiente bloque interno.

En el método del analizador delegado, realizo una prueba sencilla para distinguir entre el XML de los Estados Unidos y de Canadá, creo una instancia del analizador secundario correspondiente y lo establezco como el analizador actual a partir de ese punto. También establezco el analizador primario del analizador secundario en self, para que el analizador secundario pueda devolver el control del análisis al elemento primario, cuando llega al final del XML que es capaz de manipular (ver Ilustración 7).

Ilustración 7 Método del analizador delegado

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

En el código equivalente en Windows Phone, primero configuro una solicitud web para el sitio web de los EE. UU. y el de Canadá. Aquí empleo un WebClient, aunque frecuentemente resulta más adecuado una solicitud HttpWebRequest para lograr un rendimiento y capacidad de respuesta óptimos. Configuro un controlador para el evento OpenReadCompleted y luego abro la solicitud en forma asincrónica:

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
}

En el controlador del evento OpenReadCompleted, extraigo para cada solicitud los datos que se devolvieron en forma de objeto Stream y los entrego a un objeto secundario para analizar el XML. Como tengo dos solicitudes web independientes y dos controladores de evento OpenReadCompleted independientes, no hace falta etiquetar las solicitudes ni de realizar pruebas para identificar la solicitud a la que pertenecen los datos entrantes. Tampoco tengo que manipular cada fragmento de datos entrantes para construir el documento XML completo. En su lugar, puedo relajarme y esperar hasta que recibo todos los datos:

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

Para analizar el XML, a diferencia de iOS, Silverlight incluye un analizador DOM en forma estándar, que está representado por la clase XDocument. Así que en vez de una jerarquía de analizadores, puedo emplear XDocument directamente para realizar todo el trabajo de análisis:

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

Prestaciones de vistas y servicios

En Windows Phone, el objeto App es estático y está disponible para todos los otros componentes dentro de la aplicación. De la misma manera, en iOS está disponible un tipo delegado de UIApplication en la aplicación. Para facilitar las cosas, defino una macro que puedo usar en cualquier parte en la aplicación para obtener el delegado de la aplicación y convertir debidamente al tipo SVAppDelegate puntual:

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

Esto me permite, por ejemplo, invocar el método refreshData del delegado de la aplicación cuando el usuario pulsa el botón Refresh: un botón que pertenece al controlador de la vista:

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

Cuando el usuario pulsa el botón About, quiero presentar la pantalla About, tal como se observa en la Ilustración 8. En iOS, creo una instancia de un SVAboutViewController, que tiene un XIB asociado, con un elemento de texto desplazable para la guía del usuario, además de tres botones adicionales en una Barra de herramientas.

The SeaVan About Screen in iOS and Windows Phone
Ilustración 8 Pantalla About de SeaVan en iOS y en Windows Phone

Para mostrar este controlador de vista, creo una instancia de este y le envío al objeto actual (self) un mensaje presentModalViewController:

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

En la clase SVAboutViewController, implemento un botón Cancel para descartar este controlador de vista y para que el control se revierta al controlador de vista que lo invocó:

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

Ambas plataformas ofrecen una forma estandarizada para que las aplicaciones invoquen las funciones de las aplicaciones integradas, tales como correo electrónico, teléfono y SMS. La principal diferencia es si el control se devuelve a la aplicación una vez que se devuelve de la función integrada, lo que siempre ocurre en Windows Phone. En iOS, esto ocurre con algunas características pero no con otras.

En la clase SVAboutViewController, cuando el usuario pulsa el botón Support, quiero redactar un mensaje de correo electrónico que el usuario pueda enviar al equipo de desarrollo. MFMailComposeViewController (presentado nuevamente como vista modal) se presta muy bien para este fin. Este controlador de vista estándar también implementa un botón Cancel, que realiza exactamente lo mismo para descartarse a sí mismo y revertir el control a la vista que lo invocó:

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

La forma estandarizada de obtener instrucciones en un mapa en iOS es invocar Google Maps. El inconveniente de este método es que dirige al usuario a la aplicación compartida (integrada) Safari, y no hay forma de devolver el control a la aplicación desde el programa. Como quiero minimizar los puntos donde el usuario sale de la aplicación, en vez de instrucciones, presento un mapa del paso fronterizo de interés mediante un SVMapViewController personalizado que aloja un control MKMapView corriente:

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

Para permitir que el usuario escriba una reseña, puedo crear un vínculo que dirige a la aplicación en la Tienda de aplicaciones de iTunes. (El identificador de nueve dígitos en el siguiente código es el identificador de la Tienda de aplicaciones.) Luego paso esto al explorador Safari (una aplicación compartida). No me queda otra alternativa más que salir de la aplicación:

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

El equivalente en Windows Phone del botón About es un botón en la ApplicationBar. Cuando el usuario pulsa este botón, invoco NavigationService para desplazarme hasta la página About:

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

Igual que en la versión para iOS, la página About presenta una guía del usuario sencilla, en forma de texto desplazable. No hay ningún botón Cancel, ya que el usuario puede pulsar el botón Back en hardware para volver atrás desde esta página. En vez de los botones Support y App Store, tengo controles Hyperlink­Button. Para el correo electrónico de soporte, puedo implementar el comportamiento en forma declarativa mediante un NavigateUri que especifica el mailto: https:. Esto es suficiente para invocar EmailComposeTask:

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

Configuro el vínculo Review con un controlador de Clic en código y luego invoco el iniciador MarketplaceReviewTask:

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

Atrás, en MainPage, en vez de ofrecer un botón independiente para la característica Map/Directions, implemento el evento SelectionChanged en el ListBox para que el usuario pueda pulsar en el contenido para invocar esta característica. Este método está en armonía con las aplicaciones para la Tienda Windows, donde el usuario debe interactuar directamente con el contenido en vez de hacerlo en forma indirecta, a través de elementos de cromo. En este controlador, lanzo un iniciador de 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();
}

Configuración de la aplicación

En la plataforma iOS, las preferencias de las aplicaciones se administran en forma central mediante la aplicación integrada Settings, que proporciona una interfaz para que los usuarios editen la configuración tanto de las aplicaciones integradas como las de terceros. En la Ilustración 9 se muestra la interfaz de usuario principal de Settings y la vista específica de SeaVan en iOS, junto con la página de configuración en Windows Phone. SeaVan tiene una sola configuración: un alternador para la característica de actualización automática.

Standard Settings and SeaVan-Specific Settings on iOS and the Windows Phone Settings Page
Ilustración 9 Configuración corriente de SeaVan en la página de configuración para iOS y para Windows Phone

Para incorporar las configuraciones en una aplicación, uso Xcode para crear un tipo de recursos especial, conocido como agrupación de configuración. Luego configuro los valores de la configuración mediante el editor de configuración de Xcode; no hace falta codificar nada.

En el método de la aplicación, que aparece en la Ilustración 10, me aseguro de que las configuraciones estén sincronizadas y luego capturo el valor actual del almacén. Si el valor de actualización automática es Verdadero, entonces inicio el temporizador. Las API permiten obtener valores y establecerlos desde la aplicación, así que también podría proporcionar una vista de configuración dentro de la aplicación, además de la vista de la aplicación en la aplicación Settings.

Ilustración 10 Método application

- (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;
}

En Windows Phone no puedo agregar la configuración de la aplicación a la aplicación de la configuración global. En vez de esto, proporciono una interfaz de usuario propia para la configuración dentro de la aplicación. En SeaVan, igual que en la página About, la página SettingsPage es simplemente otra página más. Proporciono un botón en ApplicationBar para ir hasta esta página:

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

En SettingsPage.xaml, defino un elemento ToggleSwitch para la característica de actualización automática:

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

No me queda otra alternativa más que entregar el comportamiento de la configuración dentro de mi aplicación, pero puedo convertirlo en una ventaja e implementar un modelo de vista AppSettings para la aplicación y enlazarlo con la vista mediante enlace de datos, igual que en cualquier otro modelo de datos. En la clase MainPage, inicio el temporizador en función del valor de la configuración:

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

Notas de la versión y aplicación de ejemplo

Versiones de la plataforma:

  • Windows Phone SDK 7.1 y el kit de herramientas de Silverlight para Windows Phone
  • iOS 5 y Xcode 4

SeaVan se publicará en Windows Phone Marketplace y iTunes App Store.

No es tan difícil

Crear una aplicación dirigida tanto a iOS como Windows Phone no es tan difícil: son más las semejanzas que las diferencias. Ambas plataformas usan MVVM con un objeto de aplicación y uno o más objetos de página o vista, y las clases de la interfaz de usuario están asociadas a XML (XAML o XIB), que se edita en un editor gráfico. En iOS enviamos mensajes a un objeto, mientras que en Windows Phone se invoca un método en un objeto. Pero la diferencia es casi académica, e incluso podemos usar la notación con puntos en iOS si nos desagrada la notación al estilo “[mensaje]”. Ambas plataformas tienen mecanismos de eventos y delegados, métodos de instancia y estáticos, miembros públicos y privados, y propiedades con descriptores de acceso get y set. En ambas plataformas podemos invocar las funcionalidades de las aplicaciones integradas y ambas son compatibles con las configuraciones de usuarios. Evidentemente, debemos hacernos cargo de dos bases de código, pero la arquitectura de la aplicación, el diseño de los principales componentes y la experiencia de usuario se puede conservar entre ambas plataformas. ¡Inténtelo y se llevará una grata sorpresa!

Andrew Whitechapel tiene más de 20 años de experiencia como desarrollador. Actualmente trabaja como jefe de programas en el equipo de Windows Phone y es responsable de partes centrales en la plataforma de aplicaciones. Su nuevo libro se llama “Windows Phone 7 Development Internals” (Microsoft Press, 2012).

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Chung Webster y Jeff Wilcox