Cocoaはやっぱり!
インターネットにアクセスしよう
Web Kit #1 : コンテンツをダウンロードしてファイルに保存

今回のテーマ

Safariの正式リリースとともにWeb KitWeb FoundationというWebブラウザのエンジンのフレームワークも公開されました。これらは、Web版のApplication KitとFoundationに相当するものです。このフレームワークを使うことで、独自のWebブラウザの開発やインターネットへのアクセスが簡単に行えるようになります。そこで、これらのフレームワークを使い方を解説してきます。

今回は、若干地味ではありますが、基礎固めということで「 Web上のコンテンツをダウンロードしてファイルに保存する 」というテーマを扱います。

推奨環境 この解説は、以下の環境を前提に作成し、動作確認等を行っています。ご確認ください。

改版履歴

サンプルの概要

今回作成するサンプルは「 Webサーバ上にある画像をダウンロードしてファイルに保存し、ウィンドウにも表示する 」というものです。URLを入力して、Accessボタンをクリックするとダウンロードしてきて、NSImageViewに画像を表示するというものです。ダウンロードの経過表示もプログレスバーで行います。

NSURLDownloadの概要

ファイルをディスクにダウンロード保存するには、まさにそのために作られたといっても過言ではないクラスが存在します。NSURLDownloadがそれです。指定されたURLのコンテンツをダウンロードし、指定のファイルパスへファイルへ保存するのはもちろん、ダウンロード後のファイルのデコード ( MacBinary、BinHex等 ) も行わせることも出来ますし、BASIC認証の処理など、ひととおりの機能を持っています。

まずは、NSURLDownloadの基本的な動きをシーケンス図を使って把握しておきましょう。なお、以下の説明は、話を簡単にするためにHTTPプロトコルを使っているときのものになっています。

右側のMyClassが自分でコーディングするクラスです。このクラス内でNSURLDownloadのインスタンスを生成します。すると、ダウンロード処理が自動的に開始されます。生成時には、NSURLDownloadからの様々な通知を受けるインスタンス、つまりデリゲート ( delegate ) も指定するようになっています。今回は、MyClassをデリゲートに指定したときの図になっています。

NSURLDownloadはサーバと通信を行って、まず最初にHTTPのヘッダーを取得します。取得できたらその通知がデリゲートに届きます。HTTPのヘッダーには、取得しようとしているコンテンツの種類や最終更新日などの情報が入っています。ここで、コンテンツの種類であるMIMETypeを取得するなどの処理を行います。

その後、ファイルをハードディスク等のどこに保存するかのパスを聞いてきます。URL等からファイル名については暫定のもの ( title.gif等 ) を知らせてくれます。これに対して、保存先のパスを決めて返答します。その後、確定したファイルパスを教えてくれます。これは、すでに同名のファイルがあった場合などに、「 title-1.gif 」のように保存パスが変わることがあるためです。

ここまでで、ファイルのダウンロードの準備が整います。その後は、データを受信する度に通知が来ますので経過表示などを行います。ダウンロードが完了したら、完了通知が来ます。

インスタンス構成図

最初にインスタンスの構成図をみておきます。MyClassというのがコーディングする部分で、vUrlがURLを入力するNSTextField、vImageがダウンロードした画像を表示するNSImageView、vProgが経過表示を行うNSProgressIndicatorです。Accessボタンがクリックされたら、MyClassのaccess:メソッドが呼ばれるようにしています。

ダウンロードの開始

さて、コードを見ていきましょう。まずは、Accessボタンをクリックした時に起動されるaccess : メソッドです。

