Windows Phone

Criando um aplicativo para o Windows Phone e para o iOS

Andrew Whitechapel

Baixar o código de exemplo

Há um grande volume de documentação sobre compatibilização de aplicativos do iOS com o Windows Phone. Porém, neste artigo, começarei da premissa de que você deseja criar um novo aplicativo do zero que possa ser executado nas duas plataformas. Aqui não farei julgamentos sobre qual plataforma é melhor. Em vez disso, usarei uma abordagem prática para criar o aplicativo e descrever as diferenças e semelhanças encontradas no caminho de ambas as plataformas.

Como membro da equipe do Windows Phone, sou um apaixonado por essa plataforma. Mas meu ponto principal aqui não é dizer que uma plataforma é melhor que a outra, mas que as plataformas são diferentes e exigem abordagens um pouco diferentes de programação. Embora seja possível desenvolver aplicativos iOS em C#, use o sistema MonoTouch, que é um ambiente minoritário. Neste artigo, uso Objective-C e Xcode para o iOS, e Visual Studio e C# para o Windows Phone.

UX alvo

Meu objetivo é atingir a mesma UX em ambas as versões do aplicativo, além de garantir que cada versão permaneça verdadeira para o modelo e a filosofia da plataforma de destino. Para ilustrar o que quero dizer, considere que a versão do aplicativo do Windows Phone implementa a interface de usuário principal com uma ListBox de rolagem vertical, enquanto a versão do iOS implementa a mesma interface de usuário com um ScrollViewer horizontal. Obviamente, essas diferenças são apenas de software, isto é, eu poderia criar uma lista de rolagem verticalmente no iOS ou uma lista de rolagem horizontalmente no Windows Phone. Forçar essas preferências seria menos verdadeiro para as respectivas filosofias de design. No entanto, quero evitar essas "ações artificiais".

O aplicativo, SeaVan, exibe as quatro passagens de fronteira terrestre entre Seattle, nos Estados Unidos, e Vancouver, Colúmbia Britânica, no Canadá, com os tempos de espera em cada uma das diferentes vias de passagem. O aplicativo busca os dados via HTTP usando os sites do governo dos EUA e do Canadá e atualiza os dados manualmente por meio de um botão ou automaticamente por meio de um timer.

A Figura 1 apresenta as duas implementações. Uma diferença que poderá ser observada é que a versão do Windows Phone apresenta reconhecimento de tema e usa a cor de ênfase atual. Já a versão do iOS não tem um conceito de cor de ênfase ou tema.


Figura 1 A tela da interface de usuário principal do aplicativo SeaVan em um dispositivo iPhone e Windows Phone

O dispositivo Windows Phone tem um modelo de navegação baseado em página estritamente linear. Toda a interface de usuário significativa da tela é apresentada como uma página, e o usuário navega para frente e para trás por meio de uma pilha de páginas. É possível atingir a mesma navegação linear no iPhone, mas o iPhone não é limitado por esse modelo, de modo que você tem a liberdade de aplicar qualquer modelo de tela de sua preferência. Na versão do SeaVan no iOS, as telas auxiliares, como Sobre, são controladores de exibição modais. De uma perspectiva tecnológica, esses controladores, a grosso modo, são equivalentes aos pop-ups modais do Windows Phone.

A Figura 2 apresenta um esquema da interface de usuário generalizada, com elementos de interface internos em branco e elementos de interface externos (iniciadores/seletores no Windows Phone, aplicativos compartilhados no iOS) em laranja. A interface de usuário das configurações (em verde-claro) é uma anomalia que descreverei mais adiante neste artigo.


Figura 2 Interface de usuário generalizada do aplicativo

Outra diferença da interface de usuário é que o Windows Phone usa uma ApplicationBar como um elemento de interface de usuário padronizado. No SeaVan, nessa barra o usuário encontra botões para acessar recursos auxiliares no aplicativo (a página Sobre e a página Configurações) e para atualizar os dados manualmente. Não há equivalente direto para essa barra no iOS, de modo que na versão do SeaVan no iOS, uma Barra de Ferramentas simples fornece a UX equivalente.

