Создание приложения для Windows Phone и iOS

OSzone.net » Microsoft » Разработка приложений » Windows Phone » Создание приложения для Windows Phone и iOS
Автор: Эндрю Уайтчэпл
Иcточник: MSDN Magazine
Опубликована: 06.12.2012

Документации по переносу приложений с iOS на Windows Phone имеется в изобилии, но в этой статье я начну с допущения, что вы хотите создать новое приложение с нуля, ориентированное на обе эти платформы. Я не выношу никаких оценок насчет того, какая платформа лучше. Вместо этого я выбрал прагматичный подход к созданию приложения и попутно опишу различия и сходства этих платформ.

Как член группы Windows Phone я очень люблю платформу Windows Phone, но моя основная цель здесь не в том, чтобы показать превосходство одной платформы над другой, а обратить внимание на их различия и на то, что они требуют разных подходов к программированию. Хотя приложения iOS можно разрабатывать на C#, используя систему MonoTouch, эта среда не пользуется особой популярностью. В данной статье я использую для iOS стандартные Xcode и Objective-C, а для Windows Phone — Visual Studio и C#.

Достижение одинакового UI в обеих версиях

Моя цель — добиться одинакового UI в обеих версиях приложения, в то же время обеспечив приверженность каждой версии приложения модели и философии целевой платформы. Для иллюстрации сказанного замечу, что версия приложения для Windows Phone реализует основной UI с помощью вертикально прокручиваемого ListBox, тогда как iOS-версия — с помощью горизонтального ScrollViewer. Очевидно, что эти различия не более чем программные, т. е. я мог бы создать вертикально прокручиваемый список в iOS или горизонтально прокручиваемый список в Windows Phone. Однако введение подобного варианта вызвало бы отклонение от соответствующих философий дизайна, а я хочу избежать таких «неестественных решений».

Приложение (SeaVan) отображает четыре пограничных пункта между Сиэтлом в США и Ванкувером в Канаде с временем ожидания на каждой линии проезда. Приложение получает данные по HTTP с правительственных веб-сайтов США и Канады и позволяет обновлять эти данные либо вручную, нажатием кнопки, либо автоматически по таймеру.

На рис. 1 представлены две реализации. Одно из отличий в том, что версия для Windows Phone поддерживает темы и использует текущий акцентный цвет (accent color). В противоположность этому в iOS-версии нет тем или концепции акцентного цвета.

*
Рис. 1. Экран основного UI для приложения SeaVan на iPhone и устройстве Windows Phone

Устройство Windows Phone имеет строго линейную постраничную модель навигации. Весь значимый экранный UI представляется в виде страницы, и пользователь перемещается вперед и назад по стеку страниц. Той же линейности навигации можно добиться и на iPhone, но iPhone не ограничен этой моделью, поэтому вы получаете свободу в выборе предпочтительной модели экрана. В iOS-версии SeaVan вспомогательные экраны, такие как About (О программе), являются контролерами модальных представлений. С технологической точки зрения, это примерно эквивалентно модальным всплывающим окнам (modal popups) Windows Phone.

На рис. 2 показано схематическое представление обобщенного UI с внутренними UI-элементами в белых блоках и внешними UI-элементами в светло-серых блоках (средства запуска/выбора в Windows Phone, общие приложения в iOS). UI настройки параметров (в темно-сером блоке) — аномалия, о которой я расскажу в этой статье позже.

*
Рис. 2. Обобщенный UI приложения

Main UIОсновной UI
Scrolling ListПрокручиваемый список
Map/DirectionsКарта/направления
SettingsНастройки
AboutО программе
Support E-mailПоддержка электронной почты
Review/RatingsОбзор/рейтинги

Другое отличие в UI — Windows Phone использует ApplicationBar в качестве стандартного UI-элемента. В SeaVan эта панель находится там же, где и кнопки для запуска вспомогательной функциональности приложения (открытия страниц About и Settings), а также для обновления данных вручную. В iOS нет прямого эквивалента ApplicationBar, поэтому в iOS-версии SeaVan аналогичные возможности обеспечиваются простым Toolbar.

