Newsstandアプリケーションの作り方

iOS5以降でNewsstandKit.frameworkを使ってNewsstandアプリケーションの開発が可能になりました。
しかし、あまりネット上に開発ノウハウがあまりないので備忘録としてまとめておきます。

# 1. Nessstandアプリとは？
iOSのNewsstandフォルダに格納されるiOSアプリケーションです。
通常のiOSアプリとの違いは「記事などのNewsstandコンテンツデータをバックグラウンドでダウンロードする特別なパーミッションを持つ」ぐらいで、開発方法や記事データの表示などはアプリケーションごとに異なります。
このtipsではNewsstandKit.frameworkを用いて記事データをダウンロードするという部分に絞ってまとめています。

# 2. NewsstandKit.frameworkの使い方
iOSでNewsstand対応アプリケーションを開発するには、「NewsstandKit.framework」を用います。
まずプロジェクトにNewsstandKit.frameworkをインポートしましょう。

![スクリーンショット 2013-05-26 1.39.12.png](https://qiita-image-store.s3.amazonaws.com/0/12230/6191ad20-f292-daaf-c4fc-9a21a9e7462c.png)

インポートが完了したらinfo.plistに以下を記述。

![スクリーンショット 2013-06-09 10.51.20.png](https://qiita-image-store.s3.amazonaws.com/0/12230/d414846f-3db9-7cd1-0ae6-92746a46b182.png)

Icon files (iOS 5)についてはお好みで設定してください。
通常はApplication presents content in Newsstand にYESを設定するだけでNewsstandアプリとして認識されます。
加えて、Required background modesにApp processes Newsstand Kit downloadsを設定することでアプリケーションがバックグランドでNewsstandコンテンツ(記事データ等)のダウンロード権限を得ることができます。
ここまでで一度実行してみて、以下のようにNewsstandフォルダにアプリケーションアイコンが格納されることを確認してください。(されない場合は各種設定を確認！）
![iOSシミュレータのスクリーンショット 2013.06.09 10.58.53.png](https://qiita-image-store.s3.amazonaws.com/0/12230/51c22d46-075b-5d24-7da8-c7d1a7e6795f.png)

# 3. AppDelegateの設定
Newsstandアプリはほとんどの場合、プッシュ通知をフックにコンテンツをバックグラウンドでダウンロードさせるパターンが多いかと思います。

そこで、AppDelegateに以下の３つの処理を記述する必要があります
1. `application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken`でプッシュ通知を受け取るためのデバイストークンの登録処理を記述
2. `application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions`にNewsstandアプリの開発に必要な処理を記述
3. `application didReceiveRemoteNotification:(NSDictionary *)userInfo`にNewsstand用のプッシュ通知を受け取った際の処理を記述

1についてはアプリケーションごとに用意したサーバのAPIに準拠するため省略します。

2. Newsstandアプリケーションの記事データをダウンロードするためのプッシュ通知は以下のような基本のフォーマットがあります。

```json:NewsstandPushNotification
{
    "aps": {
        "content-available": 1
    }
}
```
このNewsstand用のプッシュ通知は「１日に１回」という制約があり、プロダクション環境ではこれに準拠する必要がありますが、開発時に１日に１回などという制約があっては開発にとても時間がかかってしまいます。しかしAppDelegate.mに以下を記述することで開発時に限り、この制約を回避することができます。加えて、プッシュ通知をフックにNewsstandコンテンツをダウンロードする場合はRemoteNotificationTypeに`UIRemoteNotificationTypeNewsstandContentAvailability`を指定する必要があります。

```objc:AppDelegate.m
- (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」があります。主にこの３つのクラスを用いて書籍の管理・ダウンロードを行います。

Newsstandコンテンツをダウンロードする大まかな流れは
1. content-available: 1のプッシュ通知をフックに起動し、Newsstandコンテンツの配信サーバに問い合わせるAPIをコール
2. 最新のNewsstandコンテンツが存在する場合はNKLibraryに対象のNKIssueを生成して格納する
3. NKIssueからNKAssetDownloadを生成してダウンロードを開始する
となります。

NKIssueはNSString型の「名前」で管理され、NKLibrary内でユニークである必要があります。

以下はこれら３つのクラスを用いたNewsstandKitのラッパークラスを簡単に書いてみたものです。
※大まかなサンプルコードです。実際に実装する際はアプリケーションごとの特性に合わせて適宣変更する必要があります。
※IssueInfoクラスはAPIから得たコンテンツ情報を格納するデータクラスだと思ってください。

```objc:NKWrapper.h
//
//  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
```

```objc:NKWrapper.m
//
//  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`でコンテンツのダウンロードを開始します。

かなりざっくりな備忘録...