Inversamente, a versão do iOS tem um PageControl - a barra preta na parte inferior da tela, com quatro indicadores de ponto posicionais. O usuário pode rolar horizontalmente por meio das quatro passagens de fronteira, passando o dedo no conteúdo em si ou tocando no PageControl. No SeaVan do Windows Phone, não há equivalente do PageControl. No SeaVan do Windows Phone, o usuário rola pelas passagens de fronteira passando o dedo diretamente no conteúdo. O benefício de usar o PageControl é que ele é fácil de configurar, assim, cada página é encaixada e fica completamente visível. Não há suporte padrão para a ListBox de rolagem do Windows Phone, portanto, o usuário pode acabar com exibições parciais de duas passagens de fronteira. Tanto a ApplicationBar quanto o PageControl são exemplos de onde não tentei tornar a UX nas duas versões mais uniforme do que ela pode ser apenas usando o comportamento padrão.

Decisões de arquitetura

O uso da arquitetura MVVM (Model-View-ViewModel) é encorajado em ambas as plataformas. Uma diferença é que o Visual Studio gera código que inclui uma referência ao viewmodel principal no objeto de aplicativo. O Xcode não faz isso. Você é livre para conectar seu viewmodel ao seu aplicativo onde quiser. Em ambas as plataformas, faz sentido conectar o viewmodel ao objeto de aplicativo.

Uma diferença mais significativa é o mecanismo pelo qual os dados do modelo fluem pelo viewmodel para a exibição. No Windows Phone, isso é feito por meio da vinculação de dados, que permite especificar em XAML como os elementos da interface de usuário são associados aos dados do viewmodel - e o tempo de execução cuida de propagar os valores de fato. No iOS, embora haja bibliotecas de terceiros que apresentem comportamento semelhante (com base no padrão Key-Value Observer), não há equivalente de vinculação de dados nas bibliotecas padrão do iOS. Em vez disso, o aplicativo deve propagar manualmente os valores dos dados entre o viewmodel e a exibição. A Figura 3 ilustra a arquitetura generalizada e os componentes do SeaVan, com viewmodels em rosa e exibições em azul.


Figura 3 Arquitetura generalizada do SeaVan

Objective-C e C#

Obviamente, uma comparação detalhada entre Objective-C e C# está além do escopo de um artigo breve, mas a Figura 4 fornece uma mapeamento aproximado das principais construções.

Figura 4 Principais construções em Objective-C e seus equivalentes em C#

Objective-C Conceito Equivalente na C#
@interface Foo : Bar {} Declaração de classe, incluindo herança class Foo : Bar {}

@implementation Foo

@end

Implementação da classe class Foo : Bar {}
Foo* f = [[Foo alloc] init] Instanciação e inicialização da classe Foo f = new Foo();
-(void) doSomething {} Declaração do método de instância void doSomething() {}
+(void) doOther {} Declaração do método de classe static void doOther() {}

[myObject doSomething];

ou

myObject.doSomething;