В свою очередь, в iOS-версии есть PageControl — черная панель внизу экрана с четырьмя позиционными точечными индикаторами. Пользователь может горизонтальной прокруткой просматривать ситуацию на четырех пограничных пунктах, либо смахивая сам контент, либо постукивая по PageControl. В версии SeaVan для Windows Phone эквивалента PageControl нет. Вместо этого пользователь может прямо протаскивать контент пальцем. Одно из преимуществ PageControl — его легко настроить так, чтобы каждая страница стыковалась с границами экранами и была видимой полностью. В случае прокрутки ListBox в Windows Phone стандартной поддержки такой возможности нет, поэтому пользователь может увидеть частичные представления информации о двух пограничных пунктах. Как ApplicationBar, так и PageControl являются примерами, где я не пытался сделать UI в двух версиях более единообразным, чем это возможно при использовании стандартного поведения.

Архитектурные решения

На обеих платформах приветствуется использование архитектуры Model-View-ViewModel (MVVM). Одно отличие состоит в том, что Visual Studio генерирует код, который включает ссылку на основной viewmodel в объекте application. Xcode этого не делает: вы можете подключить viewmodel в свое приложение где угодно. На обеих платформах имеет смысл подключать viewmodel к объекту application.

Более существенная разница заключается в механизме, посредством которого данные Model проходят через viewmodel в view. В Windows Phone это достигается связыванием с данными, что позволяет указывать в XAML, какие UI-элементы сопоставляются с данными viewmodel, — и исполняющая среда берет на себя весь процесс передачи данных. В iOS, хотя для нее существуют сторонние библиотеки, обеспечивающие аналогичное поведение (на основе шаблона Key-Value Observer), эквивалента связывания с данными в стандартных библиотеках нет. Вместо этого приложение должно самостоятельно пересылать данные между viewmodel и view. Рис. 3 иллюстрирует обобщенную архитектуру и компоненты SeaVan, причем объекты viewmodel выделены темно-серым цветом, а представления — светло-серым.

*
Рис. 3. Обобщенная архитектура SeaVan

Government Web SitesПравительственные веб-сайты
AppПриложение
XML Parser HelperВспомогательный компонент разбора XML
BorderCrossingsBorderCrossings
Main UIОсновной UI
Scrolling ListПрокручиваемый список
NameName
Car delayCar delay
Nexus delayNexus delay
Truck delayTruck delay
CoordinatesCoordinates
BorderCrossingBorderCrossing
IDID
NameName
Car delayCar delay
Nexus delayNexus delay
Truck delayTruck delay
CoordinatesCoordinates
Data UpdatesОбновления данных

Objective-C и C#

Детальное сравнение Objective-C и C# со всей очевидностью выходит далеко за рамки этой короткой статьи, на в табл. 1 дано примерное сопоставление основных конструкций.

Табл. 1. Основные конструкции в Objective-C и их эквиваленты в C#

Objective-CКонцепцияЭквивалент в C#
@interface Foo : Bar {}Объявление класса, включающее наследованиеclass Foo : Bar {}

@implementation Foo

@end

Реализация классаclass Foo : Bar {}
Foo* f = [[Foo alloc] init]Создание экземпляра класса и инициализацияFoo f = new Foo();
-(void) doSomething {}Объявление метода экземпляраvoid doSomething() {}
+(void) doOther {}Объявление метода классаstatic void doOther() {}

[myObject doSomething];

or

myObject.doSomething;

