Newsstand app. Создание iOS журнала

от автора

Последнее время я занимался разработкой iOS версии одного бумажного журнала. Собственно, это и есть попытка раскрыть сию тему.

Начну со вступления. Что же такое Newsstand? Откуда возникла такая сущность и во что она превратилась? Размышляя, пришел к следующему: это версия журнала, обернутая в iOS программу, отличается от pdf просто невменяемым размером. Одна из причин — огромная куча картинок. Однако, эта куча и создает глянец iOS журнала. Причиной же появления идеи Newsstand, я так понимаю, была позиция Apple, относительно прав собственности на некий контент. Т.е. была задача честно (часто платно) распространять периодику, да так, чтоб ее было трудно копипастить. Эти ребята с задачей справились — полагаю нету электронного издательства с большим денежным оборотом, чем Newsstand (если так можно сказать).

Ознакомившись, на уровне пользователя, с несколькими журналами, настало время искать техническое решение. Первым, всплыл Newsstand Kit.

Newsstand Kit состоит из следующих штуковин: особый механизм управления библиотекой (NKLibrary) отдельных выпусков журнала (NKIssue) и их загрузка (NKAssetDownload), включающая бэкграунд загрузку. Ну клево! Однако, на тот момент мне было совершенно неясно как превратить эти штуковины в журнал.

Все же. Очевидно, что программка-журнал состоит из двух частей: менеджмент выпусков журнала и отображение отдельного журнала. Т.е. Newsstand Kit ориентирован на первую составляющую — менеджмент. Вторую составляющую — отображение одного выпуска журнала — Apple никак не обременила какими-то шаблонами. Начну с нее, второй.

Теоретически, мы имеем всю необходимую информацию, скачанную с нашего сервера, и обязаны ее отобразить. Т.е. речь пошла о »Приложении-брошюре». Скачанную информацию можно, условно, разделить на сырцы (изображения, фильмы, тексты, html-ины, pdf-ы) и конфиг, по которому мы сможем все это собрать воедино. Предполагаю, что не всем будет интересно читать про »Брошюрку», потому отправляю таких далее — к части о менеджменте.

Как я упоминал выше, Apple мало ограничивает разработчиков в части отображения конкретного журнала. Однако, наверное, принято хорошим тоном, листание журнала свайпом. Для этого я использовал UIScrollView и идею бесконечного скролла (dev apple -> WWDC -> ScrollView Techniques -> Infinite scrolling). Если в двух словах, то на всю главную View нужно положить UIScrollView. У UIScrollView установить contentSize в три экрана по ширине, и положить на нее, последовательно три UIView (каждая, размером с один экран). Там еще пара настроек и мы имеем красивое листание по свайпу. На финише остается реализовать метод делегата у UIScrollView — scrollViewDidEndDecelerating: — и, по нему, обновлять внутренние UIView. Усложнение бесконечного скролла — если вложить в видимую UIVeiw еще один, аналогичный, UIScrollView — приводит к горизонтальному и вертикальному перелистыванию.

ОК. Остается вопрос: а где же брать нужные в данный момент UIView?
Начнем с »данного момента». Можно сказать, что его характеристикой является номер открытой страницы журнала. Выходит что нам, в приложении, нужна штуковина, которая будет хранить индекс текущей страницы. Такая штуковина нужна одна на все приложение, и к ней должен быть простой доступ — нам нужен синглтон. Имея индекс, вопрос сводится к новому: где взять UIView адекватный индексу? Наверное, конфиг его знает. Ну и далее: а почему бы не попросить наш синглтон прочитать конфиг и, зная индекс, выдать нужную вьюшку? Приведу интерфейс синглтона и перейдем к конфигу

@interface DataManager : NSObject  @property (nonatomic) int articleIndex; @property (nonatomic) int pageIndex; @property (nonatomic, strong) NSArray *articles;  + (id) sharedManager;  - (UIView *) currentView; - (UIView *) prevView; - (UIView *) nextView; - (UIView *) upperView; - (UIView *) lowerView;  - (int) lastArticleIndex; - (int) lastPageIndex;  @end 

Конфигом в Objective-C принято или удобно видеть .plist файл. Собственно, нужно собрать plist, который будет отображать страницы журнала, какие-то настройки этих страниц и линки на сырцы. Полагаю, на примерах будет понятнее

<plist version="1.0"> <array> 	<array> 		<dict> 			<key>class</key> 			<string>SimplePage</string> 			<key>properties</key> 			<dict> 				<key>baseImage</key> 				<string>1-0.png</string> 			</dict> 		</dict> 	</array> 	<array> 	<array> 	<array> 

		<dict> 		<dict> 			<key>class</key> 			<string>PhotoPage</string> 			<key>properties</key> 			<dict> 				<key>baseImage</key> 				<string>9-0-4.png</string> 				<key>photos</key> 				<array> 					<dict> 						<key>photo</key> 						<string>9-1-f1.png</string> 						<key>thumbnail</key> 						<string>9-1-f1m.png</string> 					</dict> 					<dict> 					<dict> 					<dict> 

