☆Cocoa Foundationクラス(等)勉強室2
ファイル操作;NSFileManager/NSFileHandle
iOSは見かけ上「ファイルという概念が非常に薄い」。
少なくとも、WindowsやMacの様に、ユーザーがファイルを直接操作する環境が存在しない。
基本的には、ファイルはアプリケーションごとに保持しており、
データファイルの共有はいくつかのクラスを用いて可能になっているが、
たとえばフォントなど、リソースの共有とか言う概念はない。
なぜ「ファイルという概念を隠蔽する」のか。
ファイルというのは、コンピューターにとっては基本中の基本の概念ではあるが、
結構難しいものの考え方である。それを「携帯電話」程度の物で意識させることは必要ない、ということであろう。
家電と一緒なのだ。最近はTVでインターネットアクセスが出来る様になっているが、
そこにファイルの概念は見えてこない。レコーダーだって、内部ではファイルで画像を記録しているはずだが、
それを見せない。そういうことだ。
PCのファイルシステムに慣れている人が、iOSデバイスを触るとまず面食らうのがここだ(と思う)が、
このデバイスの向かっている先を考えれば、これは仕方ないことである。
ではあっても内部的にはファイルの概念はあるようなので、そのクラスを見ていく。
なお、以降「フォルダ」と「ディレクトリ」は同じ意味である。
Mac上のフォルダはそれを1つのファイルとして認識させることも可能で、真の意味で「フォルダ」であったが、
iOSのそれはWindowsと同じでディレクトリ構造そのものだけである。
NSFileManager
メソッド名 | 動作 |
+(NSFileManager *)defaultManager | デフォルトのファイルマネージャーを返す |
-(BOOL)fileExistsAtPath:(NSString *)path | ファイルが実際に存在しているか調べる |
-(BOOL)createFileAtPath:(NSString *)path contents:(NSData *)contents attributes:(NSDictionary *)attributes
| 内容データで名前と属性を指定してファイルを作る |
-(NSData *)contentsAtPath:(NSString *)path | パスで指定されるファイルからNSDataを作る |
-(BOOL)changeCurrentDirectoryPath:(NSString *)path | 現在のディレクトリパスを変更 |
-(NSString *)currentDirectoryPath | 現在のディレクトリパスを返す |
-(BOOL)createDirectoryAtPath:(NSString *)path withIntermediateDirectories:(BOOL)createIntermediates attributes:(NSDictionary *)attributes error:(NSError **)error
| 属性を指定して空のディレクトリを作る |
-(BOOL)copyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error
| srcPath から desPath にディレクトリまたはファイルをコピーする |
-(BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error
| srcPath から desPath にディレクトリまたはファイルを移動する |
-(BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error
| 指定したパスのファイル/ディレクトリを削除 |
-(BOOL)contentsEqualAtPath:(NSString *)path1 andPath:(NSString *)path2
| ファイルやディレクトリの中身を比較 |
-(BOOL)fileExistsAtPath:(NSString *)path | ファイルが実際に存在しているか、またディレクトリかどうかを返す |
-(BOOL)fileExistsAtPath:(NSString *)path isDirectory:(BOOL *)isDirectory |
指定パスの中にファイルが実際に存在しているか、またディレクトリかどうかを返す
|
-(BOOL)isReadableFileAtPath:(NSString *)path | パスのファイルは読み込み可能か調べる |
-(BOOL)isWritableFileAtPath:(NSString *)path | パスのファイルは書き込み可能か調べる |
-(NSDirectoryEnumerator *)enumeratorAtPath:(NSString *)path | 指定したパスのディレクトリの内容をディレクトリ列挙子オブジェクトで返す |
-(NSArray *)subpathsAtPath:(NSString *)path | 全てのファイルやディレクトリ(サブディレクトリも含めて)のパスの配列を返す |
ファイル属性キー
以下のキーは、NSDictionary オブジェクトで ファイル属性値にアクセスするためのキーである。
使うのは以下のメソッド。
createDirectoryAtPath:withIntermediateDirectories:attributes:error:
createFileAtPath:contents:attributes:
(NSString定数値)
NSFileType | ファイル属性値(後述) |
NSFileSize | ファイルサイズ(バイト);リソースフォークのサイズは含まない |
NSFileModificationDate | 最終変更日 |
NSFileReferenceCount | 参照回数 |
NSFileOwnerAccountName | ファイル保有者の名前 |
NSFileSystemFileNumber | ファイル番号 |
NSFileExtensionHidden | ファイルの拡張子を隠すかどうか |
NSFileImmutable | ファイルが可変かどうか |
NSFileAppendOnly | ファイルが読み込み専用かどうか |
NSFileCreationDate | ファイル作成日 |
NSFileBusy | ファイルがBUSYかどうか |
たくさんのメソッドが存在するが、その全部を覚える必要はない。
Cの標準関数にも多数のファイルアクセス関数があるが、使うのはごく一部だけで十分なのと同じである。
実際のファイルアクセスに必要そうな部分のみを使ってみる。
なお、上記を見れば解るが、実際にファイルに読み書きするメソッドはこの中には含まれていない。
それは、NSArray/NSMutableArrayやNSDictionaryなど、データを保持する側のクラスが持っているからである。
まずは基本的なファイルアクセス方法。iOSレベルではこれだけで十分かもしれない。
// ファイルポインターを取得する
NSFileManager *fileManager = [NSFileManager defaultManager];
// ディレクトリ検索リストを作成する
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// paths[0]を取り出す;そこにディレクトリ名が入っているらしい
NSString *documentsDirectory = (([paths count] > 0)? // 見つかったら
[paths objectAtIndex:0] // 最初のもの
: NSTemporaryDirectory() ); // なければ一時ディレクトリ
// ファイルパスの末尾にファイル名(ここでは dictionary.plist )を追加
NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:@"dictionary.plist"];
// ファイルの存在を確認
if ([fileManager fileExistsAtPath:dataPath]) {
// 辞書をファイル内容から生成する
dictionary = [[NSDictionary alloc] initWithContentsOfFile:dataPath];
}
ここでは珍しくクラスではない関数を使っている。
NSArray *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory
, NSSearchPathDomainMask domainMask
, BOOL expandTilde)
動作
ディレクトリ検索パスのリストをつくる
引数
directory | フォルダを指定 |
NSPathUtilities.h
NSDocumentDirectory | ドキュメント |
NSApplicationDirectory | アプリケーション |
NSDesktopDirectory | デスクトップ |
NSLibraryDirectory | ライブラリ |
NSUserDirectory | ユーザー |
NSDeveloperDirectory | デベロッパー |
NSApplicationSupportDirectory | アプリケーションサポート |
NSDeveloperApplicationDirectory | デベロッパー/アプリケーション |
NSAllApplicationsDirectory | すべてのアプリケーションのディレクトリ |
NSAllLibrariesDirectory | すべてのライブラリのフォルダディレクトリ |
|
domainMask | どこのディレクトリを探すか |
NSUserDomainMask | ユーザホーム |
NSSystemDomainMask | システム |
NSLocalDomainMask | ローカル |
NSNetworkDomainMask | ネットワーク |
NSAllDomainsMask | 全て |
|
expandTilde | ~(チルダ)を展開するかどうか | YES=展開する/NO=しない
チルダ付きというのはユーザーディレクトリであるということ、らしい
|
返り値
配列の中にパスが入る
複数の場所を探す様に指定した場合は、複数の値が返ってくるのでが配列になっている
である。よくわからん部分もあるが、「この例の通りに使う」とだけ覚えておけばいいのだろう。
この例では「辞書」を読み込んでいるが、配列でも基本的に全く同じである。
これを書くために色々と調べてみたが、どうもCocoa Touchを使う上ではファイルのオープンやクローズはする必要がなさそうである。
Cでファイル操作を多数書いてきた身にとっては違和感があるが、それはそれで楽なのかもしれない。
しかし、OSレベルでオープン・クローズがないはずはないので、イニシャライザとクラスの終了処理で行われているのだろう。
もう少しCに近いファイルアクセスを実現する場合には
NSFileHandleを使う。
バックグラウンド処理も標準で用意されている。
NSFileHandle
メソッド名 | 動作 | Cの相当関数など |
+(id)fileHandleForReadingAtPath:(NSString *)path | 読み込みファイル/デバイスハンドルを作る | fopen |
+(id)fileHandleForReadingAtURL:(NSURL *)path error:(NSError **)error | 読み込みファイル/デバイス/URLハンドルを作る | fopen("rb") |
+(id)fileHandleForUpdatingAtPath:(NSString *)path | 読み書きファイル/デバイスハンドルを作る | fopen |
+(id)fileHandleForUpdatingAtURL:(NSURL *)path error:(NSError **)error | 読み書きファイル/デバイス/URLハンドルを作る | fopen("rw") |
+(id)fileHandleForWritingAtPath:(NSString *)path | 書き込みファイル/デバイスハンドルを作る | creat |
+(id)fileHandleForWriteingAtURL:(NSURL *)path error:(NSError **)error | 書き込みファイル/デバイス/URLハンドルを作る | fopen("w") |
+(id)fileHandleWithStandardError | 標準エラー出力のファイルハンドルを作る | stderr |
+(id)fileHandleWithStandardInput | 標準入力のファイルハンドルを作る | stdin |
+(id)fileHandleWithStandardOutput | 標準出力のファイルハンドルを作る | stdout |
+(id)fileHandleWithNullDevice | NULLデバイスのファイルハンドルを作る | |
-(id)initWithFileDescriptor:(int)fileDescriptor | ファイルデスクリプタで初期化して、ファイルハンドルを返す | open |
-(NSData *)availableData | ファイルポインタからファイルの終わりまで読んだNSDataを返す | |
-(void)closeFile | ファイルを閉じる | fclose |
-(NSData *)readDataOfLength:(NSUInteger)length | 指定した長さ分を読んでNSDataを返す | fread |
-(NSData *)readDataToEndOfFile | ファイルの終わりまでを読んでNSDataを返す | |
-(void)writeData:(NSData *)data | ファイルハンドルにNSDataを書く | fwrite |
-(void)readInBackgroundAndNotifyForModes:(NSArray *)modes | バックグラウンドで読んで、通知する | |
-(void)readInBackgroundAndNotify | バックグラウンドでファイルを読んで、読み終わったら通知する | |
-(void)readInBackgroundAndNotifyForModes:(NSArray *)modes: | バックグラウンドでファイルの最後まで読んで、通知する | |
-(void)readInBackgroundAndNotif | バックグラウンドでファイルの最後まで読んで、通知する | |
-(unsigned long long)offsetInFile | ファイルポインタの位置を返す | ftell |
-(unsigned long long)seekToEndOfFile | ファイルの最後を探する | feof |
-(void)seekToFileOffset:(unsigned long long)offset | 指定した位置にファイル読み出しポインタを移動 | fseek |
-(void)synchronizeFile | メモリ上ディスクキャッシュ残るデータをディスクに吐き出す | fflush |
-(void)truncateFileAtOffset:(unsigned long long)offset | 指定するオフセットでファイルを切り詰めたり広げたりする | |
// パスをフルパスにする
NSString* outputFilePath = [outputFilePath stringByExpandingTildeInPath];
// 出力するファイルを生成(書き込み側が新規の場合は必須)
[[NSFileManager defaultManager] createFileAtPath:outputFilePath contents:nil attributes:nil];
// ファイルポインターを取得
NSFileHandle* ifp = [NSFileHandle fileHandleForReadingAtPath:inputFilePath];
NSFileHandle* ofp = [NSFileHandle fileHandleForWritingAtPath:ouputFilePath];
// ファイルがない場合はnilが返ってくる
// 書き込み側が新規の場合も、先にファイルを作っておかないと「存在しない」になるので注意
// 出力ループ
@try {
NSData* data
while (data = [ifp readDataOfLength:1]) {
[ofp writeData:data];
}
} @finally {
[ifp closeFile];
[ofp closeFile];
}
ファイルの新規作成の場合、ファイル作成(createFileAtPath:)とファイルポインターの取得が別になるのが注意点。
既存ファイルの場合はファイル作成の処理は省ける。
ここではバイナリデータの格納できるNSDataを使って1バイト毎Read→Writeしている。
やはりここでもファイルオープン処理はない。クローズは一応書かれているが、
エラー時のみしか通らない。正常処理に関してはそれらは不要のようだ。
Objective-CはCの完全互換だから、Cの標準関数でのファイルアクセスも出来る。
私はCに慣れているのでほとんどがCの関数で書いてしまう。
特に問題はないようである。
URL操作;NSURL/NSURLConnection/NSURLRequest/NSURLResponse
NSURLはURLを操作するクラスである。
ファイル(パス)名はNSStringであるが、URLはNSStringではなく専用のクラスが与えられている。
NSStringの子クラスであると思われるが、URLを分解するメソッド等が追加される。
はっきり言って、URLの構成自体よく知らないが、少なくとも「URLはNSStringで処理するのではない」ということだけは理解しておかなければならない。
まずは
NSURL
のURL分解系。URLに小細工しておけば、どこから飛んできたかなどを知ることができるので、それを取得するときなどに使える。
メソッド名 | 動作 |
+(id)URLWithString:(NSString *)URLString | 文字列でNSURLを作る |
-(NSString *)absoluteString | URLをNSStringで取得する |
-(NSURL *)absoluteURL | URLをNSURLで取得する |
-(NSString *)scheme | レシーバーのスキームを取得する | http,ftp,mailto,file等 |
-(NSString *)host | ホストを取得する |
-(NSNumber *)port | ポート番号を取得する | 指定なしの場合は0 |
-(NSString *)fragment | アンカーを取得する |
-(NSString *)path | パスを取得する |
-(NSString *)query | クエリーを取得する |
-(NSString *)parameterString | パラメータ文字列を取得する |
-(NSString *)password | パスワードを取得する |
-(NSString *)user | ユーザー名を取得する |
-(NSURL *)baseURL | レシーバーのベースURLを取得する |
-(NSString *)resourceSpecifier | リソース指示子を取得する |
-(NSString *)relativePath | 関連パスを取得する |
-(NSString *)relativeString | 関連パスを取得する |
NSURL *url_data = [NSURL URLWithString:@"https://www.test.jp:8080/cgi-bin/test.cgi?TEST=1&TEST2#TEST3"];
absoluteString | (NSString*)@"https://www.test.jp:8080/cgi-bin/test.cgi?TEST=1&TEST2#TEST3" |
absoluteURL | (NSURL*)@"https://www.test.jp:8080/cgi-bin/test.cgi?TEST=1&TEST2#TEST3" |
resourceSpecifier | https://www.test.jp:8080/cgi-bin/test.cgi?TEST=1&TEST2#TEST3 |
scheme | https |
host | www.test.jp |
port | 8080 |
fragment | TEST3 |
path | /cgi-bin/test.cgi |
relativePath | /cgi-bin/test.cgi |
query | TEST=1&TEST2 |
parameterString | nil |
password | nil |
user | nil |
baseURL | ? |
relativeString | ? |
次に
NSURL
のURL操作系。
メソッド名 | 動作 |
-(BOOL)isFileURL | ファイルのURLか調べる |
+(id)URLWithString:(NSString *)URLString relativeToURL:(NSURL *)baseURL | ベースURLと文字でNSURLを作って初期化して返す
URLの書式はHTML中のmailto:の書式である(RFC822ではない)
|
+(id)fileURLWithPath:(NSString *)path | パスからURLを作る |
-(id)initFileURLWithPath:(NSString *)path | ファイルパスからNSURLを初期化して返す |
-(id)initWithScheme:(NSString *)scheme host:(NSString *)host path:(NSString *)path | スキーム、ホストとパスでURLとして新しくつくられたNSURLを初期化して返す |
-(id)initWithString:(NSString *)URLString | NSURLを文字で初期化して返す |
-(NSURL *)standardizedURL | ..や.の付いたURLを正規化する |
NSURLConnectionはNSURLDownloadに近い動作をするが、
NSURLDownloadが必ずファイルへの書き出しを伴うのに対して、
NSURLConnectionはメモリ上で処理する。
内部動作については
ここ参照のこと。
NSURLConnectionには
同期=一気読み込みと非同期=並行動作で読み込みの2つがあるらしい。
前者は終了するまで待つことになるので、そこで処理が止まってしまうことと、それゆえに大きなデータ取得には適さないので、
非同期で取得する方法の方が一般的らしい。プログレスバーを出すなど、途中に処理を追加したい場合も非同期を使う。
NSURLConnection
メソッド名 | 動作 |
-(id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate | リクエストとデリゲートでNSURLConnectionを初期化する |
+(BOOL)canHandleRequest:(NSURLRequest *)request | リクエストを扱う事ができるかを返す |
-(void)cancel | 接続をキャンセルする |
デリゲートメソッドは以下の通りである。
メソッド名 | 動作 |
-(void)connection:(NSURLConnection *)connection
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge |
didFailWithError:(NSError *)error |
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge |
didReceiveData:(NSData *)data |
didReceiveResponse:(NSURLResponse *)response |
didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite |
willCacheResponse:(NSCachedURLResponse *)cachedResponse |
| 接続処理途中で呼び出される。
認証キャンセル時 |
接続できなかった時 |
認証しなければいけない時 |
データを受信時 |
レスポンス受信時 |
要求のボディ(メッセージデータ)が送られた時(例えばhttpポスト要求において) |
レスポンスをキャッシュしようとした時 |
|
-(void)connectionDidFinishLoading:(NSURLConnection *)connection | データのロードが完了したときに呼び出される |
これもNSDownload同様メソッドはいろいろあるが、基本的には一定の処理手順を経る経過として呼び出されるだけなので、
ほぼ固定の書き方となる。
// 同期読み込み
#import "Foundation/Foundation.h"
int main()
{
id pool = [[NSAutoreleasePool alloc] init];
NSURL *url = [NSURL URLWithString:@"http://www.example.com/"];
// URL Request
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
// 結果を格納するオブジェクト
NSURLResponse *resp;
// エラーを格納するオブジェクト
NSError *err;
// 同期的な呼び出し。sendSynchronousRequestが味噌
// 終了するまで帰ってこない
NSData *result = [NSURLConnection
sendSynchronousRequest:req
returningResponse:&resp
error:&err
];
// 結果をコンソールへ出力。writeはCの標準関数。ただし、この書き方は「例」なので、このままでは動かない(includeもないし)
write(1, [result bytes], [result length]);
[pool release];
}
非同期の場合は、デリゲートが必要とされている(initWithRequest:)。
// 非同期読み込み
#import "Foundation/Foundation.h"
// 通信の進捗具合に応じて呼び出される
@interface myUrlConnDelegate : NSObject
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
@end
@implementation myUrlConnDelegate
// レスポンスを受け取った時点で呼び出される。データ受信よりも前なので注意
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(@"didReceiveResponse");
}
// データを受け取る度に呼び出される
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(@"didReceiveData");
}
// データを全て受け取ると呼び出される
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"connectionDidFinishLoading");
}
@end
int main(int argc, char *argv[])
{
id pool = [[NSAutoreleasePool alloc] init];
NSLog(@"main");
NSURLRequest *req;
req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.example.com/"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0
];
// 呼び出しを受け取るためのオブジェクトを生成
myUrlConnDelegate *del = [[myUrlConnDelegate alloc] init];
// myUrlConnDelegateへdelegateしながら初期化
NSURLConnection *conn;
conn = [[NSURLConnection alloc] initWithRequest:req delegate:del];
if (conn) {
NSLog(@"main : NSURLConnection create success");
} else {
NSLog(@"main : conn is nil");
return 1;
}
// 実際の動作はNSRunLoop=並行動作を行うクラス で行われる
NSLog(@"main : Before NSRunLoop");
[[NSRunLoop currentRunLoop] run];
// このサンプルコードはここへ到達しない
[pool release];
}
上記サンプルに出てくる
NSURLRequest/NSURLResponseは以下のメソッドをもつ。が、とりあえず例の部分だけ「暗記」しておけばいいと思う。
NSURLRequest
メソッド名 | 動作 |
+(id)requestWithURL:(NSURL *)theURL
なし |
cachePolicy:(NSURLRequestCachePolicy)cachePolicy timeoutInterval:(NSTimeInterval)timeoutInterval |
|
URLからNSURLRequestを作る |
キャッシュポリシーとタイムアウト間隔でNSURLRequestを作る |
|
-(id)initWithURL:(NSURL *)theURL
なし |
cachePolicy:(NSURLRequestCachePolicy)cachePolicy timeoutInterval:(NSTimeInterval)timeoutInterval |
|
URLでURLリクエストを初期化して返す |
キャッシュポリシーとタイムアウト間隔でNSURLRequestを初期化して返す |
|
-(NSURLRequestCachePolicy)cachePolicy | キャッシュポリシーを返す
NSURLRequestCachePolicy(enum値)
NSURLRequestUseProtocolCachePolicy | キャッシュは特定のURLロード要求のために使われる |
NSURLRequestReloadIgnoringLocalCacheData | URLからのキャッシュを使わずデータをロードする |
NSURLRequestReloadIgnoringCacheData | 同上 |
NSURLRequestReloadIgnoringLocalAndRemoteCacheData | 解析中 |
NSURLRequestReturnCacheDataElseLoad | 解析中 |
NSURLRequestReturnCacheDataDontLoad | 解析中 |
NSURLRequestReloadRevalidatingCacheData | 解析中 |
|
-(NSURL *)mainDocumentURL | メインドキュメントURLを返す |
-(NSTimeInterval)timeoutInterval | タイムアウト間隔を返す |
-(NSURL *)URL | URLを返す |
-(NSDictionary *)allHTTPHeaderFields | HTTPヘッダを返す |
-(NSData *)HTTPBody | ボディを返す |
-(NSString *)HTTPMethod | HTTPメソッドを返す |
-(BOOL)HTTPShouldHandleCookies | クッキーを使用するかを返す |
-(NSString *)valueForHTTPHeaderField:(NSString *)field | HTTPヘッダの指定した値を返す |
NSURLResponse
メソッド名 | 動作 |
-(id)initWithURL:(NSURL *)URL MIMEType:(NSString *)MIMEType expectedContentLength:(NSInteger)length textEncodingName:(NSString *)name | URLResponseを初期化して返す |
-(long long)expectedContentLength | 受け取る長さを返す |
-(NSString *)suggestedFilename | 提案されるファイル名を返す |
-(NSString *)MIMEType | MIMEタイプを返す |
-(NSString *)textEncodingName | テキストエンコーディング名を返す |
-(NSURL *)URL | URLを返す |
なお、MacOS10.6では「initWithFileは使わずinitWithURLを使うべし」というお達しが出ているようなので、
iOSも必要なら、それに準じるべきなのであろう。
リソースプログラミング(ちょっとだけ)、バンドル;NSBundle
リソースとはなんぞや。誤解混じりで超簡単に書けば「プログラム中で、データとなる部分全ての集合体」のことである。
グラフィック、音声などのデータは言うに及ばず、数値や文字列も、これみなリソースなのである。
iOSのユーザーインターフェース(以下UI)設計は、基本的にインターフェースビルダー(以下IB)に任せるようになっている。
IBはUIオブジェクトの配置や色や大きさなど全ての属性、表示文字列などを設定できる。
そを設定するはプログラムのソースとして出力される・・・わけではない。
ソースコードは作られず、nibファイルというものの中に格納される。
「nibファイル」と書いているが、実際の拡張子は.xibである。
このファイルはMacOS Xの元となったNextSTEPにあった「.nib」ファイルに由来するので、
現在でも慣例的に「nibファイル」と呼ばれる。
ということは、プログラム実行時にはそれを読み込まないとUI部が表示されないということでもある。
この様に、プログラム上で固定すべきでない情報を全て「リソース」として外部ファイルに
置いてしまうプログラム方法を、アップルでは特に「リソースプログラミング」と呼んでいるようである。
UI情報だけでなく、アイコンファイル名、GPSを使うかどうかの情報、プログラム名なども全て
リソースに格納するようになっており、プログラム名-info.plistにその情報が含まれている。
リソースはプログラム本体とは別に外部にあるため、その変更だけならプログラムを全くいじらず行うことが出来る。
ここで書いたとおり、未実装なメニュー項目を入れておくことも出来る。
また、各国版を作るためのローカライズも(プログラム側をちゃんと設計しておけば)nibファイルの編集だけで出来てしまう。
なかなかすごいことではあるが、逆に、UI設計を全てIBにゆだねるプログラム設計、
ターゲット-アクションを設定するなどがインターフェースビルダーを理解する上で一番難しい場所だとも言える。
UI部分以外のリソースを作るには、
プロパティリスト(Property List)という物を使う。
プロパティリストというのは、NSDictionaryの内容をファイルに落としたものであるが、通常「*.plist」ファイルである。
プロパティリストはXMLフォーマット=テキストファイルでもいいので(デフォルトはバイナリフォーマット)、手で書くこともできるが、
プログラム的に生成出来る場合を除き、「PorpertyList Editor」を使った方が楽である。
プロパティリストに含めることができるデータ型は、Dictionary、Array、String、Data、Date、Number、Boolean である。
Dictionary のデータを作りたいときは、Root を Dictionary に、Array のデータが欲しいときは、Root を Array に
設定する。
そうやって作ったリソースを、パッケージの中に入れてやると、NSBundleを使って取り出すことができる。
NSBundleはアプリケーションファイル内にバンドル(同梱)されているファイルの位置を表すクラスである。
バンドル内には通常以下のようなものが格納される。
- 実行可能コード
- 画像
- ローカライズ文字列
- 音声(サウンド)
- nibファイル
そのほか、先に書いたとおりのProperty Listのデータが格納される。
どのアプリケーションでも最低1つのバンドルがあり、メインバンドル(main bundle)と呼ぶ。
NSBundle
メソッド名 | 動作 |
+(NSBundle *)mainBundle | メインバンドルを返す |
-(NSString *)bundlePath | バンドルディレクトリのフルパス名を返す |
-(NSString *)pathForResource:(NSString *)name ofType:(NSString *)extension
| バンドルのリソースから名前と拡張子でファイルを探してそのパスを返す
|
-(NSDictionary *)infoDictionary | Info.plistの含む内容を辞書として返す |
-(NSString *)resourcePath | リソースを含んでいるレシーバのサブディレクトリのフルパス名を返す |
他にもいろいろあるが、iOS上で使う文はこの程度で事足りると思う。
UIKitを使えるようにしたときには、(カテゴリ)により、
以下のメソッドが追加される。
NSBundle UIKit Additions
メソッド名 | 動作 |
-(NSArray *)loadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary*)options |
レシーバーバンドル内のnibファイルの内容を解凍する
|
まず、pathForResource: ofType: を使って、パスを取得する。
その中身を、Dictionary だったら
NSDictionary の dictionaryWithContentsOfFile:を、
Array だったら
NSArray の arrayWithContentsOfFile:を使って読み出し格納することができる。
NSBundle* mainBundle;
NSString* fileName;
mainBundle = [NSBundle mainBundle];
// リソースのパスを取得
fileName = [mainBundle pathForResource:@"sample" ofType:@"plist"];
//fileName = [mainBundle pathForResource:@"sample.plist" ofType:nil]; // これでも良い
if (fileName) {
id dict = [NSDictionary initWithContentsOfFile:fileName];
}
この例では、"sample.plist"というファイルに辞書のデータが入ってると想定している。
リソースプログラミングについては、
「リソースプログラミングガイド」(Resource Programming Guide)
も参照のこと。
デフォルト設定;NSUserDefaults
NSUserDefaultsクラスは、超簡単に言えば、「設定値を保存する」ためのクラスである。
アプリケーションの各種設定はアプリケーションでファイルを作ってその中に保存する、
なんてことはしないで、基本はこのクラスで記録するわけである。
NSUserDefaultsクラスは、一般型(float,doube,integer,BOOL値,URL等)とid型にアクセスするためのメソッドを提供している。
デフォルトオブジェクトは内部ではプロパティーリストになっている。
ということは辞書構造なわけであり、各値はキー名(defaultName)とともに設定する。
キー名はNSStringであり、読み出し時はそのキー名で指定する。
NSUserDefaults
メソッド名 | 動作 |
+(NSUserDefaults *)standardUserDefaults | ユーザーデフォルトオブジェクトを返す |
-(void)setBool:(BOOL)value forKey:(NSString *)defaultName | BOOL値を設定する |
-(void)setDouble:(double)value forKey:(NSString *)defaultName | double値を設定する |
-(void)setFloat:(float)value forKey:(NSString *)defaultName | float値を設定する |
-(void)setInteger:(NSInteger)value forKey:(NSString *)defaultName | NSInteger(int)値を設定する |
-(void)setObject:(id)value forKey:(NSString *)defaultName | オブジェクトを設定する |
-(void)setURL:(NSURL *)url forKey:(NSString *)defaultName | URLを設定する |
-(BOOL)boolForKey:(NSString *)defaultName | 指定キーに割り付けられているBOOL値を返す |
-(double)doubleForKey:(NSString *)defaultName | 指定キーに割り付けられているdouble値を返す |
-(float)floatForKey:(NSString *)defaultName | 指定キーに割り付けられているfloat値を返す |
-(NSInteger)integerForKey:(NSString *)defaultName | 指定キーに割り付けられているNSInterger値を返す |
-(id)objectForKey:(NSString *)defaultName | 指定ユーザーに最初に出現したオブジェクトを返す |
-(NSURL *)URLForKey:(NSString *)defaultName | 指定キーに割り付けられているURL文字列を返す |
-(NSArray *)arrayForKey:(NSString *)defaultName | 指定キーに割り付けられている配列オブジェクトを返す |
-(NSData *)dataForKey:(NSString *)defaultName | 指定キーに割り付けられているデータオブジェクトを返す |
-(NSDictionary *)dictionaryForKey:(NSString *)defaultName | 指定キーに割り付けられている辞書オブジェクトを返す |
-(NSString *)stringForKey:(NSString *)defaultName | 指定キーに割り付けられている文字列を返す |
-(NSArray *)stringArrayForKey:(NSString *)defaultName | 特定キーに割り付けられている文字列の配列を返す |
-(void)removeObjectForKey:(NSString *)defaultName | 指定デフォルトキーを持つ値を削除する |
-(BOOL)synchronize | 内容をディスクに書き込む。
通常でユーザーデフォルトオブジェクトはオンメモリにキャッシュされているが、
これを発行することでディスク上に書き込まれる。
iOS4以降ではユーザーアプリのマルチタスクができるようになったため、
一連のユーザーデフォルトオブジェクト設定が終わったら、
これを発行して内容を確定した方が良いとされている。
|
-(id)init | NSUserDefaultsオブジェクトを現在のユーザーのデフォルトで初期化する |
-(id)initWithUser:(NSString *)username | NSUserDefaultsオブジェクトを指定のユーザーのデフォルトで初期化する |
書き込まれるのはアドレスではなく、その時点での実内容である。
従って、書き込んだ後にその与えたクラスなどの内容を変更しても影響しない。
日時;NSDate/NSDateFormatter/NSLocale/NSTimeZone/NSCalender/NSDateComponents
NSDateは日時を操作するクラスである。
日時、特に日付の処理というものは、実は国際化を考える上でかなり面倒な部分である。
日本は年/月/日の順であるが、アメリカは月/日/年だし、欧州は日/月/年である。
英語圏では月を数字ではなく英語名で書く場合も多い(Jan,Febとか)。
なぜこのように地域によって表記が違うかというのは、日に対するものの考え方の違いとして
理解することができると思う。すなわち、日本は毎日が違う日だというものの考え(一期一会)だから年から書く、
アメリカは毎年同じようなことをしているから、年の識別は余り重要視しない、
欧州に至っては、毎月同じようなことをしているので以下略。
まあ、日本には元号があり、しかも明治以降はともかく江戸までは数年で変わることもあったので、
「年の識別が非常に重要だった」というのが本当のところだろうが。
余談はさておき、実際に、組み込み機器を造り、それを海外でも売ろうと思うと、
この日付順への対応は必須となる。で、結構面倒なので、クラスとして用意してくれるなら、
「そりゃ楽だ」というものだ。まあ、実際にはそんなに楽ではないが。
この辺、日本人はよく理解しているようだが、欧米では余り認識がないらしく、
順固定である場合が多い。だからあちら産のソフトはちょいと使いにくいと思うことがある。
これまた余談。
NSDateは単に日時を記録するだけではなく、後述の並行動作系の投入時間の指定などにも使われる。
NSDate
メソッド名 | 動作 |
+(id)date | 現在の日付と時間でNSDateを作る |
+(id)dateWithTimeInterval:(NSTimeInterval)seconds sinceDate:(NSDate *)date | 指定日時から指定秒数後のNSDateを作成して返す |
+(id)dateWithTimeIntervalSince1970:(NSTimeInterval)seconds | 1970年1月1日0時0分0秒から指定した秒数前後した日時を返す |
+(id)dateWithTimeIntervalSinceNow:(NSTimeInterval)seconds | 現在の日時から指定した秒数で日時を作る
即時の場合はseconds=0.0にする |
+(id)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds | 2001年1月1日0時0分0秒から指定した秒数前後した日時を返す |
+(id)distantFuture | 遠い未来の日付を返す 特殊な日時状態を設定するときに使う |
+(id)distantPast | 遠い過去の日付けを返す 用途は〃 |
-(id)init | NSDateを初期化する |
-(id)initWithTimeIntervalSinceNow:(NSTimeInterval)seconds | 現在の日付、時間から指定した秒数前後した日時で初期化 |
-(id)initWithTimeInterval:(NSTimeInterval)seconds sinceDate:(NSDate *)refDate | 指定日付時間より指定秒数前後した日時を初期化 |
-(id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds | 2001年1月1日0時0分0秒から指定した秒数前後した日時で初期化 |
-(id)initWithTimeIntervalSince1970:(NSTimeInterval)seconds | 1970年1月1日0時0分0秒から現在の日時の間隔で初期化 |
-(NSTimeInterval)timeIntervalSinceDate:(NSDate *)anotherDate | 2つの時間差を計算する |
-(NSTimeInterval)timeIntervalSinceNow | 現在の時間との時間差を計算する |
-(NSTimeInterval)timeIntervalSinceReferenceDate | グリニッジ標準時の2001年1月1日0時0分0秒と現在との差を秒数で返す |
-(NSTimeInterval)timeIntervalSince1970 | 1970年1月1日0時0分0秒から現在の日時の間隔を返す |
-(NSString *)description | YYYY-MM DD HH:MM:SS±HHMM 文字列にして返す |
-(NSString *)descriptionWithLocale:(id)locale | ローカライズした文字列にして返す |
-(BOOL)isEqualToDate:(NSDate *)anotherDate | 日時比較をする |
-(NSDate *)earlierDate:(NSDate *)anotherDate | 日時を比較、より古い日時を返す |
-(NSDate *)laterDate:(NSDate *)anotherDate | 日時を比較して、より新しい日時を返す |
-(NSComparisonResult)compare:(NSDate *)anotherDate | 日付を比較してその結果を返す |
NSDateで得られた日時を特定のフォーマットやローカライズに対応させて出力したいときは、
NSDateFormatterを使う。
主なフォーマット文字列は以下の通り。詳細は
こちら。
1文字だけ書いているが、文字を繰り返すことで、その桁数になる、もしくは表記が変わる(「例」HH=00,01~23,H=0,1~23)。
大文字小文字も意味が違う。
主なフォーマット文字列 | 内容 |
y | 年 |
M | 月(01-12) |
d | 日(01-31) |
H | 時間(00-23) |
h | 時間(00-11) |
m | 分(00-59) |
s | 秒(00-59) |
w | 年内の週 |
W | 月内の週 |
ZZZZ | GMTからの時差(HHmm;例+0900) |
zzzz | タイムゾーン名 |
NSDateFormatter *formatter = [NSDateFormatter alloc];
[formatter setLocale:[NSLocale currentLocale]];
[formatter setDateFormat:@"HH:mm"];
NSDate *date = [NSDate date];
NSString* dateStr = [formatter stringFromDate:date];
ここで重要なのは、setLocale:でロケール=地域を設定することである。
setDateFormat前にこれをしておかないと、正しい文字列が返らないらしい。
現在設定されているロケールは[NSLocale currentLocale]で取得できるので、そのままsetLocaleに指定する。
日本語の場合はsetLocale:ja_JPらしい。
NSDateFormatter
メソッド名 | 動作 |
-(void)setLocale:(NSLocale *)locale | レシーバーのロケールを設定する |
-(NSLocale *)locale | レシーバーのロケールを返す |
-(void)setDateFormat:(NSString *)string | 日付フォーマットを設定する |
-(NSString *)dateFormat | フォーマット形式の文字列を返す |
-(NSString *)stringFromDate:(NSDate *)date | 日時文字列を返す |
-(NSDate *)dateFromString:(NSString *)string | 文字列からNSDateを返す |
-(void)setShortMonthSymbols:(NSArray *)array | 省略形月表記を設定する |
-(NSArray *)shortMonthSymbols | 省略形月表記を取得する |
-(void)setMonthSymbols:(NSArray *)array | 月表記を設定する |
-(NSArray *)monthSymbols | 月記号を取得する |
-(void)setShortWeekdaySymbols:(NSArray *)array | 省略形曜日表記を設定する |
-(NSArray *)shortWeekdaySymbols | 省略形曜日表記を取得する |
-(void)setWeekdaySymbols:(NSArray *)array | 曜日表記を設定する |
-(NSArray *)weekdaySymbols | 曜日表記を取得する |
-(void)setAMSymbol:(NSString *)string | AM記号を設定する |
-(NSString *)AMSymbol | AMシンボルを取得する |
-(void)setPMSymbol:(NSString *)string | PM記号を設定する |
-(NSString *)PMSymbol | PMシンボルを取得する |
-(void)setCalendar:(NSCalendar *)calendar | カレンダーを設定する |
-(NSCalendar *)calendar | レシーバーのカレンダーを取得する |
-(void)setTimeZone:(NSTimeZone *)tz | タイムゾーンを設定する |
-(NSTimeZone *)timeZone | タイムゾーンを取得する |
NSDateFormatterはスレッドセーフではないらしい。内部に静的ワークを持っているからだと思われる。
マルチスレッドプログラムするときは要注意である。
NSLocale
メソッド名 | 動作 |
+(id)currentLocale | 現在のユーザーのロケールを返す |
+(id)systemLocale | システムロケールを返す |
+(NSArray *)ISOCountryCodes | ISO国コード を返す |
+(NSArray *)ISOCurrencyCodes | ISO通貨コードを返す |
+(NSArray *)ISOLanguageCodes | ISO言語コードを返す |
タイムゾーンまでごねごねしたいときは、
NSTimeZoneまで持ち出す。
基本的にはsystemTimeZoneで取得してNSDateFormatter setTimeZone:に与えるだけでよい。
NSTimeZone
メソッド名 | 動作 |
+(NSTimeZone *)systemTimeZone | システムによって現在使われているタイムゾーンを返す |
+(id)timeZoneWithAbbreviation:(NSString *)abbreviation | 省略形からタイムゾーンを作る(日本標準時の省略形は"JST") |
+(id)timeZoneForSecondsFromGMT:(NSInteger)seconds | グリニッジ標準時からの秒差でタイムゾーンを作る |
-(NSString *)abbreviation | レシーバのタイムゾーンの省略形(日本標準時ならJST)を返す |
-(NSString *)abbreviationForDate:(NSDate *)aDate | 指定した日付でタイムゾーンの省略形を返す |
-(NSString *)name | タイムゾーンの地域名を返す |
-(NSInteger)secondsFromGMT | レシーバーのタイムゾーンとグリニッジ標準時との間隔を秒で返す |
-(NSInteger)secondsFromGMTForDate:(NSDate *)aDate | 指定した日付でタイムゾーンとグリニッジ標準時との間隔を秒で返す |
-(BOOL)isDaylightSavingTime | 現在はサマータイム(DST)中かを返す |
-(BOOL)isDaylightSavingTimeForDate:(NSDate *)aDate | 指定した日付はサマータイム(DST)中かを返す |
-(NSData *)dadata | レシーバのタイムゾーンのデータを返す |
-(BOOL)isEqualToTimeZone:(NSTimeZone *)aTimeZone | タイムゾーンを比較して同じかを返す |
-(NSString *)description | タイムゾーンを表す文字列を返す |
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
NSTimeZone* aTimeZone = [[NSTimeZone systemTimeZone] retain];
[dateFormatter setTimeZone:aTimeZone];
[aTimeZone release];
[dateFormatter setLocale:[NSLocale systemLocale]];
[dateFormatter setDateStyle:NSDateFormatterShortStyle];
[dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
NSCalendarは日時情報を操作するクラスである。
NSCalendar
メソッド名 | 動作 |
+(id)autoupdatingCurrentCalendar | ユーザー設定の変更に同期するカレンダーを返す |
+(id)currentCalendar | ロケールに従いカレンダーを返す(システムカレンダー設定には同期しない) |
-(NSString *)calendarIdentifier | カレンダーIDを返す |
-(NSDate *)dateByAddingComponents:(NSDateComponents *)comps
toDate:(NSDate *)date options:(NSUInteger)opts | 引数分加算したNSDateを作成する |
-(NSDate *)dateFromComponents:(NSDateComponents *)comps | 引数から絶対日付のNSDateを作成する |
-(NSUInteger)firstWeekday | レシーバーの最初の曜日(weekday)番号を返す |
-(id)initWithCalendarIdentifier:(NSString *)string | 与えられたIDで新しいカレンダーオブジェクトを割り付ける |
-(NSRange)maximumRangeOfUnit:(NSCalendarUnit)unit | 指定日付要素の最大値を得る |
-(NSRange)minimumRangeOfUnit:(NSCalendarUnit)unit | 指定日付要素の最小値を得る
NSCalendarUnit(enum値)
NSEraCalendarUnit | 時代単位(int) |
NSYearCalendarUnit | 年単位(int) |
NSMonthCalendarUnit | 月単位(int) |
NSDayCalendarUnit | 日単位(int) |
NSHourCalendarUnit | 時単位(int) |
NSMinuteCalendarUnit | 分単位(int) |
NSSecondCalendarUnit | 秒単位(double) |
NSWeekCalendarUnit | 週単位(int) |
NSWeekdayCalendarUnit | 曜日単位(int) |
NSWeekdayOrdinalCalendarUnit | 週序数単位(int) |
NSQuarterCalendarUnit | 四半期単位(int) |
|
-(void)setFirstWeekday:(NSUInteger)weekday | レシーバーの最初の曜日番号を設定する |
-(NSUInteger)minimumDaysInFirstWeek | レシーバーの最初の週の最小日付を得る |
-(void)setMinimumDaysInFirstWeek:(NSUInteger)mdw | レシーバーの最初の週の最小日付を設定する |
-(NSLocale *)locale | ロケールを返す |
-(void)setLocale:(NSLocale *)locale | ロケールを設定する |
-(NSTimeZone *)timeZone | タイムゾーンを返す |
-(void)setTimeZone:(NSTimeZone *)tz | タイムゾーンを設定する |
NSDate *currentDate = [NSDate date];
NSDateComponents *comps = [[NSDateComponents alloc] init];
[comps setMonth:2];
[comps setDay:3];
NSDate *date = [gregorian dateByAddingComponents:comps toDate:currentDate options:0];
[comps release];
NSDateComponents は日時情報そのものを持つクラスである。指定日時からNSDateを直接作ることは出来ず、必ずこのクラスを経由する。
NSDateComponents
メソッド名 | 動作 |
-(NSCalendar *)calendar | レシーバーのカレンダーを得る |
-(NSDate *)date | 日付を得る |
-(NSInteger)year | 年を得る |
-(NSInteger)month | 月を得る |
-(NSInteger)day | 日を得る |
-(NSInteger)weekday | 曜日を得る(1=日~7) |
-(NSInteger)week | 週を得る(解釈はカレンダーによって異なる) |
-(NSInteger)weekdayOrdinal | 週序数を得る(月の第week週の第weekdayOrdinal日) |
-(NSInteger)era | 時代単位を得る |
-(NSInteger)hour | 時を得る |
-(NSInteger)minute | 分を得る |
-(NSInteger)second | 秒を得る |
-(NSInteger)quarter | 四半期を得る |
-(NSTimeZone *)timeZone | タイムゾーンを得る |
-(void)setCalendar:(NSCalendar *)cal | カレンダーを設定する |
-(void)setYear:(NSInteger)v | 年を設定する |
-(void)setMonth:(NSInteger)v | 月を設定する |
-(void)setDay:(NSInteger)v | 日を設定する |
-(void)setWeekday:(NSInteger)v | 曜日を設定する(1~7) |
-(void)setWeek:(NSInteger)v | 週を設定する |
-(void)setWeekdayOrdinal:(NSInteger)v | 週序数を設定する |
-(void)setEra:(NSInteger)v | 時代単位を設定する |
-(void)setHour:(NSInteger)v | 時を設定する |
-(void)setMinute:(NSInteger)v | 分を設定する |
-(void)setSecond:(NSInteger)v | 秒を設定する |
-(void)setQuarter:(NSInteger)v | 四半期を設定する |
-(void)setTimeZone:(NSTimeZone *)tz | タイムゾーンを設定する |
// 年月日、時分秒を指定して日時オブジェクトを生成する
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *comps = [[NSDateComponents alloc] init];
[comps setYear:2011];
[comps setMonth:3];
[comps setDay:2];
[comps setHour:14];
[comps setMinute:45];
[comps setSecond:0];
NSDate *date = [calendar dateFromComponents:comps];
NSLog(@"日時=%@", date); // 2011-03-02 14:45:00 +0900
[comps release];
タイマー;NSTimer/NSTimeInterval/NSInvocation/NSMethodSignature
NSTimerはμITRONでいうところの「周期起動」を実現するクラスである。
一定時間毎に処理したい事がある場合はこれを使う。
一番簡単な例は「時計を表示する」とか。
もちろん周期ではなく、一発だけ起動もできる。
タイマー設定の仕方はいくつかある。
NSTimer
メソッド名 | 動作 |
+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
|
秒単位で周期起動を設定する。ターゲットはCurrentRunloop(後述)で実行される。 |
+(NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
|
秒単位で周期起動を設定する。
NSRunLoopのaddTimer:でRunLoopに追加しないと有効にならないので注意。
|
+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds invocation:(NSInvocation *)invocation repeats:(BOOL)repeats |
秒単位で周期起動を設定する。ターゲットはCurrentRunloop(後述)で実行される。
NSInvocationクラスで実行オブジェクトを指定する
|
+(NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds invocation:(NSInvocation *)invocation repeats:(BOOL)repeats |
秒単位で周期起動を設定する。
NSInvocationクラスで実行オブジェクトを指定する。
NSRunLoopのaddTimer:でRunLoopに追加しないと有効にならないので注意。
|
-(id)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats |
指定日時以降に周期的に起動するタイマーを設定
NSRunLoopのaddTimer:でRunLoopに追加しないと有効にならないので注意。
|
-(void)fire | オブジェクトに起動メッセージを送る(強制起動) |
-(void)invalidate | タイマーを停止する |
-(BOOL)isValid | タイマーが現在有効かどうか返す |
-(NSDate *)fireDate | タイマーが最後に呼びだされる時刻を返す |
-(void)setFireDate:(NSDate *)date | タイマーが起動する日付・時間を設定する |
-(NSTimeInterval)timeInterval | タイマーが呼び出される時間間隔を返す |
-(id)userInfo | 追加情報を返す |
主な引数を設定する値は以下の通り。
引数名 | 内容 |
seconds | 周期指定:整数位=秒単位,少数位ms単位 |
target | タイマー起動時にaSelectorのメッセージを送るオブジェクトを指定
このオブジェクトは、タイマーが解放されるまで保持される
|
aSelector | タイマーの発動時にtargetに送るメッセージ
- (void)timerFireMethod:(NSTimer *)theTimer
このメソッドの引数としてタイマーは自分自身を渡す
|
userInfo | タイマーがaSelectorのメソッドに渡すユーザ情報を指定
このオブジェクトは、タイマーが解放されるまで保持される
nil=指定なし
|
repeats | YES=繰り返す、NO=一発起動のみ |
NSTimeIntervalは1/1000秒=1ms単位で時間を指定する型である(クラスではない)。
測定開始時
NSDate *startTime = [NSDate date];
経過時間取得時
NSTimeInterval elapsedTime = [startTime timeIntervalSinceNow]; // 現在の時刻とstartTimeの時間差を算出
NSString* str = [NSString stringWithFormat:@"経過時間:%f", -elapsedTime]; // 現在時間の方が後なので≦0になるので-している
NSInvocationは、起動するオブジェクトを指定するためのクラスである。
NSTimerで起動するオブジェクトを保持するが、この内容を変更することで、
同じタイマーで異なるオブジェクトの起動が可能になる。
初期状態では引数や返値が保持されないので、必要ならretainArgumentsで保持指定をしておく。
NSInvocation
メソッド名 | 動作 |
-(void)setTarget:(id)anObject | 起動オブジェクトにターゲットを設定する |
-(void)setSelector:(SEL)selector | 起動オブジェクトにセレクタを設定する |
+(NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)signature | 起動オブジェクトを作る |
-(id)target | 起動オブジェクトのターゲットを返す |
-(SEL)selector | 起動オブジェクトにセットされているセレクタを返す |
-(BOOL)argumentsRetained | 起動オブジェクトは引数を保持するかを返す |
-(void)retainArguments | 起動オブジェクトが引数を保持するように設定する |
-(void)invoke | 起動オブジェクトにセットされているターゲットにメッセージを送信する |
-(void)invokeWithTarget:(id)anObject | セットされているターゲットと別のターゲットにメッセージを送信する |
-(NSMethodSignature *)methodSignature | 起動オブジェクトのメソッドシグネチャを返す |
-(void)setArgument:(void *)buffer atIndex:(NSInteger)index | 番号で指定した引数に値をコピーする(arg[atIndex]=値) |
-(void)getArgument:(void *)buffer atIndex:(NSInteger)index | 番号で指定した引数の値をコピーする(arg[atIndex]の値) |
-(void)setReturnValue:(void *)buffer | 起動オブジェクトの戻り値をバッファに入れる |
-(void)getReturnValue:(void *)buffer | 起動オブジェクトを起動した後のリターン値をバッファに入れる |
@implementation MyObject
NSTimer *timer= nil ; // クラス内グローバル変数
-(void) timerControl
{
// タイマー発動時に呼び出されるメソッド
~
}
- (void)startTimer
// タイマーを作成してスタート
{
// メソッドシグネチャ
NSMethodSignature* aSignature;
// 起動オブジェクト
NSInvocation *invocation;
// セレクタ
SEL aSelector = @selector(timerControl);
// メソッドシグネチャ
aSignature = [self methodSignatureForSelector:aSelector ];
// 起動オブジェクトを作る
invocation = [ NSInvocation invocationWithMethodSignature:aSignature ];
// ターゲットをセット
[ invocation setTarget: self ];
// セレクタをセット
[ invocation setSelector: aSelector ];
// タイマースタート
timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 invocation:invocation repeats: YES ];
}
@end
NSMethodSignatureは・・・よくわからん。
タイマー指定時に上記のように使うことだけ覚えておけばいいのだろう。当面は。
「frameLength」などはなかなか興味深いメソッドではある。
組み込み機器ではよくスタックオーバーで「わけの解らんバグ」が出たからなぁ。
NSMethodSignature
メソッド名 | 動作 |
-(NSUInteger)frameLength | 引数がスタックの上で占めるバイト数を返す |
-(const char *)getArgumentTypeAtIndex:(NSUInteger)index | メソッドの番号で指定した引数のタイプを返す |
-(NSUInteger)numberOfArguments | メソッドシグネチャが持つ引数の数を返す |
-(NSUInteger)methodReturnLength | 戻り値のために必要なバイト数を返す |
-(const char *)methodReturnType | メソッドの戻り値のタイプを表しているC文字列を返す |
並行動作;NSRunLoop
NSRunLoopはマウスやキーボードなどのイベント取得処理、NSTimerのタイマー待ち、ファイル入出力やNSURLDownloadなどに
おける並行動作指示時の実動作を行う。
一般的に各スレッドで自動的にNSRunLoopオブジェクトが作成されるらしい。
NSRunLoop
メソッド名 | 動作 |
+(NSRunLoop *)currentRunLoop | 現在のスレッドの実行ループオブジェクトを返す |
+(NSRunLoop *)mainRunLoop | メインスレッドの実行ループオブジェクトを返す |
-(void)run | 即時、実行ループを実行 |
-(void)runUntilDate:(NSDate *)limitDate | 期限まで実行ループを実行する |
-(void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode | 実行ループにタイマーを加える |
-(void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(id)anArgument |
指定したアクションセレクタ、ターゲット、引数のアクションを中止する |
-(void)cancelPerformSelectorsWithTarget:(id)target |
指定したターゲットのアクションを中止する |
-(void)performSelector:(SEL)aSelector target:(id)target argument:(id)anArgument order:(NSUInteger)order modes:(NSArray *)modes |
カレントスレッドでメソッドを実行する |
-(NSString *)currentMode | 現在のモードを返す |
-(NSDate *)limitDateForMode:(NSString *)mode | 次にタイマーが実行される時間を返す |
-(BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate | 期限付きループを実行 |
-(void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate | 指定時間まで入力を受け付けないようにする |
上記メソッドの引数「forMode」には以下の定数を与える。
定数名 | 動作 | 定義ヘッダー |
NSDefaultRunLoopMode | NSConnection以外の入力ソースに対する処理を行う;メニュー起動時などは無視される | NSRunLoop.h |
NSRunLoopCommonModes | メニュー実行中なども起動される | NSRunLoop.h |
NSEventTrackingRunLoopMode | イベント監視ループ | NSApplication.h |
@implementation MyObject
NSTimer *timer;
-(void)timerControl
// タイマーで起動される処理
{
int i;
i = [myOutlet intValue] +1 ;
[myOutlet setIntValue:i];
[myOutlet display];
}
- (void)myAction:(id)sender
{
timer = [NSTimer
timerWithTimeInterval: 1.0f // 1秒単位
target: self
selector: @selector(timerControl)
userInfo: nil
repeats: YES
];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSEventTrackingRunLoopMode];
}
- (void)fire:(id)sender
{
~
}
- (void)stop:(id)sender
{
~
}
通知;NSNotification/NSNotificationCenter
NSNotificationクラスは、通知の仕組みを使用する時、その通知する内容を保持するクラスである。
NSNotification
メソッド名 | 動作 |
+(id)notificationWithName:(NSString *)aName object:(id)anObject | 新規通知を作成する |
+(id)notificationWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)userInfo | ユーザー情報付きで新規通知を作成する
|
-(NSString *)name | 通知の名前を返す |
-(id)object | 通知に割り付けられたオブジェクトを返す |
-(NSDictionary *)userInfo | レシーバーに割り付けられたユーザー情報の辞書を返す |
一方、NSNotificationCenterは、通知という仕組みそのものを使えるようにするクラスである。
これを使うと、あるイベントが発生した時に、同じアプリ内の別クラスでそのイベントを拾いたいうということが
出来る様になる。
NSNotificationCenter
メソッド名 | 動作 |
+(id)defaultCenter | プロセスのデフォルトの通知センターを返す |
-(void)addObserver:(id)notificationObserver selector:(SEL)notificationSelector name:(NSString *)notificationName object:(id)notificationSender |
レシーバーの電報(dispatch)テーブルに通知者を受けるクラス(オブザーバー)を追加する
|
-(void)postNotification:(NSNotification *)notification | レシーバーに通知を送る |
-(void)postNotificationName:(NSString *)notificationName object:(id)notificationSender |
通知を新規作成する
|
-(void)postNotificationName:(NSString *)notificationName object:(id)notificationSender userInfo:(NSDictionary *)userInfo |
ユーザー情報付きで通知を新規作成する
|
-(void)removeObserver:(id)notificationObserver |
レシーバーの電報(dispatch)テーブルからnotificationObserverのオブザーバーを指定しているエントリを全削除する
|
-(void)removeObserver:(id)notificationObserver name:(NSString *)notificationName object:(id)notificationSender |
レシーバーの電報(dispatch)テーブルから一致するエントリを削除する
|
NortificationとKVOはできることが似ているが、
基本的にはKVOはユーザーが使うもの、Notificationはシステムが提供するものという違いがある感じ。
delegateもちょっと似てるが、UIKitの中にはNotificationを使って変更を知らせてくるものもあるので、
そのときはそれに従う必要があると言うことである。
//----------------------------------------------
// キーボード表示/消去通知
//----------------------------------------------
-(void)keyboardNotification:(BOOL)onoff
// キーボード表示通知の設定
{
if (onoff) { // ON
NSLog(@"keyboardNotification on");
// キーボード表示時に通知させる
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
// キーボード消去時に通知させる
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
//
} else { // OFF
NSLog(@"keyboardNotification off");
// キーボード通知の解除
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
// キーボード消去時に通知させる
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil];
}
}
-(void)keyboardWillShow:(NSNotification *)notification
{
if (keyboardFrameEnd.size.height!=0) {
NSLog(@"すでに表示中なので何もしない");
// なぜかキーボードがすでに表示されているにもかかわらず再度通知がかかることがあるので
return;
}
// キーボードの表示完了時の場所と大きさを取得
keyboardFrameEnd = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
// iOSバグ:横画面において、キーボードwidthとheightが逆になってる(iPadで確認、iPhoneは不明)
// deviceOrientationではシミュレーター上では方向が得られないので、簡易的に幅の大小で判定している
CGFloat w=keyboardFrameEnd.size.width;
if (w<keyboardFrameEnd.size.height) {
NSLog(@"横画面なのでwidth←→heightを入れ替える");
keyboardFrameEnd.size.width=keyboardFrameEnd.size.height;
keyboardFrameEnd.size.height=w;
}
}
-(void)keyboardDidHide:(NSNotification *)notification
{
~
}
エラー処理;NSError
ファイルやURLの処理を始めると必ず必要になるのがエラー処理。
基本的に、内蔵であれ外部であれデバイスにアクセスするときは必ずエラー処理を書かなければならない。
メッセージを出して処理を止めるにしても、回復・回避処理をするにしてもである。
しかし、それをOS標準のものにまかせたり、完全に抜けているプログラムがいかに多いことか。
組み込み機器では、エラーの処理の出来で相手の技量が分かってしまう。
特に台湾や韓国の安物系ではこれがいい加減なものが多くて、あとで色々困るハメになることが多い。
iOSでは非常に詳細なログを取られているので必要ないかもしれないが、エラー発生時の情報収集も
エラー処理の重要な役割である。何がどうなってエラーが発生したか。
それが追えれば、対策はかなり楽になることもある。
はっきり言えば「ユーザーや営業の言葉ほどアテにならないことはない」ので、
機械に状況を記録させて、それを回収して解析するのが一番なのである。
余談はともかく、Cocoa Touchでは、絶対にエラーが発生する可能性があるクラスのため、
その情報を返すためのエラー専用のクラスをもうけている。
NSError
メソッド名 | 動作 |
+(id)errorWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)dict | エラーオブジェクトを作る |
-(id)initWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)dict | エラーオブジェクトを初期化して返す |
-(NSInteger)code | エラーコードを返す |
-(NSString *)domain | ドメインを返す |
-(NSDictionary *)userInfo | 追加情報を返す |
-(NSString *)localizedDescription | エラーを説明する文字列を返す |
まあ、一応このようなメソッドが存在するが、これらを使うのは自前でエラーを書くときだけである。
標準で用意されているクラスを使うときは、返ってくるエラー情報を得るために、引数として与えるのがほとんどだろう。
NSErrorはこれだけで使うことはない。エラー処理を指定できるメソッドに引数として渡すことになる。
BOOL fileCopy(NSString *src,NSString *des)
{
NSLog(@"fileCopy:%@ → %@",src,des);
NSFileManager *fp=[NSFileManager defaultManager];
NSError *error;
BOOL ret=[fp copyItemAtPath:src toPath:des error:&error];
if (!ret) {
NSLog(@"error=%@",error);
}
return ret;
}
この例ではまずリターン値でエラーの発生を調べ、発生しているときのみerrorを読み出している。
引数として渡したときのNSErrorは、エラー発生時出ないと内容が設定されないことがある。
従って、errorがnilかどうかでエラーを判定してはいけない。
if (error!=nil) {
NSLog(@"error=%@",error);
}
と書いてはいけないと言うことだ。
もしそう書きたければ、
NSError *error = nil; // これが重要
BOOL ret=[fp copyItemAtPath:src toPath:des error:&error];
if (error!=nil) {
NSLog(@"error=%@",error);
}
としなければならない。
例外処理;NSException
「例外処理」とは、通常の処理を強制的に打ち切って別処理へ飛ばす機能だと思えばよい。
基本的には例外に飛んだ後元の処理に戻すことも出来る。
昔のMS-BASICのon error goto/error/resumeに近いし(X-BASICには存在しない)、
68000CPUにおけるTRAP命令に近いと思えばよい。
NSExceptionは例外を管理するクラスである。
NSException
メソッド名 | 動作 |
+(NSException *)exceptionWithName:(NSString *)name reason:(NSString *)reason userInfo:(NSDictionary *)userInfo | 例外を作る |
-(void)raise | 例外を発生させる |
+(void)raise:(NSString *)name format:(NSString *)format, ... |
フォーマット付き文字列情報を持って例外を発生させる
|
-(id)initWithName:(NSString *)name reason:(NSString *)reason userInfo:(NSDictionary *)userInfo | 例外を初期化する |
-(NSString *)name | 例外名を返す |
-(NSString *)reason | 例外の理由を返す |
-(NSDictionary *)userInfo | 例外の追加情報を返す |
例外の強制発生は下のように行う。
- (void)foo
{
~
if (result == -1) {
[[NSException exceptionWithName:@"FooException" // 例外名
reason:@"Result returned -1.." // その理由
userInfo:nil] // 情報
raise];
}
}
または
{
NSException *matrixException;
matrixException = [[NSException alloc]
initWithName:@"Exception"
reason:@"reason"
userInfo:nil
];
[matrixException raise];
}
例外を受ける側は
「Objective-Cの例外処理」で書いたとおりである。
- (void)bar
{
@try {
[self foo]; // ここで例外が発生したら
}
@catch (NSException *exception) { // ここで受ける
NSLog(@"main:Caught %@:%@", [exception name], [exception reason]);
// localExceptionというCの文字列変数(char*)にも例外名が代入されている
printf("%s\n", [[localException name] cString]);
// 別の例外処理に投げるとき
// @throw localException;
}
@finally { // 例外がなくてもここは実行される
printf("Here is normal.\n");
}
もし、bar内で例外を@catchしないと、barを呼び出したメソッドに例外が渡る。
例外処理中にメソッドを中断してしまいたい=そこで呼び出し側に戻りたいときは、以下のマクロを使う。
マクロ名 | 動作 |
NS_VOIDRETURN | リターン値なしで戻る |
NS_VALUERETURN() | リターン値付きで戻る(引数,型) |
これらはマクロなので、プリプロセッサによって実際のコードに展開されている。
@catch (NSException *exception) { // ここで受ける
~
NS_VALUERETURN(@"Error", NSString *);
// NS_VALUERETURN(-1, int);
}
スレッドの途中で捕捉されなかった例外は、最終的に実行環境に用意されたデフォルトの例外ハンドラがこれを捕まえて、
メッセージを出す。通常のアプリケーションではその後、イベントループに戻るのでそのまま動くことが多いと考えられるが、
基本的にこの状態はバグなので好ましくない。
このデフォルトのハンドラを別なものと置き換えるための、関数が用意されている。
void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler *handler);
void MyUncaughtExceptionHandler(NSException *exception)
// 自前の例外処理
{
#if 0 // 超簡単
printf("uncaught %s\n", [[exception name] cString]);
#else // スタック内容を表示
NSArray *callStackArray = [exception callStackReturnAddresses];
int frameCount = [callStackArray count];
void *backtraceFrames[frameCount];
for (int i=0; i<frameCount; i++) {
backtraceFrames[i] = (void *)[[callStackArray objectAtIndex:i] unsignedIntegerValue];
}
#endif
}
int main()
{
~
NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
~
}
なお、この方法で処理(受信)できるのはあくまで例外だけであって、signalは受信できない。
signalまで処理したいときは以下のように、発生しうる全てのsignalに対してトラップ処理を書くしかない。
(もちろんシグナル名を配列にして処理を短くすることは出来るけど、意味は同じ。)
#include <signal.h>
void mysighandler(int sig, siginfo_t *info, void *context)
{
void *backtraceFrames[128];
// backtrace()はMacOSのUnix部分の関数
int frameCount = backtrace(backtraceFrames, 128);
// report the error
}
int main(int argc, char *argv[])
{
struct sigaction mySigAction; // 関数と同名だが、こちらは構造体(Objective-Cの命名規則参照)
mySigAction.sa_sigaction = mysighandler;
mySigAction.sa_flags = SA_SIGINFO;
//
sigemptyset(&mySigAction.sa_mask);
sigaction(SIGQUIT, &mySigAction, NULL); // こちらは関数
sigaction(SIGILL , &mySigAction, NULL);
sigaction(SIGTRAP, &mySigAction, NULL);
sigaction(SIGABRT, &mySigAction, NULL);
sigaction(SIGEMT , &mySigAction, NULL);
sigaction(SIGFPE , &mySigAction, NULL);
sigaction(SIGBUS , &mySigAction, NULL);
sigaction(SIGSEGV, &mySigAction, NULL);
sigaction(SIGSYS , &mySigAction, NULL);
sigaction(SIGPIPE, &mySigAction, NULL);
sigaction(SIGALRM, &mySigAction, NULL);
sigaction(SIGXCPU, &mySigAction, NULL);
sigaction(SIGXFSZ, &mySigAction, NULL);
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release];
return (retVal);
}
Xcodeで例外のデバッグを行う方法。
XcodeのBreak Pointを表示した状態で、下の「+」を押して「Add Exception Breakpoint」を選択すると、
例外発生をXcodeでとらえることができる。
これを設定して実行すると、意外な所で例外が発生していることに気がつくこともある。
多くの例外は実行時にアプリを落とす原因となるが、中にはOSが良きに計らってくれるのか、
無視されているものもあるようである。