Enviar uma mensagem para (invocar um método em) um objeto myObject.doSomething();
[self doSomething] Enviar uma mensagem para (invocar um método no) objeto atual this.doSomething();
-(id)init {} Inicializador (construtor) Foo() {}
-(id)initWithName:(NSString*)n price:(int)p {} Inicializador (construtor) com parâmetros Foo(String n, int p) {}
@property NSString *name; Declaração de propriedade public String Name { get; set; }
@interface Foo : NSObject <UIAlertViewDelegate> Foo subdivide NSObject em classes e implementa o protocolo UIAlertViewDelegate (equivalente aproximado de uma interface da C#) class Foo : IAnother

Principais componentes do aplicativo

Para iniciar o aplicativo SeaVan, crio um novo aplicativo Single View em Xcode e um aplicativo Windows Phone no Visual Studio. Ambas as ferramentas vão gerar um projeto com um conjunto de arquivos iniciais, incluindo classes que representam o objeto de aplicativo e a exibição ou página principal.

A convenção do iOS é usar prefixos de duas letras em nomes de classe, de modo que todas as classes personalizadas do SeaVan sejam prefixadas com "SV". Um aplicativo do iOS começa com o método principal C usual, que cria um representante do aplicativo. No SeaVan, essa é uma instância da classe SVAppDelegate. O representante do aplicativo é equivalente ao objeto de aplicativo no Windows Phone. Criei um projeto no Xcode com a ARC (Contagem de Referência Automática) ativada. Isso adiciona uma declaração de escopo @autoreleasepool ao redor de todo o código em principal, conforme mostrado aqui:

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

O sistema agora faz uma contagem de referência automaticamente dos objetos que crio e lança-os automaticamente quando a contagem chega a zero. A declaração @autoreleasepool trata harmoniosamente a maioria dos problemas comuns de gerenciamento de memória do C/C++ e aproxima bem mais a experiência de codificação da C#.

Na declaração de interface para SVAppDelegate, especifico que ela será <UIApplicationDelegate>. Isso significa que ela responde às mensagens do representante de aplicativo padrão, como application:didFinishLaunchingWith­Options. Também declaro uma propriedade SVContentController. No SeaVan, isso corresponde à classe MainPage padrão no Windows Phone. A última propriedade é um ponteiro SVBorderCrossings — esse é o meu viewmodel principal, que manterá uma coleção de itens SVBorderCrossing, cada um representando uma passagem de fronteira:

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

Quando o principal é iniciado, o aplicativo é inicializado e o sistema envia a ele a mensagem de aplicativo com o seletor didFinishLaunching­WithOptions. Compara isso com o Windows Phone, onde o equivalente lógico seria o aplicativo Lauching ou os manipuladores de eventos Activated. Aqui, carrego um arquivo XIB (Xcode Interface Builder) denominado SVContent e o uso para inicializar minha janela principal. O equivalente do Windows Phone para um arquivo XIB é um arquivo XAML. Os arquivos XIB são, na verdade, arquivos XML, embora normalmente você os edite indiretamente com o editor XIB gráfico do Xcode - semelhante ao editor XAML gráfico no Visual Studio. Minha classe SVContentController é associada ao arquivo SVContent.xib, da mesma maneira que a classe MainPage do Windows Phone é associada ao arquivo MainPage.xaml.

Por fim, instancio o viewmodel SVBorderCrossings e invoco seu inicializador. No iOS, geralmente, você usa alloc e init em uma instrução para evitar as possíveis armadilhas de usar objetos não inicializados:

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

No Windows Phone, o equivalente da operação de carregamento do XIB normalmente é feito para você em segundo plano. A compilação gera essa parte do código, usando seus arquivos XAML. Por exemplo, se você mostrar os arquivos ocultos na sua pasta Obj, no MainPage.g.cs, você verá o método InitializeComponent, que carrega a XAML do 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));
  }
}

A classe SVContentController como minha página principal hospedará um visualizador de rolagem que, por sua vez, hospedará quatro controladores de exibição. Cada controlador de exibição consequentemente será populado com dados de uma das quatro passagens de fronteira Seattle-Vancouver. Declaro a classe para ser <UIScrollViewDelegate> e defino três propriedades: uma UIScrollView, uma UIPageControl e uma NSMutableArray dos controladores de exibição. scrollView e pageControl são declaradas como propriedades IBOutlet, que me permite conectá-las aos artefatos da interface de usuário no editor XIB. O equivalente no Windows Phone é quando um elemento na XAML é declarado com um x:Name, gerando o campo da classe. No iOS, também é possível conectar os elementos da interface de usuário XIB às propriedades IBAction na sua classe, permitindo vincular os eventos da interface de usuário. O equivalente do Silverlight é quando você adiciona, vamos dizer, um manipulador Click na XAML para vincular o evento e fornecer código stub ao manipulador de eventos na classe. Curiosamente, minha SVContentController não subdivide nenhuma classe da interface de usuário. Em vez disso, ela subdivide a classe base NSObject. Ela funciona como um elemento da interface de usuário no SeaVan, pois implementa o protocolo <UIScrollViewDelegate>, isto é, responde às mensagens de scrollView:

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

Na implementação da SVContentController, o primeiro método a ser invocado é awakeFromNib (herdade de NSObject). Aqui, crio a matriz de objetos SVViewController e adiciono cada exibição de página ao 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 fim, quando o usuário passa o dedo em scrollView ou toca no controle de página, obtenho a mensagem scrollViewDidScroll. Nesse método, alterno o indicador PageControl quando mais da metade da página anterior ou da próxima página fica visível. Então carrego a página visível, mais a página em ambos os lados (para evitar flashes quando o usuário iniciar a rolagem). A minha última tarefa aqui é invocar um método privado, updateViewFromData, que busca os dados do viewmodel e os define manualmente em cada campo na interface de usuário:

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

