Документации по переносу приложений с 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 |
BorderCrossings | BorderCrossings |
Main UI | Основной UI |
Scrolling List | Прокручиваемый список |
Name | Name |
Car delay | Car delay |
Nexus delay | Nexus delay |
Truck delay | Truck delay |
Coordinates | Coordinates |
BorderCrossing | BorderCrossing |
ID | ID |
Name | Name |
Car delay | Car delay |
Nexus delay | Nexus delay |
Truck delay | Truck delay |
Coordinates | Coordinates |
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):
- анализаторы Simple API for XML (SAX) — ваш код уведомляется в процессе прохода анализатора по дереву XML;
- анализаторы Document Object Model (DOM) — считывают весь документ и формируют его представление в памяти, которое можно запрашивать для получения различных элементов.
По умолчанию 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;
}
Сведения по версиям и приложению-примеру
Версии платформ:
- Windows Phone SDK 7.1 и Silverlight for Windows Phone Toolkit;
- iOS 5 и Xcode 4.
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 можно сохранять унифицированными на обеих платформах. Попробуйте — и вы будете приятно удивлены!