第11章
第11章
リッチテキストとアーカイブ
この章ではMyClipのText Viewをリッチテキストと呼ばれる属性付き文字列を保持できる形式に変更します。リッチテキストへの変更はInterface Builderで簡単にできますが、そのデータ形式を保存する方法は少し難しいです。この章はしっかりと押さえていきましょう。
11. 1. 1 Rich Text Format
Rich Text Format(リッチテキストフォーマット)はマイクロソフト社によって開発された文書用ファイルフォーマットです。現在ではMacのテキストエディットをはじめ多くのワープロソフトで採用されています。前章までのMyClipで採用していたプレーンテキスト形式と比べると文字ごとにフォント・色・サイズ・スタイルなどを設定することができます。また簡易的な文章のレイアウトなどもでき、拡張子は「.rtf」となります。
PDF(ポータブルドキュメントフォーマット)が閲覧と印刷をおもな目的にしているのに対してRTFは編集が可能なことが特徴になっていす。またMac OS XではRTFD(Rich Text Format Directory)という添付書類付きリッチテキストというフォーマットもあります。この章ではこのRTFDを採用してMyClipがリッチテキストと一緒に画像も保存できるようにしたいと思います。
●プレーンテキストとリッチテキストの違い
作業に入る前にプレーンテキストとリッチテキストの違いを少し説明したいと思います。
□プレーンテキストの場合
MyClipを起動してください。Text Viewに書かれている文字の中でボールドにしたいものを選択して右クリックします。しかしプレーンテキストの場合はBold(ボールド)という選択肢は現れません。
図Text View属性1
「Format」メニュー→「Font」→「Bold」を選んでも選択している文字だけではなくすべての文字がボールドになってしまいます。
図Text View属性2
□リッチテキストの場合
しかしリッチテキストの場合は選択した文字の上で右クリックするとコンテキストメニューの中に「Bold」という選択肢が見つかります。
図Text View属性3
そしてそのBoldをクリックすると選択していた文字だけがボールド(太字)になります。
図Text View属性4
11. 1. 2 Interface Builderでの設定
では作業をはじめます。いつものとおり前章前節までのプロジェクトのバックアップをとってフォルダ名を適宜変更してください。私の場合は「MyClip 9.3」としました。そして作業をするのは常に「MyClip」フォルダのプロジェクトで行うようにします。
●インスペクタパネルの設定
「MyClip」フォルダの「MyClip.xcodeproj」をダブルクリックしてプロジェクトウィンドウを開きます。そしてプロジェクトウィンドウのなかのMainMenu.xibファイルをダブルクリックするとInterface BuilderでMainMenu.xibファイルが開きます。MyClipウィンドウのText View(英文字部分)を選択してインスペクタパネルのAttributesタブを選びます。インスペクタパネルのタイトルが「Text View Attributes」になっていることを確認してください。そしてAllowsグループから「Rich Text」と「Graphics」にチェックを入れ、Usesグループから「Automatic Link Detection」にもチェックを入れます。
図Automatic Link Detection
●各項目の説明
Rich Text
文字どおりText Viewをリッチテキスト対応にします。
Graphics
リッチテキストを「添付書類付きリッチテキスト」RTFDにします。
例えば次のようにテキストビューに画像をドラッグ&ドロップしたときに
図Graphicsドロップ
Graphicsにチェックを入れていない場合は、テキストビューに画像のある場所のパス名と画像名が文字列として表示されます。
図Graphics未対応
Graphicsにチェックを入れている場合は、テキストビューに画像が表示されます。
図Graphics対応
Automatic Link
Detection
テキストビューにURLを入力すると青地にアンダーラインというリンクを表す文字列になります。そしてそのURLをクリックするとブラウザが自動的に起動してURLのサイトが表示されます。なお残念なことにメールアドレスの自動リンクには対応していないみたいです。また一般的なWebブラウザの場合とはことなりリンクの上にマウスを持っていってもカーソルの形が指型に変わるということもありません。
図Automatic Link Detection
先にMyClipの実行画面でインスペクタパネルの各設定項目の説明をすることになりましたが、Interface
Builderでの作業はインスペクタパネルの設定だけで完了しています。ファイルを保存してXcodeのプロジェクトウィンドウに戻りXcode 3.1では「ビルドして進行」を、Xcode 3.2では「ビルドと実行」をクリックしてください。そしてここまで実例をあげてきたようにボールドなどの属性を設定したり画像をドラッグ&ドロップしたりURLを書き込んだりして実際に試してみてください。
図MyClip実行画面
色々な属性や画像を持ったリッチテキストが表示されると思います。ファイルを保存してMyClipを1度終了させてみてください。そして再び起動してみます。しかし今度は、画像は表示されずに属性のないテキストのみが表示されます。
図MyClipの再起動後の実行画面
MyClipはまだ画像や属性を持った文字列を保存(書き込み)できる仕組みにはなっていないのです。したがって属性を持たない文字列のみを保存して再起動後にその属性のないテキストデータを読み込んでテキストビューに表示したのです。
次節の「アーカイブ」ではMyClipに画像や属性を持つ文字列を保存(書き込み)できる仕組みを作っていきたいと思います。
11. 2. 1 ファイル入出力の概要
第7章でも説明いたしましたがファイルからデータを読み込みすることやファイルへデータを書き込み (書き出し・保存)することを「ファイル入出力」と呼びます。
第7章の復習になりますが、Objective-Cには次の3つのファイル入出力方法が用意されています。
1. テキストデータをテキストのまま読み込み、テキストのまま書き込む。
2. テキストデータおよびそのほかのデータ形式をテキストデータとバイナリデータの両方が存在した形で読み込み、テキストデータとバイナリデータの両方が存在した形で書き込む。
この方法はプロパティリストと呼ばれています。
3. テキストデータおよびそのほかのデータ形式をバイナリデータとして読み込み、バイナリデータとして書き込む。
現在のMyClipは属性を持たない文字列だけを読み込んだり書き込んだりするしくみになっています。この属性を持たない文字列のことをプレーンテキスト(plain text)と呼びます。コンピュータが扱うデータは大きく分けるとこのプレーンテキストデータとバイナリデータの2つに分かれます。バイナリ(binary)とは2進数のことになります。例のオンとオフの2種類だけで表される010011100010101などのことです。
文字列の属性や画像などを保持しようとする場合にはこのバイナリデータで読み書きする必要があります。これらのバイナリデータもしくはバイナリファイルと呼ばれるものはバイトの並びであるバイト列として表されています。ひとつのオンとオフを表す単位をビット(bit)と呼びます。そしてそのビットを8個まとめた単位がバイト(byte)になります。Objective-Cではオブジェクトをこのバイト列に変換することをアーカイブと呼びます。そしてバイト列から元のオブジェクトへ復元することを非アーカイブ(アンアーカイブ)と呼びます。
なおアーカイブ(archive)という言葉は一般的には公文書、古文書、公文書保管所、文書保管所などの意味なります。またコンピュータ用語としても「データをまとめて保管する」もしくは「データを圧縮する」という意味になります。しかしObjective-Cでは「データをバイナリ化(バイト列化)する」という意味として使います。このデータをバイト列化することを一般的なコンピュータ用語としてはシリアライズと呼びます。この両者はたんに呼び方が違うだけで同じことを表しています。
また厳密に言うとプレーンテキストデータも当然のことながら本当はバイナリデータになります。しかしあえて人間の目に見える文字として読めるようにしているデータ形式のことをプレーンテキストもしくはテキストと呼んで区別することになっています。
また互換性の問題に対処するための理由などによりバイナリデータの一部をテキストデータとして残すファイル形式もあります。Objective-Cで言えば前述の2番目のプロパティリストがそれにあたります。しかし本書ではプロパティリストによるファイル入出力についての説明いたしません。ただしユーザデフォルトやのちの章で登場してくるアプリケーション全体の設定をする際にはプロパティリストを使います。したがってプロパティリストとはどういうものかはその時に分かっていただけると思います。また実践編ではプロパティリストにおいて良く使われるXMLという記述法についての説明もいたしております。
なおObjective-Cのプロパティリストは「バイナリデータの一部をテキストデータとして表す」というよりも「テキストデータの一部にバイナリデータを埋め込む」という形になっています。このことはたんにバイナリデータとテキストデータの比率の差だけだと考えていただいて結構です。
11. 2. 2 アーカイブの手順
●アーカイブの概要
さきほどは「オブジェクトをバイト列に変換する」ことをアーカイブだと説明しました。しかしオブジェクトにはインスタン変数とメソッドの部分が存在しています。そして実際にバイト列に変換する必要のあるのはほとんど場合インスタン変数だけになります。
アーカイブは単独のオブジェクトをデータ化することも、あるいはオブジェクトのまとまりを丸ごとデータ化することもできます。このオブジェクトのまとまりのことをオブジェクトグラフ(object graph)と呼びます。オブジェクトグラフをアーカイブする場合にはこのまとまりの起点となっているオブジェクトを指定してアーカイブすることになります。この起点となるオブジェクトのことをルートオブジェクトと呼びます。
●アーカイブの手順
アーカイブを行うには次の3つの手順を踏まなくてはなりません。
1.アーカイブするオブジェクトがNSCodingプロトコルの採用をしていること。
2.採用したNSCodingプロトコルメソッドの定義においてNSCoderオブジェクトを用いてインスタン変数のエンコード(バイナリデータ化)とデコード(データの復元)の機能を実装すること。
3.最後にArchiverを用いて指定したオブジェクトをバイト列(バイナリデータ)に変換し、Unarchiverを用いてバイト列(バイナリデータ)から、もとのオブジェクトに復元します。
以上がアーカイブを行うために必要な手順になります。次に各手順の詳細を説明します。
●手順1と手順2
アーカイブを必要とするオブジェクトのクラスがNSCodingというプロトコルと呼ばれるものに準拠していることを宣言します。この行いのことを「プロトコルの採用」と呼びます。そしてそのオブジェクトの持つそれぞれのインスタン変数をバイナリデータ化するメソッドとバイナリデータからインスタン変数へ復元するメソッドを定義 (実装)します。プロトコルはデリゲートやターゲットアクションと同じくObjective-Cのデザインパターンのうちのひとつです。なおプロトコルの詳細は補足資料Bで説明しています。そしてこのインスタン変数のバイナリデータ化とその復元にはNSCoderというオブジェクトを使って行います。
話が少し込み入ってきたと思います。そこでそれぞれの用語とその意味を表にまとめたいと思います。
表:アーカイブとエンコード 作表が必要であればお願いいたします。このままで良ければこのままで結構です。
・アーカイブ オブジェクトをバイナリデータ(バイト列)に変換する
・非アーカイブ バイナリデータ(バイト列)をオブジェクトに復元する
・エンコード 各種データをバイナリデータに変換する
・デコード バイナリデータを各種データに復元する
説明を繰り返します。
アーカイブとはオブジェクトをバイト列に変換することです。そこして実際にバイト列に変換されるのはオブジェクトの中のインスタン変数の中のデータです。このデータをバイナリデータに変換することをエンコードと呼びます。そしてその逆、つまりバイナリデータをインスタン変数の値に復元することをデコードと呼びます。またアーカイブされたバイト列をオブジェクトに復元することを非アーカイブと呼びます。
さらに理解を助けるためにプロトコルを採用したクラス(オブジェクト)の構造を図示します。
図NSCodingプロトコルとNSCoderオブジェクト 作図お願いいたします。
なおCocoaフレームワークの既存のオブジェクトの多くは、すでにNSCodingプロトコルを採用しています。このような場合には当然「プロトコルの採用」の手順は行う必要はありません。
●手順3
アーカイブを行いたいオブジェクトにNSCodingプロトコルメソッドを実装しただけでは当然アーカイブは行われません。また勝手にアーカイブが行われても困ります。アーカイブを行う場合にはNSArchiverというクラスのオブジェクトにアーカイブを行うメッセージを送ります。また非アーカイブを行う場合にはNSUnarchiverというクラスのオブジェクトに非アーカイブを行うメッセージを送ります。
しかしMac OS X 10.2 Jaguar以降ではそれぞれに対してNSKeyedArchiverとNSKeyedUnarchiverを使うことが推奨されています。このKeyedArchiverと普通のArchiverの違いはNSCoderでオブジェクトの各インスタン変数をコード化(エンコード)する時に、そのエンコードしたデータにキーという識別子を付けるか付けないかの違いになります。
インスタン変数が複数存在するオブジェクトを想像してみてください。キーを付けた場合はそのデータを復元(デコード)する時に、キーによって識別された正しいインスタン変数にデータが格納されていきます。復元する順番は関係ありません。また復元しないデータがあってもそのデータだけがインスタン変数に格納されないだけです。
しかしキーのないNSArchiverではアーカイブする時に各インスタン変数をソースコードどおりに上から順番にエンコードします。そして非アーカイブする時にはエンコードの順序どおりにデコードしていきます。途中で復元しないデータがあった場合にはインスタン変数と復元されるデータにズレが生じます。
なおこのキー付きアーカイバとキーなしアーカイバはともにNSCoderのサブクラスになっています。そしてNSCoderではキー付きアーカイバもキーなしアーカイバも使うことができるようになっています。しかしこの節ではキー付きアーカイバ(NSKeyedArchiver)とキー付き非アーカイバ(NSKeyedUnarchiver)だけを使用します。NSKeyedArchiverはNSArchiverのサブクラスになっています。したがってNSKeyedArchiverでキーのないアーカイブも行えることになります。
●アーカイブの実際
前項ではアーカイブを行うには
1.アーカイブされるオブジェクトに「NSCodingプロトコルの採用」がされていること
2.そしてそのオブジェクトにインスタン変数のバイナリデータ化と復元化を行うNSCoderによるメソッドが実装されていること
と説明いたしました。ではMyClipのようにアーカイブされるオブジェクトのインスタン変数がひとつだけで、そのインスタンスに格納されているデータがNSCodingプロトコルの採用をすでに行っているオブジェクトの場合はどうなるのでしょうか。答えは、そのアーカイブされるオブジェクトにはプロトコルの採用も、プロトコルメソッドの定義も、NSCoderによるメソッド実装も行わなくて良いということになります。
MyClipの場合にはアーカイブのコーディングをプログラマが行わなければならないのはModelクラスだけになります。このことについてはのちほど詳しく説明することして話を先に進めます。Modelクラスはインスタンス変数がひとつだけでそのインスタンス変数に格納されているのはNSCodingプロトコルをすでに採用しているオブジェクトです。したがって今回は「プロトコルの採用」と「プロトコルメソッドの定義と実装」は省略できることになります。なお省略せずにコーディングしても問題はありません。
これらの理由によりこの章では「プロトコルの採用」「プロトコルメソッドの定義」「NSCoderによるプロトコルメソッドの実装」についての説明は省略せていただき、NSKeyedArchiverとNSKeyedUnarchiverによるアーカイブと非アーカイブを行う方法だけを説明することにいたします。そしてこれらの「プロトコルの採用」以下のことは補足資料B「プロトコル」であらためて説明することにいたします。
この節ではInterface Builderでの作業はありません。ここではまずXcodeでコーディングを行い、それからコードの説明をしていきたいと思います。前節までのプロジェクトのバックアップをとって作業をはじめてください。
Mondel.h
#import <Cocoa/Cocoa.h>
@interface Model : NSObject {
NSTextStorage
*string;
}
- (void)setString:(NSTextStorage *)aString;
- (NSTextStorage *)string;
@end
●コード説明
NSTextStorage
*string;
stringインスタン変数のデータの型をNSStringからNSTextStorageに変更しています。テキストビューは属性のないプレーンテキストを設定・取得するメソッドと属性付きのテキストや画像などを設定・取得するメソッドの両方を備えています。プレーンテキストの場合はNSStringオブジェクトとしてテキストビューの値が表現されます。それに対して属性付きテキストや画像などのリッチテキストの見た目どおりのテキストビューの値を表現するためにNSTextStorageというオブジェクトが用意されています。この章は添付書類(画像)付きリッチテキストファイルで入出力を行うことを目的としています。したがってstringインスタン変数の型をこのNSTextStorage型に変更いたします。
- (void)setString:(NSTextStorage
*)aString;
- (NSTextStorage
*)string;
インスタン変数のデータ型の変更にともない、アクサセメソッドの引数と戻り値の型も変更します。
Model.m
#import "Model.h"
@implementation Model
- (void)setString:(NSTextStorage
*)aString
{
[string
release];
string
= [aString copy];
}
- (NSTextStorage *)string
{
return
string;
}
@end
●コード説明
- (void)setStringLNSTextStorage *)aString
引数の型をNSStringからNSTextStorageに変更しています。
string = [aString
copy];
以前はテキストビューからstringメソッドで取得した値をコピーしてからsetStringメソッドの引数に渡していました。今回はテキストビューからtextStorageメソッドで取得した値を直接引数に渡しています。そしてsetStringメソッドの中で引数aStringをコピーしてインスタン変数に代入しています。この両者はコピーを行う場所が違うだけで行っていることは同じです。しかし今回は後々の説明がしやすいようにこちらの方法を採用しました。
今の段階ではコピーがどこで行われたのかということは気にされなくても大丈夫です。ここで注意しなければいけないことはテキストビューからtextStorageメソッドで取得される値は一時オブジェクトであるということです。したがって取得した値をコピーして新しいオブジェクトとして確保しておかなければstringインスタン変数の値はいずれ消えてしまいます。
またこのことを防ぐためには string = [aString retain]; とすることもできそうですが、テキストビューの場合にはstringメソッドで値を取得した場合と同じく、textStorageメソッドで取得した値も、取得時のサスペンド(その時点での値を保持したものという意味)ではありません。stringおよびtextStorageメソッドで取得した値は、テキストビューの文字列や画像を変更すると過去に取得した値も変更されるという特殊なものになっています。
このためcopy メソッドでその時点の値を新しいオブジェクトとして確保するという方法がここでは一番有効な手段になります。
以上でModelクラスのコーディングは完了しました。なおXcodeの製品ドキュメントでNSStringおよびNSTextStorageのクラスリファレンスを見ていただいても、この両方が「NSCodingプロトコルの採用」をすでに行っていることが分かります。
図NSString Class Reference
図:NSTextStorage Class Reference
両方のクラスリファレンスの「Conforms to」というグループにNSCodingが明記されています。さきほどは「準拠」という言葉を使いましたがconformはコンピュータ用語として「適合」と訳されることが多いみたいです。またNSTextStorageの場合は「NSCoding
(NSAttributedString)」となっていますが、これは2つ上のスーパークラスであるNSAttributedStringクラスでNSCodingプロトコルの採用が行われていることを表しています。
11. 2. 3 NSKeyedArchiver
ではアーカイバによるアーカイブを行っていきます。最初はオブジェクトをバイト列へ変換するアーカイブ化のコーディングを行います。しかしその前に大事な説明があります。
●コーディングの対象
MyClipにおいてプログラマによってアーカイブとアンアーカイブのコーディングが必要となるのはModelオブジェクトだけです。MyClipのMVCのV(ビュー)にあたるMainMenu.xibファイルはアプリケーションによって起動時にアンアーカイブされて終了時にアーカイブされる決まりになっています。MainMenu.xibファイルに含まれているオブジェクトによっては必要になった時にアンアーカイブされて必要でなくなった時にアーカイブされます。そしてC(コントローラ)にあたるControllerオブジェクトもMainMenu.xibファイルに含まれていますのでこのオブジェクトのアンアーカイブとアーカイブもアプリケーションに任せることになります。したがってプログラマが責任をもってアーカイブとアンアーカイブを行わなければならないのはModelオブジェクトだけになります。ただしこれはMyClipの場合はそうだというだけですべてのアプリケーションにあてはまるとは限りません。
Controller.h
Controller.hで変更するコードはありません。
Controller.m
この節の「アーカイバ」に関連してController.mで変更されるメソッドは
init
awakeFromNib
textDidChange:
readFromFile
writeToFile:
の5つになります。そしてそのうちこの項ではNSKeyedArchiverに関係するinit、textDidChange:、writeToFile:の3つのメソッドのコーディングとコード説明を行いたいと思います。いつものとおり強調表示されているコードが追加・変更されるコードになります。
●コードリスト
/* initメソッド */
- (id)init
{
self = [super init];
if
(self) {
model = [[Model alloc] init];
filePath = [[NSMutableString alloc]
initWithCapacity:0];
[filePath appendString:NSHomeDirectory()];
[filePath
appendString:@"/Library/Application Support/MyClip"];
[[NSFileManager defaultManager] createDirectoryAtPath:filePath withIntermediateDirectories:YES
attributes:nil
error:NULL];
[filePath appendString:@"/MyClip.data"];
[self readFromFile];
}
return
self;
}
/* textDidChange:メソッド */
- (void)textDidChange:(NSNotification
*)aNotification
{
[model
setString:[textView textStorage]];
[window
setDocumentEdited:YES];
}
/*
writeToFile:メソッド */
- (IBAction)writeToFile:(id)sender
{
[NSKeyedArchiver
archiveRootObject:[model string] toFile:filePath];
[window
setDocumentEdited:NO];
}
●コード説明
initメソッド
[filePath appendString:@"/MyClip.data"];
読み込みおよび書き込み(書き出し、保存)するファイル名をMyClip.txtからMyClip.dataに変更しています。
textDidChange:
プレーンテキスト対応の時には次のような式になっていました。
[model setString:[[textView string] copy]];
しかし今回はリッチテキスト対応にするためにテキストビューから値を取得するメッセージをstringからtextStorageに変更しています。これにより得られる値は属性のある文字列と添付書類(画像)になります。
[model
setString:[textView textStorage]];
そして得られた値をModelオブジェクトのsetStringメッセージに渡しています。前章まではsetStringメッセージに渡す前に値をcopyして新しいオブジェクトとして渡していました。しかしこの章からはModelオブジェクトのsetStringメソッドのほうで引数として受け取った値のcopyを行うようにしています。今回copyを行う場所を変えたことによる影響や効果は何もありません。このことが関係してくるのはずっと後の「プロパティ」のところになってからです。しかしコードの流れもこちらのほうがキレイですし、全体的に統一もとれるのでこの章からこちらのコーディングを採用することにいたします。
writeToFile:
[NSKeyedArchiver
archiveRootObject:[model string] toFile:filePath];
NSKeyedArchiverはオブジェクトをバイト列へ変換するクラスですのでwriteToFile:メソッドに実装するのが妥当だということはご理解いただけると思います。archiveRootObject: toFile:メソッドは、メソッド名がarchive○○となっていてレシーバがNSKeyedArchiverになっていることからクラスメソッドであることがわかります。実際にクラスリファレンスを見てみましょう。
図NSKeyedArchiver Class Reference
意外とメソッド数が少ないので分かりやすいと思います。またメソッド名からもこのメソッドが行いたいことに一番近いメソッドであることも分かっていただけると思います。メソッド名をクリックして完全なシグネチャを確認しておきましょう。
+ (BOOL)archiveRootObject:(id)rootObject
toFile:(NSString
*)path
第1引数にはアーカイブしたいオブジェクトを指定します。アーカイブグラフの場合は起点となるオブジェクトを指定します。またアーカイブグラフでない単独のオブジェクトの場合でもこのメソッドを使います。オブジェクトには色々な種類がありますので引数の型もidになっています。第2引数にはアーカイブしたいファイルへのパスをNSString型の文字列で指定します。
11. 2. 4 NSKeyedUnarchiver
繰り返しなりますが、オブジェクトをバイト列に変換するものがアーカイバで、バイト列をオブジェクトに復元するものが非アーカイバ(アンアーカイバ)になります。先頭にKeyedが付いているのはキーによってどのインスタン変数に復元する値かを区別することができるアーカイバ(あるいはアンアーカイバ)であることを表しています。
この項でコーディングするController.mのメソッドはawakeFromNibとreadFromFileになります。アンアーカイブなのでreadFromFileに実装するのが妥当であるということはご理解いただけると思います。
●コードリスト
/* awakeFromNib メソッド
*/
-
(void)awakeFromNib
{
[textView setString:@""];
if ([model string]) {
[textView
insertText:[model string]];
[window setDocumentEdited:NO];
[textView
scrollRangeToVisible:NSMakeRange(0, 0)];
}
}
/* readFromFile メソッド */
-
(void)readFromFile
{
[model setString:[NSKeyedUnarchiver unarchiveObjectWithFile:filePath]];
}
●コード説明
readFromFile
[model setString:[NSKeyedUnarchiver
unarchiveObjectWithFile:filePath]];
このコードでは
[NSKeyedUnarchiver unarchiveObjectWithFile:filePath]
で得られた値をModelオブジェクトのsetStringメッセージ引数として渡しています。
NSKeyedUnarchiverのリファレンスを見てみましょう。
図NSKeyedUnarchiver
このクラスもメソッド数が少ないのでどのメソッドが必要としているメソッドなのか見当が付けやすいと思います。+unarchiveObjectWithFile:メソッドをクリックして正確なシグネチャを確認します。
+ (id)unarchiveObjectWithFile:(NSString
*)path
引数としてアンアーカイブしたいデータファイルのパスをNSString型の文字列として渡せば復元されたオブジェクトがid型として返ってくることが分かります。繰り返しの説明になりますが、ここではその返ってきた値をModelオブジェクトのsetString:メッセージの引数として渡しています。そしてModelオブジェクトのstringインスタン変数には復元されたオブジェクトのcopyが設定されることになります。NSTextViwを使ったMyClipアプリケーションにとってstringインスタン変数に値を代入する前にcopyしておかなければならない理由はまえにもいくつかあげました。今回の場合は「クラス名と同じ名前で始まるクラスメソッドは一時オブジェクトを作成する」という決まりにしたがってcopyメソッドを使っています。そしてここでもsetStringのcopyメッセージが有効に働いていることになります。
awakeFromNib
if ([model string]) {
[textView
insertText:[model string]];
[window setDocumentEdited:NO];
[textView
scrollRangeToVisible:NSMakeRange(0, 0)];
}
if([model string])で[model string]にデータがあれば続く3行のメッセージ式が実行されます。Controllerオブジェクトが初期化される時にそのControllerオブジェクトのinitメソッドのなかで自身のreadFromFileメソッドが実行されています。その時にデータが存在していればそのデータから復元されたオブジェクトが。またデータが存在していなければnilがModelオブジェクトのstringインスタン変数に設定されます。もし読み込むべきデータがなかった場合、つまりif文の条件式がnilの場合はブロック文の中の3つの式が実行されることはありません。
[textView
insertText:[model string]];
insertText:はTextViewのインスタンスメソッドです。テキストビューの中で次に文字などを挿入することになっている箇所(カーソル、カーソル位置などと呼ばれています)へ引数で指定しているオブジェクトを挿入します。awakeFromNibメソッドでははじめにテキストビューに空文字を設定していますので挿入ポイント(カーソル)はテキストビューの先頭位置になっています。
-
(void)insertText:(id)aString
なおこのメソッドのシグネチャではinsertTextとなっていますが挿入できるものはテキストだけではなくオブジェクトであればおおむね大丈夫になっているみたいです。引数の型もid型になっています。問題は実際にこのメッセージを受け取った側のText Viewがどのオブジェクトに対応できているかということになると思います。
NSTextViewにはこれ以外にもテキストやオブジェクトを設定するメソッドが揃っています。いろいろと試して使い勝手の良いものを探してみてください。
[window setDocumentEdited:NO];
insertText:メッセージでオブジェクトを挿入した場合はテキストビューが変更されたと判断されtextDidChange:デリゲートメッセージが送信されます。するとウィンドウに編集済みマークが入ることになります。そこでもう一度setDocumentEditedの値をNOに戻して編集済みマークを消しています。
[textView scrollRangeToVisible:NSMakeRange(0,
0)];
このメソッドはNSTextViewのスーパークラスのNSTextで定義されているメソッドです。引数で指定した箇所をスクロールの中に表示するようにします。
テキストビューでは次に文字が挿入されるカーソル位置がビューの中に表示されるようになっています。もしアンアーカイブで長い文字列を読み込んだ場合、画面には最後の挿入ポイントが表示され文章の最初は表示されないことも起こります。この状態を避けるために文の最初が画面の中に表示されるようにこのメソッドを使っています。このメソッドの正確なシグネチャは次のようになります。
- (void)scrollRangeToVisible:(NSRange)aRange
引数にはNSRangeという型が指定されています。型名の後にアスタリスク( * )が付いていませんのでオブジェクトではありません。NSRangeは構造体になっています。
11. 2. 5 NSRange
文字列のどこからどこまでを範囲とするかを表すための構造体です。定義は次のようになっています。
typedef struct _NSRange {
NSUInteger
location;
NSUInteger
length;
} NSRange;
1番目のメンバ変数が文字列の何番目の文字かを表し。2番目のメンバ変数がそこから何文字分の長さを範囲とするかを表しています。
このNSRangeを作る関数がNSMakeRangeです。第1引数に文字列の何番目かを表すNSUIntegerを。第2引数にそこから何文字を範囲とするかを表すNSUIntegerを指定するとその値を表すNSRange構造体が作成されます。
NSMakeRange(0, 0)
このコードで文字列の0番目の文字から0文字分の長さを表すNSRangeが作成され、テキストビューに文字列の先頭が表示されることになります。
●実行
コーディングはこれですべて終わりました。Xcode 3.1では「ビルドして進行」を、Xcode 3.2では「ビルドと実行」をクリックして実行してみてください。まだMyClip.dataというファイルは存在しないので何も表示されていないウィンドウが起動します。色々なスタイルを持つ文字を入力してみてください、またURLも入力してみてください。画像も挿入してみましょう。テストが終わりましたら必ず保存をしてからMyClipを終了してください。ユーザのホームフォルダ→「ライブラリ」フォルダ→「Application Support」フォルダ→「MyClip」フォルダにMyClip.dataというファイルが出来上がります。
もう一度MyClipを起動させてください。保存したデータがすべて元通りに表示されます。
11. 2. 6 ネットワークからのアンアーカイブ
アーカイブを使ったファイル入出力においてもテキストデータのファイル入出力と同じくローカルストレージだけではなくネットワークストレージからもデータの読み込みと書き込みができます。しかし今回の場合もネットワークストレージに無条件で書き込める環境は現実的には存在しないのではないかと思われます。アカウントやパスワードに答えなければアクセスが許可されないのが通常の状態でしょう。
そこで基礎編では今回もネットワークストレージのバイト列データをアンアーカイブしてテキストビューに表示する方法だけを紹介します。もしネットワークストレージにアーカイブデータを書き込むテストをしたい場合にはローカルエリアネットワーク上であればうまく動作させることができるかもしれません。本書では紹介いたしませんが興味のある方はぜひ試してみてください。
またネットワークストレージとのやりとりについては実践編においてWeb Kitを使った例をいくつか紹介しています。興味のある方はそちらもご覧になってください。
作業に入る前に今回はバックアップを2つとってください。そしてそのひとつには「MyClip 10.2」などのように前項までの作業のバックアップであることが分かるような名前にしてください。そしてもうひとつのフォルダには「MyClip 10.2Network」という名前を付けてください。そしてこの項だけこの「MyClip
10.2Network」フォルダのプロジェクトを使って作業をしていきます。さっそくプロジェクトを開いてください。
Modelオブジェクト
変更箇所はありません。
Controllerオブジェクト
Controller.h
#import <Cocoa/Cocoa.h>
#import "Model.h"
@interface Controller : NSObject {
Model
*model;
IBOutlet
NSTextView *textView;
IBOutlet
NSWindow *window;
}
- (void)readFromURL;
- (IBAction)writeToFile:(id)sender;
@end
readFromFileのメソッド宣言をreadFromURLに変更します。たんなるメソッド名の変更ですので必ずしも変更しなければならないということはありませんが何のためのメソッドなのか分かりやすくするために変更しています。
Controller.m
変更するメソッドはinitメソッド、readFromURL(元readFromFile)メソッドになります。
●コードリスト
/* init メソッド */
- (id)init
{
self = [super init];
if
(self) {
model
= [[Model alloc] init];
filePath
= [[NSMutableString alloc] initWithCapacity:0];
[filePath
appendString:NSHomeDirectory()];
[filePath
appendString:@"/Library/Application Support/MyClip"];
[[NSFileManager defaultManager]
createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil
error:NULL];
[filePath
appendString:@"/MyClipURL.data"];
[self
readFromURL];
}
return self;
}
/* readFromURL */
- (void)readFromURL
{
/* 実装はすべて変更されています */
NSURL *url
=
[NSURL URLWithString:@"http://vivacocoa.jp/MyClipURL.data"];
NSData
*data = [NSData dataWithContentsOfURL:url];
[model
setString:[NSKeyedUnarchiver unarchiveObjectWithData:data]];
}
●コード説明
initメソッドでは読み書きするファイルの名前をMyClip.dataからMyClipURL.dataに変更しています。データの読み込みメソッドreadFromURLではネットワークからデータを読み込むように別途実装しますのでinitメソッドでの変更は実質上データの書き込み時にだけ関係してきます。ようするにデータの読み込みはネットワークから行い、書き込みはローカルストレージへ行うという構造になっています。この構造はネットワークストレージからのデータのアンアーカイブをテストするために便宜上このようにしているだけです。読み書きするファイルの名前を変えたのも次章からも使い続けるMyClipアプリケーションの本来のデータを上書きしてしまわないためです。
続くメッセージ式ではメッセージ名をreadFromFileからreadFromURLに変えています。
読み込みメソッドではメソッド名をreadFromFileからreadFromURLに変更しています。そして実装はすべて変更されています。一行ずつ説明していきます。
NSURL *url
=[NSURL URLWithString:@"http://vivacocoa.jp/MyClipURL.data"];
NSURLはURLを保持するためのオブジェクトです。メッセージ式のURLWithString:はクラスメソッドです。引数にはURLをNSString文字列として渡します。例えばここをhttp://〜とするだけではなくftp://〜やfile://〜とすることもできます。このサンプルではhttp://vivacocoa.jp/MyClipURL.dataとなっていますが、実際に入力するURLはあなたがファイルをアップロードできるサーバーにしてください。例えばご契約されているプロバイダのサーバースペースなどでかまいません。
NSData *data =
[NSData dataWithContentsOfURL:url];
NSDataはバイト列データをオブジェクトとして扱うためのクラスです。少しややこしいかもしれませんがバイト列データをオブジェクトに復元するクラスではなく、バイト列データをバイト列データのまま、さもオブジェクトのように取扱うためのクラスです。このように本来オブジェクトではない値をオブジェクトのように扱うためのクラスをラッパクラスと呼びます。
dataWithContentsOfURL:メッセージはクラスメソッドになります。NSURL型の引数urlが表しているURLからバイト列を読み込んでその値をオブジェクトの形にして返します。
[model
setString:[NSKeyedUnarchiver unarchiveObjectWithData:data]];
unarchiveObjectWithData:は引数のNSData型のオブジェクトから(実際にはバイト列データ)からオブジェクトを復元するNSKeyedUnarchiverのクラスメソッドになります。このメソッドで返されたオブジェクトをModelオブジェクトのsetString:メソッドの引数として渡しています。
なおこれらの3行の式を1行にネストさせると次のようになります。
[model setString:[NSKeyedUnarchiver unarchiveObjectWithData:
[NSData dataWithContentsOfURL:
[NSURL URLWithString:@"http://vivacocoa.jp/MyClip.data
"]]]]; お疲れ様でした。以上でコーディングは完了いたしました。さっそくテストをしてみましょう。少しイレギュラーな作業手順になりますが我慢してください。
1. プロジェクトを「ビルドして進行」もしくは「ビルドと実行」します。読み込み先のURLには指定のデータファイルはないはずですので何も書かれていない白いMyClipウィンドウが表示されます。
2. テキストビューに好きな文章などを書いてください。属性を指定するなどお好きなことをどんどん行ってください。
そして表示されたフォントパネルでDFP勘亭流というフォントの72サイズを選びました。
4. このMyClipURL.dataをあなたがreadFromURLの中で指定したURLのサーバースペースへアップルロードしてください。
5. 再びMyClipを起動してください。無事に同じデータが表示されたでしょうか?
お疲れ様でした。無事にデータが表示されたことだろうと思います。これでネットワークからのバイト列データの読み込みと復元に成功したことになります。
ラッパクラス 「ネットワークからアンアーカイブ」の項でNSDataというラッパクラスが登場してきました。ラッパクラスはバイト列データや数値などの本来はオブジェクトではないものをオブジェクトして扱うためのクラスです。バイト列や数値などにオブジェクトとしてのインタフェースを被せるような仕組みになっています。 ラッパはwrapperのことで「包み紙」などの意味になります。この意味のとおりにバイト列や数値をオブジェクトという包装紙で包んでオブジェクトとして扱えるようにするというイメージだと思います。 バイト列をオブジェクト化するラッパクラスがNSDataで、数値をオブジェクト化するラッパクラスがNSNumberになります。 本書をここまで読まれてすでに気づかれている方もおられるかもしれませんがCocoaフレームワークに用意されているオブジェクトのメソッドでは、引数などをオブジェクトとして求めるものが多く存在します。そのような場合にこのラッパクラスが役立つことになります。
This site is available in Safari and Snow Leopard. | (c) viva Cocoa 2006 - 2010 |