No Windows Phone, a funcionalidade correspondente é implementada na MainPage, de forma declarada, na XAML. Exibo os horários da passagem de fronteira usando os controles TextBlock no DataTemplate de uma ListBox. A ListBox rola por cada conjunto de dados na exibição automaticamente, de modo que o SeaVan no Windows Phone não tem código personalizado para manipular gestos de rolagem. Não há equivalente para o método updateViewFromData, pois essa operação é tratada pela vinculação de dados.

Buscando e analisando dados da Web

Assim como funciona um representante de aplicativo, a classe SVAppDelegate declara campos e propriedades para oferecer suporte à busca e à análise de dados de passagens dos sites norte-americanos e canadenses. Declaro dois campos NSURLConnection, para as conexões HTTP dos dois sites. Também declaro dois campos NSMutableData — buffers que usarei para acrescentar cada parte de dados à medida que são recebidas. Atualizo a classe para implementar o protocolo <NSXMLParserDelegate>, de modo que além de ser um representante de aplicativo padrão, ela também é um representante do analisador XML. Quando os dados XML são recebidos, essa classe será chamada primeiro para analisá-los. Como sei que lidarei com dois conjuntos completamente diferentes de dados XML, passarei o trabalho imediatamente para um dos dois representantes do analisador filho. Declaro um par de campos SVXMLParserUs/­SVXMLParserCa personalizados para isso. A classe também declara um timer para o recurso de atualização automática. Para cada evento do timer, invocarei o método refreshData, conforme mostrado na Figura 5.

Figura 5 Declaração de interface 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

O método refreshData aloca um buffer de dados mutável para cada conjunto de dados de entrada e estabelece as duas conexões HTTP. Estou usando uma classe SVURLConnectionWithTag personalizada que subdivide NSURLConnection, pois o modelo do representante do analisador do iOS necessita iniciar ambas as solicitações do mesmo objeto, e todos os dados retornarão para esse objeto. Desse modo, preciso diferenciar os dados norte-americanos e canadenses recebidos. Para isso, simplesmente anexo uma marca a cada conexão e armazeno em cache ambas as conexões em um NSMutableDictionary. Quando inicializo cada conexão, especifico a própria como representante. Sempre que uma parte de dados é recebida, o método connectionDidReceiveData é invocado e o implemento para acrescentar os dados ao buffer dessa marca (veja a Figura 6).

Figura 6 Configurando as conexões 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
}

Também devo implementar connectionDidFinishLoading. Quando todos os dados são recebidos (para uma das duas conexões), defino esse objeto de representante de aplicativo como o primeiro analisador. A mensagem de análise é uma chamada de bloqueio, assim, quando ela é retornada, posso invocar updateViewFromData no meu controlador de conteúdo para atualizar a interface de usuário a partir dos dados analisados:

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

Em geral, há dois tipos de analisadores XML:

  • API simples para analisadores XML (SAX), onde seu código é notificado como o analisador que percorre a árvore XML
  • Analisadores DOM (Document Object Model), que leem o documento inteiro e criam uma representação na memória que você pode consultar para diferentes elementos

O NSXMLParser padrão no iOS é um analisador SAX. Os analisadores DOM de terceiros estão disponíveis para uso no iOS, mas quis comparar plataformas padrão sem recorrer a bibliotecas de terceiros. O analisador padrão trabalha em cada elemento por vez e não compreende onde o item atual se encaixa no documento XML geral. Por esse motivo, o analisador pai no SeaVan manipula os bloqueios mais externos com os quais pode lidar e os transfere para um analisador representante filho para manipular o próximo bloqueio interno.

No método de representante do analisador, faço um teste simples para diferenciar o XML norte-americano do XML canadense, instancio o analisador filho correspondente e defino esse filho para ser o analisador atual desse ponto em diante. Também defino o analisador pai do filho para o próprio, de modo que o filho possa retornar o controle de análise de volta para o pai quando chegar ao fim do XML que pode manipular (veja a Figura 7).

Figura 7 O método de representante do analisador

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

Para o código equivalente do Windows Phone, primeiro defino uma solicitação da Web para o site dos EUA e para o site do Canadá. Aqui, uso um WebClient, mesmo que um HttpWebRequest muitas vezes seja mais apropriado para excelente desempenho e capacidade de resposta. Configuro um manipulador para o evento OpenReadCompleted e abro a solicitação de modo assíncrono:

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
}