Полагаю, что вы заметили первое свойство каждой страницы — class. Ага, как удобно! Написал в конфиге имя класса, и по нему создал нужную страничку. Фрагмент вышеупомянутого синглтона

- (UIView *) currentView {     NSArray *article = [self.articles objectAtIndex:articleIndex];     NSDictionary *page = [article objectAtIndex:pageIndex];     return [self viewWithDictionary:page]; }  - (UIView *) viewWithDictionary: (NSDictionary *) dictionary {     Class class = NSClassFromString([dictionary valueForKey:@"class"]);     NSDictionary *pageProperties = [dictionary valueForKey:@"properties"];     UIView *uiView = [[class alloc] initWithDictionary:pageProperties];     return uiView; } 

Что осталось упомянуть? Покажу интерфейс SimplePage. Остальные классы страниц, так или иначе, наследуют его

@interface SimplePage : UIView  @property (nonatomic, strong) NSString *imageDirectory;  - (id) initWithDictionary: (NSDictionary *) pageProperties;  @end 

Т.е. мы создаем набор классов, которые наследуют UIView, и согласно этим классам пишем конфиг одного журнала.

Вот еще всплыла переменная imageDirectory. Тут дело в том, что позднее нам нужно будет собрать картинки брошюры, положить к ним plist и отправить это добро жить на сервер. Посему, вместо добавления картинок в проект, я их собирал в папочке симулятора — DocumentsDirectory. А вот ее инициализация

#if IS_LOCAL     // doc/img dir     imageDirectory = [DocumentsDirectory stringByAppendingPathComponent:@"img"]; #else     // issue dir     NKIssue *nkCurrentlyReadingIssue = [[NKLibrary sharedLibrary] currentlyReadingIssue];     imageDirectory = [nkCurrentlyReadingIssue.contentURL path]; #endif      #if DEBUG     NSLog(@"imageDirectory %@", imageDirectory); #endif 

Вот и все, что я хотел рассказать про вариант реализации отображения одного журнала. Пойдем дальше?

NK Менеджмент

Само слово предполагает, что менеджерить нужно что-то. Коллекцию чего-то, скачанную с сервера. Так мы приходим к еще одной структуре. Чтоб ее понять, нужно поверхностно рассмотреть NKLibrary и NKIssue.

По простому, NKLibrary это коллекция для NKIssue-s. NKLibrary имеет ряд полезностей, т.е. это коллекция, заточенная под Newsstand. Еще это синглтон и, пожалуй, на данном этапе, все.

NKIssue хранит информацию про один журнал. Минимально, issue, обязана иметь имя и дату. Если я не ошибаюсь, имя будет ключом, а по дате будет сортировка.

Итак, что-то, где-то подсмотрев, мы имеем issues.plist

<plist version="1.0"> <array> 	<dict> 		<key>Name</key> 		<string>f-2</string> 		<key>Title</key> 		<string>лето/осень 2013</string> 		<key>Date</key> 		<date>2013-11-28T08:00:00Z</date> 		<key>Cover</key> 		<string>http://fo-nt.net/f/f2.png</string> 		<key>Content</key> 		<string>http://fo-nt.net/f/f2.zip</string> 	</dict> 	<dict> 	<dict> 

Качаем plist с сервера, перебираем его, создаем NKIssue и добавляем их в NKLibrary

