iOS5以降でNewsstandKit.frameworkを使ってNewsstandアプリケーションの開発が可能になりました。
しかし、あまりネット上に開発ノウハウがあまりないので備忘録としてまとめておきます。
1. Nessstandアプリとは?
iOSのNewsstandフォルダに格納されるiOSアプリケーションです。
通常のiOSアプリとの違いは「記事などのNewsstandコンテンツデータをバックグラウンドでダウンロードする特別なパーミッションを持つ」ぐらいで、開発方法や記事データの表示などはアプリケーションごとに異なります。
このtipsではNewsstandKit.frameworkを用いて記事データをダウンロードするという部分に絞ってまとめています。
2. NewsstandKit.frameworkの使い方
iOSでNewsstand対応アプリケーションを開発するには、「NewsstandKit.framework」を用います。
まずプロジェクトにNewsstandKit.frameworkをインポートしましょう。
インポートが完了したらinfo.plistに以下を記述。
Icon files (iOS 5)についてはお好みで設定してください。
通常はApplication presents content in Newsstand にYESを設定するだけでNewsstandアプリとして認識されます。
加えて、Required background modesにApp processes Newsstand Kit downloadsを設定することでアプリケーションがバックグランドでNewsstandコンテンツ(記事データ等)のダウンロード権限を得ることができます。
ここまでで一度実行してみて、以下のようにNewsstandフォルダにアプリケーションアイコンが格納されることを確認してください。(されない場合は各種設定を確認!)
3. AppDelegateの設定
Newsstandアプリはほとんどの場合、プッシュ通知をフックにコンテンツをバックグラウンドでダウンロードさせるパターンが多いかと思います。
そこで、AppDelegateに以下の3つの処理を記述する必要があります
1. application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
でプッシュ通知を受け取るためのデバイストークンの登録処理を記述
2. application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
にNewsstandアプリの開発に必要な処理を記述
3. application didReceiveRemoteNotification:(NSDictionary *)userInfo
にNewsstand用のプッシュ通知を受け取った際の処理を記述
1についてはアプリケーションごとに用意したサーバのAPIに準拠するため省略します。
- Newsstandアプリケーションの記事データをダウンロードするためのプッシュ通知は以下のような基本のフォーマットがあります。
{
"aps": {
"content-available": 1
}
}
このNewsstand用のプッシュ通知は「1日に1回」という制約があり、プロダクション環境ではこれに準拠する必要がありますが、開発時に1日に1回などという制約があっては開発にとても時間がかかってしまいます。しかしAppDelegate.mに以下を記述することで開発時に限り、この制約を回避することができます。加えて、プッシュ通知をフックにNewsstandコンテンツをダウンロードする場合はRemoteNotificationTypeにUIRemoteNotificationTypeNewsstandContentAvailability
を指定する必要があります。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#ifdef DEBUG
//テストで一日に何度もプッシュを受け取りたい場合に設定する
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NKDontThrottleNewsstandContentNotification"];
[[NSUserDefaults standardUserDefaults] synchronize];
#endif
//Newsstandコンテンツのプッシュ通知を受け取れるように設定
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeNewsstandContentAvailability];
NSDictionary *userInfo = [launchOptions objectForKey:UIApplicationLaunchOptionNewsstandDownloadKey];
if (userInfo != nil && [[userInfo objectForKey:@"aps"] objectForKey:@"content-available"] != nil]) {
//プッシュ通知を受け取って起動した際にダウンロード処理を開始するコードを記述
}
....
return YES;
}
あとはapplication didReceiveRemoteNotification:(NSDictionary *)userInfo
にもダウンロード処理を開始するコードを記述すればOKです。
4. NKLibrary, NKIssue, NKAssetDownload
NewsstandKit.frameworkには1号, 1版を表す「NKIssue」、NKIssueを管理する「NKLibrary」、版のダウンロードを管理するための「NKAssetDownload」があります。主にこの3つのクラスを用いて書籍の管理・ダウンロードを行います。
Newsstandコンテンツをダウンロードする大まかな流れは
1. content-available: 1のプッシュ通知をフックに起動し、Newsstandコンテンツの配信サーバに問い合わせるAPIをコール
2. 最新のNewsstandコンテンツが存在する場合はNKLibraryに対象のNKIssueを生成して格納する
3. NKIssueからNKAssetDownloadを生成してダウンロードを開始する
となります。
NKIssueはNSString型の「名前」で管理され、NKLibrary内でユニークである必要があります。
以下はこれら3つのクラスを用いたNewsstandKitのラッパークラスを簡単に書いてみたものです。
※大まかなサンプルコードです。実際に実装する際はアプリケーションごとの特性に合わせて適宣変更する必要があります。
※IssueInfoクラスはAPIから得たコンテンツ情報を格納するデータクラスだと思ってください。
//
// NKWrapper.h
// Newsstand
#import <Foundation/Foundation.h>
#import <NewsstandKit/NewsstandKit.h>
#import "IssueInfo.h"
@protocol NKWrapperDelegate;
@interface NKWrapper : NSObject
@property (weak, nonatomic) id<NKWrapperDelegate> delegate;
- (BOOL)addIssue:(IssueInfo *)issueInfo;
- (NKIssue*)issueWithIssueName:(NSString*)name;
- (void)runDownload:(IssueInfo *)issueInfo;
- (void)resumeDownloads;
@end
@protocol NKWrapperDelegate <NSObject>
@optional
- (void)completeContentDownload:(NKIssue *)issue;
- (void)sendDownloadProgress:(long long)hadSent total:(long long)total issue:(NKIssue *)issue;
- (void)downloadFailed:(NSError *)error issue:(NKIssue *)issue;
@end
//
// NKWrapper.m
// Newsstand
#import "NKWrapper.h"
@interface NKWrapper () <NSURLConnectionDownloadDelegate>
@end
@implementation NKWrapper
@synthesize delegate = _delegate;
//NKIssueをNKLibraryに追加する
- (BOOL)addIssue:(IssueInfo *)issueInfo
{
NKLibrary *lib = [NKLibrary sharedLibrary];
if (![lib issueWithName:[issueInfo name]]) {
[lib addIssueWithName:[issueInfo name] date:[issueInfo date]];
return YES;
} else {
return NO;
}
}
//issueNameからNKIssueを取得
- (NKIssue*)issueWithIssueName:(NSString*)name
{
NKLibrary *lib = [NKLibrary sharedLibrary];
if ([lib issueWithName:name]) {
return [lib issueWithName:name];
} else {
return nil;
}
}
//ダウンロードを開始する
- (void)runDownload:(IssueInfo *)issueInfo
{
NKLibrary *lib = [NKLibrary sharedLibrary];
NKIssue *issue = [lib issueWithName:[issueInfo name]];
if (issue) {
if ([issue status] == NKIssueContentStatusNone) {
//NKAssetDownloadを設定してダウンロードを開始する
NKAssetDownload *asset = [issue addAssetWithRequest:[NSURLRequest requestWithURL:[issueInfo contentURL]]];
[asset downloadWithDelegate:self];
} else if ([issue status] == NKIssueContentStatusDownloading) {
//DL途中の場合は対象のダウンロードを再開させる
for (NKAssetDownload *download in [lib downloadingAssets]) {
if ([[[download issue] name] isEqualToString:[issueInfo name]]) {
[download downloadWithDelegate:self];
}
}
}
}
}
//ダウンロード途中で処理が中断されたコンテンツのダウンロードを再開する
- (void)resumeDownloads
{
for (NKAssetDownload *asset in [[NKLibrary sharedLibrary] downloadingAssets]) {
[asset downloadWithDelegate:self];
}
}
#pragma mark - NSURLConnection Delegate
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL
{
NKAssetDownload *asset = [connection newsstandAssetDownload];
NSString *fileName = [[asset issue] name];
NSURL *fileURL = [[[asset issue] contentURL] URLByAppendingPathComponent:fileName];
NSError *error = nil;
[[NSFileManager defaultManager] moveItemAtURL:destinationURL
toURL:fileURL
error:&error];
if (error != nil) {
NSLog(@"%@", @"ファイル移動エラー");
[[NKLibrary sharedLibrary] removeIssue:[asset issue]];
return;
}
//ダウンロード完了後の処理を記述(zipをダウンロードしたなら解凍処理など)
//完了した旨を伝える
if ([self.delegate respondsToSelector:@selector(completeContentDownload:)]) {
[self.delegate completeContentDownload:[asset issue]];
}
}
//ダウンロードの進捗状況を取得
- (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes
{
//進捗状況をデリゲートに伝える
if ([self.delegate respondsToSelector:@selector(sendDownloadProgress:total:issue:)]) {
NKAssetDownload *asset = [connection newsstandAssetDownload];
[self.delegate sendDownloadProgress:totalBytesWritten total:expectedTotalBytes issue:[asset issue]];
}
}
//ダウンロード失敗時
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
//エラー処理
if ([self.delegate respondsToSelector:@selector(downloadFailed:issue:)]) {
NKAssetDownload *asset = [connection newsstandAssetDownload];
[self.delegate downloadFailed:error issue:[asset issue]];
}
}
@end
- (BOOL)addIssue:(IssueInfo*)issueInfo
でNKLibraryにNKIssueを新規に追加します。
- (void)runDownload:(IssueInfo*)issueInfo
でコンテンツのダウンロードを開始します。
かなりざっくりな備忘録...