No manipulador de eventos OpenReadCompleted de cada solicitação, extraio os dados que foram retornados como um objeto Stream e transfiro-os a um objeto auxiliar para análise do XML. Como tenho duas solicitações da Web independentes e dois manipuladores de eventos OpenReadCompleted independentes, não preciso marcar as solicitações nem fazer nenhum teste para identificar à qual solicitação os dados de entrada específicos pertencem. Também não preciso manipular cada parte de dados de entrada para criar o documento XML geral. Em vez disso, posso sentar e esperar que todos os dados sejam recebidos:

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

Para analisar o XML, em oposição ao iOS, o Silverlight inclui um analisador DOM como padrão, representado pela classe XDocument. Assim, em vez de uma hierarquia de analisadores, posso usar o XDocument diretamente para fazer todo o trabalho de análise:

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

Oferecendo suporte aos serviços e exibições

No Windows Phone, o objeto de aplicativo é estático e disponibilizado para qualquer outro componente no aplicativo. Da mesma forma, no iOS, um tipo de representante de UIApplication está disponível no aplicativo. Para simplificar as coisas, defino uma macro que posso usar em qualquer lugar no aplicativo para obter o representante do aplicativo e converter apropriadamente para o tipo SVAppDelegate específico:

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

Isso me permite, por exemplo, invocar o método refreshData do representante do aplicativo quando o usuário toca no botão Atualizar - um botão que pertence ao meu controlador de exibição.

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

Quando o usuário toca no botão Sobre, quero mostrar uma tela Sobre, conforme mostrado na Figura 8. No iOS, instancio um SVAboutViewController, que tem um XIB associado, com um elemento de texto de rolagem para o guia do usuário, bem como três botões adicionais em uma barra de ferramentas.


Figura 8 A tela Sobre do SeaVan no iOS e no Windows Phone

Para mostrar esse controlador de exibição, eu o instancio e envio o objeto atual (o próprio) uma mensagem presentModalViewController:

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

Na classe SVAboutViewController, implemento um botão Cancelar para descartar esse controlador de exibição, fazendo a reversão do controle para o controlador de exibição de invocação:

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

Ambas as plataformas oferecem uma maneira padrão para um aplicativo invocar a funcionalidade em aplicativos internos, como email, telefone e SMS. A principal diferença é se o controle é retornado ao aplicativo depois que a funcionalidade interna é retornada, o que sempre acontece no Windows Phone. No iOS, isso acontece para alguns recursos, e não para outros.

No SVAboutViewController, quando o usuário toca no botão Suporte, quero compor um email para o usuário enviar à equipe de desenvolvimento. O MFMailComposeViewController (novamente apresentando como uma exibição modal) funciona bem para esse propósito. Esse controlador de exibição padrão também implementa um botão Cancelar, que faz exatamente o mesmo trabalho para descartar a si próprio e reverter o controle para sua exibição de invocação:

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

O modo padrão de obter direções de mapa no iOS é invocar o Google Maps. O lado negativo dessa abordagem é que ela retira o usuário do aplicativo compartilhado no Safari (aplicativo interno) e não há meios de controlar o retorno de modo programático para o aplicativo. Quero minimizar os locais onde o usuário deixa o aplicativo, assim, em vez de direções, apresento um mapa da passagem de fronteira de destino usando um SVMapViewController personalizado que hospeda um controle MKMapView padrão:

- (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 o usuário insira uma revisão, posso compor um link para o aplicativo no iTunes App Store. (A ID de nove dígitos no código a seguir é a ID da App Store do aplicativo.) Em seguida, passo isso ao navegador Safari (um aplicativo compartilhado). Não tenho opção aqui, a não ser sair do aplicativo:

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

O equivalente do botão Sobre no Windows Phone é um botão em ApplicationBar. Quando o usuário toca nesse botão, invoco o NavigationService para navegar para AboutPage:

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

Da mesma forma que na versão do iOS, AboutPage apresenta um guia de usuário simples no texto de rolagem. Não há botão Cancelar porque o usuário pode tocar no botão Voltar do hardware para navegar de volta dessa página. Em vez dos botões Suporte e Loja de Aplicativos, tenho os controles Hyperlink­Button. Para o email de suporte, posso implementar o comportamento de modo declarado usando um NavigateUri que especifica o protocolo mailto:. Isso é suficiente para invocar EmailComposeTask:

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

Configuro o link Revisão com um manipulador Click no código, em seguida, invoco o iniciador MarketplaceReviewTask:

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

De volta à MainPage, em vez de oferecer um botão separado para o recurso Mapa/Direções, implemento o evento SelectionChanged na ListBox para que o usuário possa tocar no conteúdo e invocar esse recurso. Essa abordagem está em conformidade com os aplicativos da Windows Store, na qual o usuário deve interagir diretamente com o conteúdo, e não indiretamente por meio de elementos cromados. Nesse manipulador, aciono um iniciador 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();
}

Configurações do aplicativo

Na plataforma iOS, as preferências do aplicativo são gerenciadas centralmente pelo aplicativo interno Configurações, que fornece uma interface para que os usuários editem as configurações de aplicativos internos e de terceiros. A Figura 9 mostra a interface de usuário principal de Configurações e a exibição das configurações específicas do SeaVan no iOS, bem como a página de configurações do Windows Phone. Existe apenas uma configuração para o SeaVan, um alternância para o recurso de atualização automática.


Figura 9 Configurações padrão e configurações específicas do SeaVan no iOS e a página de configurações do Windows Phone

Para incorporar as configurações em um aplicativo, uso o Xcode para criar um tipo especial de recurso conhecido como um pacote de configurações. Então defino os valores das configurações usando o editor de configurações do Xcode. Nenhum código é exigido.

No método de aplicativo, mostrado na Figura 10, verifico se as configurações estão em sincronia e busco o valor atual na loja. Se o valor de configuração da atualização automática for True, inicio o timer. As APIs oferecem suporte à obtenção e definição dos valores no aplicativo, de modo que posso fornecer, como opção, uma exibição das configurações no aplicativo, além da exibição do aplicativo no aplicativo Configurações.

Figura 10 O método de aplicativo

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

No Windows Phone, não posso adicionar as configurações de aplicativo ao aplicativo de configurações globais. Em vez disso, forneço minha própria interface de usuário de configurações no aplicativo. No SeaVan, assim como a AboutPage, a SettingsPage é simplesmente outra página. Forneço um botão na ApplicationBar para navegar para essa página:

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

No arquivo SettingsPage.xaml, defino um ToggleSwitch para o recurso de atualização 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>

Não tenho opção, a não ser fornecer comportamento de configurações no aplicativo, mas posso transformar isso em vantagem e implementar um viewmodel AppSettings e vinculá-lo à exibição pela vinculação de dados, assim como com qualquer outro modelo de dados. Na classe MainPage, inicio o timer com base no valor da configuração:

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

Notas de versão e aplicativos de exemplo

Versões da plataforma:

  • Windows Phone SDK 7.1 e Kit de Ferramentas do Silverlight para Windows Phone
  • iOS 5 e Xcode 4

O SeaVan será lançado no Windows Phone Marketplace e na iTunes App Store.

Não é tão difícil

Criar um aplicativo que possa ser executado no iOS e Windows Phone não é tão difícil: há mais semelhanças do que diferenças. Use o MVVM com um objeto de aplicativo e um ou mais objetos de página/exibição, e as classes de interface de usuário são associadas ao XML (XAML ou XIB), que você edita com um editor gráfico. No iOS, você envia uma mensagem a um objeto, enquanto no Windows Phone, você invoca um método em um objeto. Mas a diferença aqui é quase acadêmica e você pode até mesmo usar notação de ponto no iOS se não gostar da notação "[message]". Ambas as plataformas têm mecanismos de evento/representante, métodos de instância e estáticos, membros privados e públicos, bem como propriedades com acessadores get e set. Em ambas as plataformas, é possível invocar as configurações de suporte do usuário e a funcionalidade de aplicativo interna. Obviamente, você precisa manter duas bases de código - mas a arquitetura do seu aplicativo, o design do componente principal e a UX podem ser consistentes entre as plataformas. Tente. Você ficará agradavelmente surpreso!

Andrew Whitechapel atua como desenvolvedor há mais de 20 anos e atualmente trabalha como gerente de programa na equipe do Windows Phone, sendo responsável por aspectos principais da plataforma do aplicativo. Seu novo livro é o "Windows Phone 7 Development Internals" (Microsoft Press, 2012).

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Chung Webster e Jeff Wilcox