ダウンロード開始 : MyClass.m
- (IBAction) access : (id) sender { // (1) 文字列からURLを作成 NSURL *url = [ NSURL URLWithString : [ vUrl stringValue ] ]; // (2) サーバーへのリクエスト情報を作成 NSURLRequest *req = [ NSURLRequest requestWithURL : url ]; // (3) ダウンロード開始 NSURLDownload *dl = [ [ NSURLDownload alloc ] initWithRequest : req delegate : self ]; [ dl autorelease ]; }

(1) vUrlからURLの文字列をstringValueで取り出して、これをもとにURLを扱うクラスであるNSURLを作成しています。URLWithString : メソッドを使います。

(2) 次に、このNSURLを使ってNSURLRequestを作成します。このNSURLRequestというのは、「 サーバーに送信するリクエスト情報を扱うクラス 」です。リクエスト情報というのは、HTTPプロトコルならば、HTTPのコマンド ( GETやHEADなど ) やリクエストヘッダー ( CookieやUser-Agentなど ) のことです。リクエストの内容については自動的に決めてくれますので、NSURLを渡すだけでよいのです。細かなリクエストの指定を行いたい場合は、サブクラスのNSMutableURLRequestを使うことになります。

NSURLRequest : 指定URLのリクエストを生成して初期化する
書式 : + (id) requestWithURL : (NSURL *) url 入力 : url - リクエストを送るURL 出力 : RETURN - 生成されたインスタンス 詳細 : リクエストの内容は自動的に作成される。内容を変更したい場合は、NSMutableURLRequestを使用する。

(3) このリクエスト情報をNSURLDownloadに渡してダウンロードを行ってもらいます。initWithRequest : delegate : メソッドで、allocで生成したNSURLDownloadのインスタンスを初期化します。このメソッドは、インスタンスを初期化後、ダウンロード処理も開始します。2つ目のパラメータのdelegate : には、NSURLDownloadからの通知を受け取るインスタンスを指定します。ここでは、selfを指定していますので、自分自身が呼び出されます。

NSURLRequest : 指定リクエストについてダウンロードを開始する
書式 : - (id) initWithRequest : (NSURLRequest *) request    delegate : (id) delegate 入力 : request - ダウンロードするためのリクエスト情報    : delegate - ダウンロード処理中の通知先 出力 : RETURN - 初期化されたインスタンス
サーバからのレスポンスの取得

initWithRequest : delegate : メソッドでダウンロードの処理が開始されます。サーバに対してリクエストを送信すると、それに対してのレスポンスが返ってきます。そうすると、デリゲートに指定したインスタンスのdownload : didReceiveResponse : メソッドが呼び出されます。ここで、サーバからのレスポンスの内容を知ることが出来ます。具体的な内容は、ダウンロードしようとしているコンテンツの種類や容量などです。

レスポンス取得通知 : MyClass.m
- (void) download : (NSURLDownload *) dl didReceiveResponse : (NSURLResponse *) response { // (1) 画像以外はダウンロード中止 if ( ! [ [ response MIMEType ] hasPrefix : @"image/" ] ) [ dl cancel ]; // (2) サーバから取得したコンテンツ容量を取得 lExLength = [ response expectedContentLength ]; // (3) プログレスバー属性変更 if ( lExLength != NSURLResponseUnknownLength ) { [ vProg setMinValue : 0 ]; // 最小値 [ vProg setMaxValue : lExLength ]; // 最大値 [ vProg setIndeterminate : NO ]; // 伸びるバー } else [ vProg setIndeterminate : YES ]; // 伸びないバー // (4) ダウンロードしたサイズの初期化 lDlLength = 0; }

このデリゲートのメソッドの第1パラメータは、先程生成したNSURLDownloadのインスタンス、第2パラメータは、サーバからのレスポンスの情報を扱うNSURLResponseクラスのインスタンスです。

(1) このレスポンスからデータの種類であるMIMETypeを取得することが出来ます。MIMETypeメソッドを使用します。画像のMIMETypeは「 image/ 」で始まっていますのでhasPrefixメソッドでチェックして、違う場合はcancelメソッドでダウンロード中止します。

NSURLResponse : MIMETypeを取得する
書式 : - (NSString *) MIMEType 出力 : RETURN - MIMETypeの文字列 ( ex. "image/gif" )
NSURLDownload : ダウンロード処理を中止する
書式 : - (void) cancel 詳細 : ダウンロード途中のファイルがあった場合は削除する。

(2) レスポンスからはこれからダウンロードするコンテンツの容量を知ることも出来ます。ダウンロード中にプログレスバーを表示しますので、その準備としてここで取得しておきます。メソッドは、 expectedContentLength を使います。ただし、この情報は必ず取得できるとは限りません。サーバによってはこの情報を返してくれない場合もありますし、間違った情報を返すサーバもまれに存在します。情報が取れないときは、NSURLResponseUnknownLengthという値が返ってきます。この値はlExLengthというMyClassのインスタンス変数に記憶しておきます。ヘッダは後程。

NSURLResponse : コンテンツの容量を取得する
書式 : - (long long) expectedContentLength 出力 : RETURN - ヘッダーから得られるコンテンツの容量。ヘッダーから得られないときは NSURLResponseUnknownLength が返る。 詳細 : この容量は必ずしも信用できるとは限らない。

(3) コンテンツ容量が取得できた場合は、プログレスバーを通常のバーが伸びていくタイプの表示にして、最大値をコンテンツ容量と同じにしておきます。取得できなかった場合は伸びないタイプに変更します。

(4) さらに、ダウンロードのプログレスバーのために、ダウンロード済みのサイズを記憶するMyClassのインスタンス変数のlDlLengthも初期化しておきます。

MyClassのヘッダは、以下のようになっています。

MyClass.h
@interface MyClass : NSObject { NSString *sDstPath; // ダウンロード先のパス long long lExLength; // コンテンツの総容量 long long lDlLength; // ダウンロードした量 IBOutlet id vUrl; // URL入力 IBOutlet id vImage; // 画像表示 IBOutlet id vIProg; // プログレスバー } - (IBAction) access : (id) sender; @end
ファイルの保存先の決定

レスポンスの後は、ダウンロードするファイルの保存先 ( ファイルパス ) の決定を行います。NSURLDownload側で、URLなどからファイル名の部分はお勧めを決めてくれます。例えば、URLが「 http://cocoyapa.com/logo.gif 」の場合は「 logo.gif 」のようになります。そして、デリゲートのdownload : decideDestinationWithSuggestedFilename : が呼び出されます。第2パラメータにそのファイル名が入ってきます。

保存先のパスの問い合わせ : MyClass.m
- (void) download : (NSURLDownload *) dl decideDestinationWithSuggestedFilename : (NSString *) filename { // (1) ホームディレクトリの下のパスを作成 NSString *sPath = [ NSHomeDirectory() stringByAppendingPathComponent : filename ]; // (2) 保存先と上書き許可の指定を行う [ dl setDestination : sPath allowOverwrite : NO ]; }

(1) このサンプルではホームディレクトリの直下にお勧めのファイル名のまま保存するという仕様にしています。NSHomeDirectory()でホームディレクトリのパスを取得して、stringByAppendingPathComponent : メソッドでファイル名を後ろに連結します。

(2) このパスをNSURLDownloadのメソッドsetDestination : allowOverwrite : を使って知らせます。このメソッドでは、保存先のファイルパスと既にそのパスにファイルが存在していた場合の上書き許可についても知らせます。上書きを禁止した場合は、「 index.html 」の場合は「 index-1.html 」といった感じで存在しないファイル名に変更して保存を行ってくれます。

NSURLDownload : ファイルの保存先と上書き指定を行う
書式 : - (void) setDestination : (NSString *) path    allowOverwrite : (BOOL ) allowOverwrite 入力 : path - ファイルの保存パス    : allowOverwrite - 指定した保存パスにファイルがあった時に上書きしてよい(=YES)

さて、ファイル名を自動的に変更してくれるのはありがたいのですが、最終的に決定したパスを知らないと、後から読み込むことも出来ません。そのため、download : didCreateDestination : というデリゲートのメソッドが呼ばれてパスが知らされます。

保存先のパスの通知 : MyClass.m
- (void) download : (NSURLDownload *) dl didCreateDestination : (NSString *) asDstPath { // (1) 現在記憶しているパスを破棄 if ( sDstPath != NULL ) [ sDstPath release ]; // (2) 新しいパスを保存 sDstPath = [ asDstPath retain ]; }

(1) まず、現在記憶しているパスを破棄します。

(2) 第2パラメータが最終的に決定したパスです。それをMyClassのインスタンス変数のsDstPathに保存しています。retainして、自動破棄されないようにしておきます。

ダウンロード処理

ここからが実際のダウンロードとファイルへの保存処理です。NSURLDownload側で処理は行ってくれまして、データを受信する度に、デリゲートの download : didReceiveDataOfLength : メソッドが繰り返し呼ばれます。

データ受信の通知 : MyClass.m
- (void) download : (NSURLDownload *) dl didReceiveDataOfLength : (unsigned) len { // (1) プログレスバーのアニメーションスタート if ( lDlLength == 0 ) [ vProg startAnimation : self ]; // (2) ダウンロード量を計算 lDlLength += len; // (3) プログレスバーを更新 if ( lExLength != NSURLResponseUnknownLength ) [ vProg setDoubleValue : lExLength ]; }

(1) データ受信量が0のときは、プログレスバーのアニメーションを開始します。

(2) 第2パラメータに今回受信したデータの量が入ってきますので、合計を計算します。

(3) コンテンツの総容量が分かっている場合は、プログレスバーを伸ばしていきます。

そして、全てのデータの受信が完了したら、デリゲートの downloadDidFinish : メソッドが呼ばれます。

ダウンロード完了通知 : MyClass.m
- (void) downloadDidFinish : (NSURLDownload *) dl { // (1) ファイルを読み込む NSImage *img = [ [ [ NSImage alloc ] initWithContentsOfFile : sDstPath ] autorelease ]; // (2) 画像を画面に表示 [ vImage setImage : img ]; // (3) プログレスバーのアニメーションを停止 [ vProg stopAnimation : self ]; }
もし、ダウンロードに失敗したときは、デリゲートのdownload : didFailWithError :メソッドが呼ばれますので、エラーの表示や後片付けなどを行います。
ダウンロード失敗通知 : MyClass.m
- (void) download : (NSURLDownload *) dl didFailWithError : (NSError *) err { // (1) プログレスバーのアニメーションを停止 [ vProg stopAnimation : self ]; // (2) エラーを表示 NSString *sSave = [ vUrl stringValue ]; [ vUrl setStringValue : [ err localizedDescription ] ]; [ vUrl display ]; // (3) 2秒経過したら戻す [ NSThread sleepUntilDate : [ NSDate dateWithTimeIntervalSinceNow : 2 ] ]; [ vUrl setStringValue : sSave ]; }

(1) プログレスバーのアニメーションを止めます。

(2) 第2パラメータにはエラー情報がNSErrorのインスタンスとして渡ってきます。NSErrorはエラーに関する情報を扱うクラスです。ここでは、 localizedDescription メソッドを使ってエラーの内容をメッセージの文字列として取得して、vUrlに2秒間に表示しています。例えば、サーバが見つからない場合「 can't find host 」という文字列が返ってきます。

(3) 2秒間停止するには、NSThreadの sleepUntilDate: メソッドを使いました。このメソッドは、指定した日時まで停止することができます。現在から2秒後の日時をNSDateで取得するには、 dateWithTimeIntervalSinceNow : メソッドが便利です。

NSError : エラーの内容を文字列で取得
書式 : - ( NSString *) localizedDescription 出力 : RETURN - エラーの内容を表す文字列。
ファイルのデコード

NSURLDownloadには、ダウンロードしたファイルのデコード機能があります。現在は、MacBinary ( .bin )、BinHex ( .hqx )、gzip ( gzip ) をサポートしています。ファイルのダウンロードが完了したら、 download: shouldDecodeSourceDataOfMIMEType: というデリゲートのメソッドが呼ばれます。ファイルの種別がMIMETypeで渡ってきますので、その種別を見てデコードしてよい場合はYES、しなくてよい場合はNOを返します。

MyClass.m
- (BOOL) download : (NSURLDownload *) download shouldDecodeSourceDataOfMIMEType : (NSString *) aType { // (1) MacBinaryとBinHexのみデコード if ( [ aType isEqual : @"application/macbinary " ] ) return( YES ); if ( [ aType isEqual : @"application/binhex" ] ) return( YES ); // (2) gzipはデコードしない if ( [ aType isEqual : @"application/gzip" ] ) return( NO ); return( NO ); // それ以外もデコードしない }

(1) このメソッドの第2パラメータには、ダウンロードしてきたコンテンツのMIMETypeが入ってきます。MacBinaryとBinHexはデコードさせるためにYESを返しています。

(2) gzipの場合はNOを返してデコードをしないようにしています。また、それ以外のものもデコードしないようにしています。

NSURLDownloadについてもっと詳しく

ざっとサンプルを使ってNSURLDownloadの使い方を見てきましたので、その他の詳しい情報を解説していきます。

■ サポートプロトコル

NSURLDownloadでサポートしているプロトコルですが、「 http、https、ftp、file 」の4つです。これは、NSURLDownloadというよりも、Web FoundationやWeb Kit全体としてこの4つのプロトコルをサポートしているという感じです。

■ 保存したファイルの更新日時

NSURLDownloadでダウンロードして保存したファイルの更新日時ですが、HTTPヘッダーにコンテンツの更新日時である Last-Modified の情報が含まれている場合は、その日時をセットしてくれるようです。

■ クッキーや認証

Cookieの送受信や認証のかかっているページへのアクセスですが、デリゲート側にメソッドを実装すると細かな制御をすることも可能ですが、何もメソッドを書いていない場合でもNSURLDownloadのデフォルトの処理を行ってくれます。

Web Foundation自身がシステムワイドな情報として、Cookieや認証のための情報(ID、パスワード等)を管理してくれますので、例えば、一度Safariを使って、認証のかかっているサイトのIDとパスワードを入力していれば、Web Foundationを使っているアプリケーションからはその認証情報を参照することができるということです。デフォルトの動作では、送信してよいかの確認のダイアログが表示された後、許可した場合は、IDやパスワードが送信されます。認証の処理については別途解説するかもしれません。