Отправка сообщения объекту (вызов его метода)myObject.doSomething();
[self doSomething]Отправка сообщения текущему объекту (вызов его метода)this.doSomething();
-(id)init {}Инициализатор (конструктор)Foo() {}
-(id)initWithName:(NSString*)n price:(int)p {}Инициализатор (конструктор) с параметрамиFoo(String n, int p) {}
@property NSString *name;Объявление свойстваpublic String Name { get; set; }
@interface Foo : NSObject <UIAlertViewDelegate>Foo создает подкласс NSObject и реализует протокол UIAlertViewDelegate (примерный эквивалент интерфейса в C#)class Foo : IAnother

Основные компоненты приложения

Чтобы запустить приложение SeaVan, я создаю новый проект Single View Application в Xcode и Windows Phone Application в Visual Studio. Обе среды сгенерируют проект с набором начальных файлов, в том числе с классами, представляющими объект application и основную страницу или представление.

По соглашению в iOS, в именах классов используются двухбуквенные префиксы, чтобы все пользовательские классы в SeaVan начинались с «SV». Приложение iOS начинается с обычного C-метода main, который создает делегат приложения. В SeaVan это экземпляр класса SVAppDelegate. Делегат приложения эквивалентен объекту App в Windows Phone. Я создал проект в Xcode с включенным Automatic Reference Counting (ARC) (автоматическим учетом ссылок). Это приводит к добавлению объявления области видимости @autoreleasepool вокруг всего кода в main, как показано ниже:

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

Теперь система будет автоматически вести учет ссылок на создаваемые мной объекты и освобождать их, когда счетчик будет обнуляться. Объявление @autoreleasepool берет на себя большую часть обычного в C/C++ управления памятью и выводит удобство в кодировании на уровень, довольно близкий к C#.

В объявлении интерфейса для SVAppDelegate я указываю, что он должен быть <UIApplicationDelegate>. Это означает, что он реагирует на стандартные сообщения делегата приложения, такие как application:didFinishLaunchingWithOptions. Я также объявляю свойство SVContentController. В SeaVan это соответствует стандартному классу MainPage в версии для Windows Phone. Последнее свойство — указатель SVBorderCrossings — это мой основной viewmodel, где будет храниться набор элементов SVBorderCrossing, каждый из которых представляет одно пересечение границы:

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

При запуске main делегат приложения инициализируется, и система посылает ему прикладное сообщение с селектором didFinishLaunchingWithOptions. Сравните это с Windows Phone, где логическим эквивалентом был бы обработчик событий Launching или Activated. Здесь я загружаю файл Xcode Interface Builder (XIB) с именем SVContent и использую его для инициализации своего основного окна. Эквивалент Windows Phone для XIB-файла — XAML-файл. XIB-файлы на самом деле являются XML-файлами, хотя обычно вы редактируете их не напрямую, а в графическом XIB-редакторе в Xcode — по аналогии с графическим XAML-редактором в Visual Studio. Мой класс SVContentController сопоставляется с файлом SVContent.xib так же, как класс MainPage в Windows Phone связывается с файлом MainPage.xaml.

Наконец, я создаю экземпляр SVBorderCrossings viewmodel и запускаю его инициализатор. В iOS вы обычно выполняете операции alloc и init в одном выражении, чтобы избежать потенциальных проблем с использованием неинициализированных объектов:

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

В Windows Phone эквивалент операции загрузки XIB, как правило, выполняется «за кулисами». Процесс сборки генерирует эту часть кода, используя ваши XAML-файлы. Например, если вы раскрываете свои скрытые файлы в собственной папке Obj, то в MainPage.g.cs увидите метод InitializeComponent, который загружает XAML объекта:

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

Класс SVContentController в качестве моей основной страницы выступает в роли хоста для scrollView, а тот в свою очередь — в роли хоста для четырех контроллеров представлений. Каждый контроллер представления в конечном счете будет заполняться данными по одному из четырех пограничных пунктов между Сиэтлом и Ванкувером. Я объявил этот класс как <UIScrollViewDelegate> и определил три свойства: UIScrollView, UIPageControl и NSMutableArray-массив контроллеров представлений. Объекты scrollView и pageControl объявлены как свойства IBOutlet, что позволяет мне подключать их к UI-артефактам в редакторе XIB. Эквивалентный вариант в Windows Phone — некий элемент в XAML объявляется с x:Name, генерируя поле класса. В iOS также можно подключать свои XIB UI-элементы к свойствам IBAction в собственном классе, тем самым подключаясь к UI-событиям. Эквивалентный вариант в Silverlight можно увидеть, когда вы добавляете, скажем, обработчик Click в XAML, чтобы подключить событие и предоставить интерфейсный код (stub code) для обработчика события в классе. Интересно, что мой SVContentController не создает подкласс какого-либо UI-класса. Вместо этого он создает подкласс базового класса NSObject. Последний функционирует как UI-элемент в SeaVan, так как реализует протокол <UIScrollViewDelegate>, т. е. реагирует на сообщения scrollView:

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

В реализации SVContentController первый вызываемым методом должен быть awakeFromNib (наследуется от NSObject). Ниже я создаю массив объектов SVViewController и добавляю представление каждой страницы в 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];
  }
}

Наконец, когда пользователь проводит пальцем по scrollView или касается PageControl, я получаю сообщение scrollViewDidScroll. В методе, показанном ниже, я переключаю индикатор PageControl, когда видно больше половины предыдущей/следующей страницы. Затем я загружаю видимую страницу и страницу по каждую сторону от нее (чтобы избежать мигания при прокручивании). Последнее, что я делаю здесь, — вызываю закрытый метод updateViewFromData, который извлекает данные viewmodel и присваивает их каждому полю в UI:

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

В Windows Phone соответствующая функциональность реализуется в MainPage (декларативно в XAML). Я вывожу время пересечения границы, используя элементы управления TextBlock в DataTemplate для ListBox. ListBox прокручивает каждый набор данных в представлении автоматически, поэтому в версии SeaVan для Windows Phone нет пользовательского кода для обработки жестов прокрутки. Эквивалента метода updateViewFromData нет, потому что эта операция выполняется через механизм связывания с данными.

Получение и разбор веб-данных

В классе SVAppDelegate также объявляются поля и свойства для поддержки и разбора данных с правительственных сайтов США и Канады. Я объявил два поля NSURLConnection для HTTP-соединений с двумя веб-сайтами. Кроме того, я объявил два поля NSMutableData — буферы, используемые для добавления каждой порции данных по мере поступления. Я обновляю класс, реализующий протокол <NSXMLParserDelegate>, поэтому он выступает не только в роли стандартного делегата приложения, но и в качестве делегата синтаксического анализатора XML. При приеме XML-данных этот класс будет вызываться первым — для их разбора. Поскольку мне известно, что приложение будет иметь дело с двумя совершенно разными наборами XML-данных, я немедленно передаю работу одному из двух дочерних делегатов синтаксического анализа. С этой целью объявляется пара пользовательских полей SVXMLParserUs/SVXMLParserCa. В этом классе также объявляется таймер для автоматического обновления. При каждом событии таймера запускается метод refreshData (рис. 4).

Рис. 4. Объявление интерфейса для 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

Метод refreshData создает изменяемый буфер данных для каждого набора принимаемых данных и устанавливает два HTTP-соединения. Я использую собственный класс SVURLConnectionWithTag, который создает подклассы NSURLConnection, так как модель делегатов синтаксического анализа в iOS требует запуска обоих запросов из одного и того же объекта и все данные поступают обратно в этот объект. Поэтому мне нужен способ как-то различать данные от правительственных сайтов США и Канады. Для этого я просто подключаю тег к каждому соединению и кеширую оба соединения в NSMutableDictionary. При инициализации каждого соединения я указываю self в качестве делегата. Всякий раз, когда принимается порция данных, вызывается метод connectionDidReceiveData, и я реализую его для дозаписи данных в буфер для текущего тега (рис. 5).

Рис. 5. Установление 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]];
    // ...код для установления аналогичного соединенияс канадским сайтом опущен
}
- (void)connection:(SVURLConnectionWithTag *)connection
  didReceiveData:(NSData *)data
{
  [[urlConnectionsByTag objectForKey:connection.tag]appendData:data];
}

Я также должен реализовать connectionDidFinishLoading. Когда все данные приняты (по любому из двух соединений), я задаю этот объект делегата приложения как первый синтаксический анализатор. Сообщение разбора является блокирующим вызовом, поэтому после возврата управления я могу вызвать updateViewFromData в своем контроллере контента, чтобы обновить UI на основе разобранных данных:

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

В целом, существует два типа синтаксических анализаторов XML (XML parsers):

По умолчанию NSXMLParser в iOS является SAX-анализатором. В iOS можно использовать сторонние DOM-анализаторы, но я хотел сравнить стандартные платформы, не прибегая к сторонним библиотекам. Стандартный анализатор проходит по каждому элементу последовательно и не имеет никакого представления о том, где находится текущий элемент в общей иерархии XML-документа. По этой причине родительский анализатор в SeaVan обрабатывает самые внешние блоки, которые ему нужны, а затем передает дочернему анализатору обработку следующего вложенного блока.

В методе делегата анализатора я выполняю простую проверку, чтобы различать U.S. XML и Canadian XML, создаю экземпляр соответствующего дочернего анализатора и указываю его в качестве текущего. Я также присваиваю родительский анализатор дочернего переменной self, чтобы дочерний анализатор мог возвращать управление родительскому, достигнув конца XML, который он может обработать (рис. 6).

Рис. 6. Метод делегата анализатора

- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
  qualifiedName:(NSString *)qName
  attributes:(NSDictionary *)attributeDict
{
  if ([elementName isEqual:@"rss"]) // начало данных от США
  {
    xmlParserUs = [[SVXMLParserUs alloc] init];
    [xmlParserUs setParentParserDelegate:self];
    [parser setDelegate:xmlParserUs];
  }
  // начало данных от Канады
  else if ([elementName isEqual:@"border_wait_time"])
  {
    xmlParserCa = [[SVXMLParserCa alloc] init];
    [xmlParserCa setParentParserDelegate:self];
    [parser setDelegate:xmlParserCa];
  }
}

В эквивалентном коде для Windows Phone я сначала подготавливаю веб-запросы для правительственных веб-сайтов США и Канады. Здесь я использую WebClient, хотя HttpWebRequest зачастую дает более высокую производительность и скорость отклика. Я устанавливаю обработчик для события OpenReadCompleted, а затем асинхронно открываю запрос:

public static void RefreshData()
{
  WebClient webClientUsa = new WebClient();
  webClientUsa.OpenReadCompleted += webClientUs_OpenReadCompleted;
  webClientUsa.OpenReadAsync(new Uri(UrlUs));
  // ...код для установления аналогичного соединенияс канадским сайтом опущен
}

В обработчике события OpenReadCompleted для каждого запроса я извлекаю данные, возвращенные в виде объекта Stream, и передаю их вспомогательному объекту для разбора XML. Так как у меня имеются два независимых веб-запроса и два независимых обработчика событий OpenReadCompleted, мне не нужно помечать запросы или выполнять какие-либо проверки, чтобы определить, к какому запросу относятся какие-либо принимаемые данные. Кроме того, мне не нужно обрабатывать каждую входящую порцию данных, чтобы сформировать полный XML-документ. Вместо этого я могу откинуться на спинку кресла и дождаться, когда будут приняты все данные:

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

В противоположность iOS для разбора XML в Silverlight в качестве стандарта используется DOM-анализатор, представленный классом XDocument. Поэтому вместо иерархии анализаторов можно применять непосредственно XDocument для выполнения всей работы:

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

Поддержка представлений и сервисов

В Windows Phone объект App статический и доступен любым другим компонентам в приложении. В iOS аналогично один тип делегата UIApplication доступен всем частям приложения. Для упрощения я определю макрос, который можно использовать в любом месте приложения для получения делегата приложения и приводить его к специфическому типу SVAppDelegate:

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

Это позволяет мне, например, вызывать метод refreshData делегата приложения, когда пользователь касается кнопки Refresh, которая принадлежит моему контроллеру представления:

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

Когда пользователь касается кнопки About, я хочу выводить экран About (рис. 7). В iOS я создаю экземпляр SVAboutViewController, с которым сопоставлен XIB, элемент прокрутки текста и три дополнительные кнопки в Toolbar.*
Рис. 7. Экран About для SeaVan в iOS и Windows Phone

Для отображения этого контроллера представления я создаю его экземпляр и передаю текущему объекту (self) сообщение presentModalViewController:

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

В классе SVAboutViewController я реализую кнопку Cancel для освобождения этого контроллера представления, возвращая управление вызвавшему контроллеру представления:

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

Обе платформы предлагают стандартный способ вызова функциональности во встроенных приложениях, таких как электронная почта, средства телефона и SMS. Основная разница в том, возвращается ли управление приложению после возврата из встроенной функциональности. В Windows Phone это происходит всегда, а в iOS — когда как (зависит от конкретной функциональности).

В SVAboutViewController, когда пользователь касается кнопки Support, мне нужно сформировать сообщение электронной почты, которое отправляется группе разработчиков. Для этой цели отлично подходит MFMailComposeViewController — он вновь выводится как модальное представление. Этот стандартный контроллер представления также реализует кнопку Cancel, которая точно так же освобождает этот контроллер и возвращает управление вызвавшему контролеру представления:

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

Стандартный способ получения направлений по карте в iOS — запуск Google Maps. Недостаток этого подхода в том, что требует от пользователя выхода в общее (встроенное) приложение Safari и нет никакой возможности программным путем вернуть управление исходному приложению. Я хочу свести к минимуму места, где пользователь покидает приложение, поэтому вместо направлений я предоставляю карту целевого пункта пересечения границы, используя собственный SVMapViewController, в котором размещен стандартный элемент управления MKMapView:

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

Чтобы у пользователя была возможность оставить отзыв, я формирую ссылку на приложение в iTunes App Store. (Девятизначный идентификатор в следующем коде является идентификатором приложения в App Store.) Затем передаю ее браузеру Safari (общему приложению). Здесь у меня нет других вариантов, кроме выхода из приложения:

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

Эквивалент кнопки About в Windows Phone — кнопка на ApplicationBar. Когда пользователь касается этой кнопки, я вызываю NavigationService для перехода на страницу AboutPage:

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

Как и в iOS-версии, AboutPage предоставляет пользователю информацию в виде прокручиваемого текста. Кнопки Cancel здесь нет, так как для возврата на предыдущую страницу можно нажать аппаратную кнопку Back. Вместо кнопок Support и App Store я разместил элементы управления HyperlinkButton. Для поддержки электронной почты можно реализовать это поведение декларативно, используя NavigateUri, где указан протокол «mailto:». Для вызова EmailComposeTask достаточно:

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

Я подготавливаю ссылку Review с обработчиком Click в коде, а затем вызываю средство запуска MarketplaceReviewTask:

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

На странице MainPage — вместо создания отдельной кнопки для доступа к функционалу Map/Directions — я реализую событие SelectionChanged в ListBox, чтобы пользователь мог вызвать этот функционал, коснувшись нужного места в контенте. Такой подход согласуется с концепцией приложений Windows Store, где пользователь должен напрямую взаимодействовать с контентом, а не опосредованно через элементы. В этом обработчике я вызываю средство запуска 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();
}

Настройки приложения

На платформе iOS предпочтения в вашем приложении централизованно управляются встроенным приложением Settings, которое предоставляет UI для пользователей, чтобы они могли изменять параметры как встроенных, так и сторонних приложений. На рис. 8 показаны основной Settings UI и конкретное представление с параметрами SeaVan в iOS, а также страница параметров в Windows Phone. Для SeaVan предусмотрен всего один параметр: переключение обновления между ручным и автоматическим режимами.

*
Рис. 8. Стандартные параметры и специфические для SeaVan параметры в iOS и страница параметров в Windows Phone

Чтобы включить параметры настройки в приложение, я использую Xcode для создания ресурса специального типа, называемого пакетом параметров (settings bundle). Затем я настраиваю значения параметров с помощью Xcode-редактора параметров — никакого кода не требуется.

В методе application, показанном на рис. 9, я проверяю согласованность параметров и извлекаю текущее значение из хранилища. Если значение параметра автоматического обновления равно True, я запускаю таймер. API-средства поддерживают как получение, так и установку значений в приложении, поэтому я мог бы выводить представление настроек в приложении в дополнение к представлению в приложении Settings.

Рис. 9. Метод 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];
  }
  // ...остальной код опущен для краткости
  return YES;
}

Сведения по версиям и приложению-примеру

Версии платформ:

SeaVan будет опубликовано как в Windows Phone Marketplace, так и в iTunes App Store.

В Windows Phone я не могу добавить параметры своего приложения в приложение глобальных настроек. Вместо этого я предоставляю свой UI настройки в приложении. В SeaVan, как и в случае AboutPage, SettingsPage — это просто еще одна страница. Для перехода на эту страницу я создаю кнопку в ApplicationBar:

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

В SettingsPage.xaml я определяю ToggleSwitch для включения/выключения автоматического обновления:

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

У меня нет другого выбора, кроме предоставления параметров в самом приложении, но я могу обернуть эту в свою пользу и реализовать AppSettings viewmodel для настроек и подключить к представлению через механизм связывания с данными — так же, как в случае любой другой модели данных. В классе MainPage я запускаю таймер с учетом значения параметра:

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

Не так трудно, как кажется

Создание одного приложения, ориентированного как на iOS, так и на Windows Phone, не столь сложная задача, как может показаться поначалу: сходств больше, чем различий. Обе платформы используют MVVM с объектом приложения и одним или более объектов страницы/представления, а UI-классы сопоставляются с XML (XAML или XIB), которые редактируются в графическом редакторе. В iOS вы отправляете сообщение объекту, тогда как в Windows Phone вы вызываете метод объекта. Но эта разница почти академическая, и вы можете даже использовать в iOS нотацию с точками, если вам не нравится нотация [message]. Обе платформы поддерживают механизмы событий/делегатов, методы экземпляров и статические методы, закрытые и открытые члены, а также свойства с аксессорами get и set. На обеих платформах можно вызывать функциональность встроенных приложений и поддерживать настройки пользователя. Очевидно, что вам придется вести две кодовые базы, но архитектуру вашего приложения, дизайн основных компонентов и UI можно сохранять унифицированными на обеих платформах. Попробуйте — и вы будете приятно удивлены!


Ссылка: http://www.oszone.net/19376/Windows-Phone-iOS