Начну со вступления. Что же такое 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/
Добавить комментарий