NKLibrary *nkLib = [NKLibrary sharedLibrary]; issuesDictionary = [NSArray arrayWithContentsOfFile:issuesPlistFilePath]; for (NSDictionary *issueDictionary in issuesDictionary) { 	NSString *name = [issueDictionary valueForKey:@"Name"]; 	NKIssue *nkIssue = [nkLib issueWithName:name]; 	if(!nkIssue) { 		[nkLib addIssueWithName:name date:[issueDictionary objectForKey:@"Date"]]; 		 		NSString *coverPath = [issueDictionary valueForKey:@"Cover"]; 		if (IS_RETINA) coverPath = [self retinaURLStringForString:coverPath]; 		NSString *coverName = [coverPath lastPathComponent]; 

Поясню. Естественно, что тут мы уже импортировали NewsstandKit. Иначе, как мы могли знать про NKLibrary и NKIssue? С легкостью в строчку, получаем экземпляр NKLibrary — nkLib. Перебирая массив, мы просим у nkLib дать нам конкретный журнал, по его имени. Если библиотека скажет »фиг Вам» — станет понятно, что в конфиге журнал есть, а в библиотеке — нету -> нужно добавить.

На скриншоте, еще есть строчка ‘if (IS_RETINA)’. Кратенько, тут дело в том, что все картинки журнала находятся на сервере. Тут мы уже знаем, какой у нас дисплей. Ну и зачем же качать картинки для чужого дисплея. Забегая вперед, скажу, что комплект закачки, для каждого журнала, Apple рекомендует оформить в виде архива. На финише, логично, сделать два архива на один журнал: простой и @2x.

Есть у нас актуальная NKLibrary. Скромно, но уже можно отобразить UI имеющихся журналов.

Визуализация отображения библиотеки для каждого журнала своя. Однако, есть что-то устоявшееся. Есть NKIssue с набором свойств — отображаем их. Среди них, особенно интересно, свойство »статус», которое может быть: none, downloading и available. Это я веду к тому, что отображенный журнал можно загружать, ожидать окончания загрузки и читать, соответственно.

Загружать нужно комплект, который был подготовлен на этапе создания брошюры. NKAssetDownload это третья NK штуковина — специализированный загрузчик для Newsstand (наверное так). Процедура: с помощью NKIssue и NSURLRequest (полученного из NSURL, который, в свою очередь получен из строки URL для выбраного журнала), создаем экземпляр NKAssetDownload. У него нужно вызвать метод downloadWithDelegate:(id <NSURLConnectionDownloadDelegate>)delegate

NSURLRequest *req = [NSURLRequest requestWithURL:downloadURL]; NKAssetDownload *assetDownload = [nkIssue addAssetWithRequest:req]; [assetDownload downloadWithDelegate:self]; [assetDownload setUserInfo:[NSDictionary dictionaryWithObjectsAndKeys: 	[NSNumber numberWithInt:index], @"Index", nil]]; 

Фичей NKAssetDownload есть background downloading. Она обязательна для Newsstand. Это означает, что загрузка будет продолжена в фоновом режиме. Там еще есть вкусности. Однако bg загрузка нас интересует, поскольку накладывает обязанность, при возвращении, вызвать у всех экземпляров NKAssetDownload метод downloadWithDelegate:

for (NKAssetDownload *assetDownload in [nkIssue downloadingAssets]) { 	[assetDownload downloadWithDelegate:self]; } 

Тут, похоже, все. Реанимируем загрузку каким-нибудь прогресс баром и ожидаем ее окончания. По окончании загрузки, корректно, заменить иконку приложения: [[UIApplication sharedApplication] setNewsstandIconImage: [publisher coverImageForIssue:nkIssue]]. Если мы использовали архив, то разархивировать его. Ах, да, приложение, после загрузки журнала, обязано записать его файлы по пути свойства NKIssue contentURL (не уверен, обязано ли. Возможно, там что-то оптимизировано).

Далее, NKIssue принимает статус »доступна» — можно показывать »брошюру».

Следующий вопрос: обновление библиотеки — сводится к повторной загрузке issues.plist и его обработке имеющимся методом. Вспомните, там мы проверяли, был ли журнал добавлен в библиотеку иль нет. Правда, возникает предметная проблематика. Для ряда журналов, пользователь может забыть про него, к моменту выхода нового. И тут, как некстати, под рукой APNS. Это еще та тема. Признаюсь, с первого раза не осилил. Да и вообще, не я сделал эти уведомления в свой журнал. Ключевым камнем преткновения был сертификат, а точнее, надобность его конвертации.

Все-же, APNS: во первых, это сервис отправки уведомлений. Программка может получить id девайса, на котором она запущена и передать его на свой сервер. Сперва, нужно подписаться на эти уведомления

@implementation AppDelegate  - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {     // Register device for receiving push notifications     [[UIApplication sharedApplication] registerForRemoteNotificationTypes:      (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert)]; 

Опосля, UIApplicationDelegate Protocol, имеет метод application:didRegisterForRemoteNotificationsWithDeviceToken:, в котором имеем DeviceToken (id) и из которого можно слить инфу своему серверу. Рядышком есть метод application:didReceiveRemoteNotification:, в котором можно что-то делать по получению уведомления.

На стороне сервера, нужно принимать запросы (HTTP), которые несут DeviceToken-ы и складировать их в БД. Далее, согласно купленным билетам, а точнее, согласно тому, что на сервере развернуто, нужно искать либу отправки сообщений на APNS. Согласно ее API, прикрепив сертификат и пароль, соединяемся и посылаем сообщения, которые содержат, как полезную нагрузку, так и DeviceToken-ы. Там еще есть нюансы, но это отдельная тема.

Помимо прочего, на iOS журнал накладывается еще два ограничения. Перовое — Privacy Policy URL. С технической стороны, тут крайне просто. Второе ограничение — необходимость распространения контента, используя iTunesConnect внутренние покупки. Как я понял, это означает, что пользователь обязан купить мой free журнал за 0 денег.

Итого, iTunesConnect In-App Purchases означает использование Store Kit. В UserDefaults я завел свойство isFreeSubscribed. По тапу проверяю его, и в случае NO показываю alert. По согласию, подписываю

SKProductsRequest *productsRequest = [[SKProductsRequest alloc] 	initWithProductIdentifiers:[NSSet setWithObject:@"FreeSubscription"]]; productsRequest.delegate=self; [productsRequest start]; 

Плюс, надо реализовать методы делегата и установить isFreeSubscribed в YES, чтоб забыть про In-App Purchases. ProductId для SKProductsRequest нужно получать на сайте iTunesConnect, в разделе Manage In-App Purchases.

На финише повествования, полагаю, что тут нету и половины журнальных нужностей, однако, хочется верить, что мне удалось очертить самые необходимые технические вопросы для самого простого журнала.

ссылка на оригинал статьи http://habrahabr.ru/post/212067/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *