2012年1月15日日曜日

CVPixelBuffer を保存する

CMSampleBuffer なんかから得られる CVPixelBuffer を保存したくなったのでしてみた。やってることは NSData を使って生のデータを保存しているだけ。このときに、再利用時に必要となりそうなパラメータをファイル名に埋め込んでいる。
- (BOOL)writeBuffer:(CVPixelBufferRef)imageBuffer
{
    CVPixelBufferLockBaseAddress(imageBuffer, 0);
    NSURL *fileURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDirectory] lastObject];
    int bufferSize = CVPixelBufferGetDataSize(imageBuffer);
    int width = CVPixelBufferGetWidth(imageBuffer);
    int height = CVPixelBufferGetHeight(imageBuffer);
    int bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
 
    NSString *fileName = [NSString stringWithFormat:@"buffer_%dx%d_%d_%d", width, height, bytesPerRow, bufferSize];
    
    unsigned char *baseAddress = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer);
    NSData *data = [NSData dataWithBytes:baseAddress length:bufferSize];
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    
    NSLog(@"PATH: %@", fileURL);
    if ([data writeToURL:[fileURL URLByAppendingPathComponent:fileName] atomically:YES]) {
        NSLog(@"success");
        return YES;
    }else{
        NSLog(@"fail");
        return NO;
    }
}
保存した生のデータを UIImage にするのはこんな感じ。決めうちで入れている値はファイル名から取得したもの。パースが必要なら NSScaner なり、ファイル名をもうちょっと工夫して -[NSString componentsSeparatedByString:] なりを使うとよさそう。
- (UIImage*)imageWithImageBufferContentsOfURL:(NSURL*)url
    NSData *data = [NSData dataWithContentsOfURL:url];
    if (!data) {
        NSLog(@"error");
        return nil;
    }
    CVPixelBufferRef imageBuffer = (CVPixelBufferRef)[data bytes];

    size_t bufferSize = 128808;
    size_t bytesPerRow = 2560;
    size_t width = 640;
    size_t height = 480;

    static CGColorSpaceRef colorSpace = NULL; if (colorSpace == NULL) {
        colorSpace = CGColorSpaceCreateDeviceRGB(); if (colorSpace == NULL) {
            NSLog(@"error");
            return nil;
        } }

    void *baseAddress = imageBuffer;
    CGDataProviderRef dataProvider =
    CGDataProviderCreateWithData(NULL, baseAddress, bufferSize, NULL);
    CGImageRef cgImage =
    CGImageCreate(width, height, 8, 32, bytesPerRow,
                  colorSpace, kCGImageAlphaNoneSkipFirst |
                  kCGBitmapByteOrder32Little,
                  dataProvider, NULL, true, kCGRenderingIntentDefault);
    CGDataProviderRelease(dataProvider);
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);

    return image
}

2011年12月21日水曜日

Time Machine と Disk Image

Lion に移行した。いろいろ変わっていてなれない部分もあるけど、おおむね気に入っている。今気になるのは CoolBook が使えないことくらいかな。久しぶりに CoolBook なしで生活してみます。

これまでの Time Machine のバックアップを一区切りし、新しくバックアップをとることにした。この時のバックアップ先は sparsebundle ディスクイメージにする。これは後々ネットワーク上のバックアップ先に変更するときに楽だからだ。

ディスクイメージを作成したら、パッケージを開き、com.apple.TimeMachine.MachineID.plist ファイルを作成する。これは以下のようなフォーマット。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>VerificationDate</key>
 <date>2011-12-10T15:41:47Z</date>
 <key>VerificationExtendedSkip</key>
 <false/>
 <key>VerificationState</key>
 <integer>1</integer>
 <key>com.apple.backupd.HostUUID</key>
 <string>xxxxxxxxxxxxxxxxxxxxxxxxxxx</string>
</dict>
</plist>
ここで問題になるのが com.apple.backupd.HostUUID。これは Hardware UUID で、System Information を探せば見つかる。

このディスクイメージをディスクのトップに置いて、Time Machine の「ディスクを選択」でこのディスクを選択すれば Time Machine がうまくやってくれる。

2011年9月2日金曜日

メソッドに機能を追加したい場合

あまりに簡素だったので追記

まえがき

UIView などの既存のクラスにメソッドを追加するにはカテゴリを使うのが一般的(参考:http://news.mynavi.jp/column/objc/008/index.html)。 あるいは全く異なる機能を持たせたいならオーバーライドする。 では、既存のメソッドの機能を残しつつ、そこに独自の機能を追加したい場合はどうするか。一番スマートなのはサブクラスを作成して対処する方法。じゃあ、「サブクラス」なんか作りたくない、という時はどうするか。Delegate などで対処できないかよく考慮した上で、ダメなら自分で作ったメソッドと置き換えてしまおう。

runtime.h

runtime.h ヘッダーにはmethod_exchangeImplementationsというメソッドを置き換える関数が用意されている。 これとカテゴリによるメソッドの追加を組み合わせればメソッドに独自の機能を追加することができる。 - [UITableView reloadData] を reloadData2 で置き換えてみよう。まずは reloadData2 を用意する。
- (void)reloadData2
{
    [self reloadData2];
    SEL selector = @selector(tableViewDidEndReloadingData:);
    if( self.delegate && [self.delegate respondsToSelector:selector] ){
        [self.delegate performSelector:selector withObject:self];
    }
}
@end
ここで、reloadData2 の中で reloadData2 を呼び出していることに注意。メソッドが置き換わると - [UITableView reloadData] の呼び出しで上記の reloadData2 が呼び出される。この時、- [UITableView reloadData2] で呼び出されるのはオリジナルの reloadData だ。今回は元々の reloadData を呼び出した後に delegate に対して tableViewDidEndReloadingData: メソッドを呼び出すようにしている。 追加するメソッドが用意できたら次はいよいよ置き換えだ。
#import <objc/runtime.h>
static void UITableViewReplaceReloadDataMethod(void)
{
    Method original = class_getInstanceMethod([UITableView class], @selector(reloadData));
    Method replace = class_getInstanceMethod([UITableView class], @selector(reloadData2));
    method_exchangeImplementations(original, replace);
}
runtime.h ヘッダーをインポートを忘れないように注意する。コードはいたって簡単だ。これをアプリケーションのどこかで呼び出せば良いのだけど、注意は1度だけ呼び出すということ。なんども呼び出してしまうと、オリジナルの reloadData を呼び出したり、reloadData2 を呼び出したりしてしまう。 -[AppDelegate init] とか -[AppDelegate awakeFromNib] あたりが良いのではないかな。

2011年8月17日水曜日

MacRuby から CoreData

アプリに入れるデータは MacRuby で作っていたりします。Objective-C でも良いんだけど、コンパイルとか面倒だし、テキスト処理は Ruby だと楽チンだし♪
#! /usr/local/bin/macruby
# coding:utf-8
#

framework 'Foundation'
framework 'CoreData'

DB_NAME = "Question"
ENTITY_NAME = "Question"

class DB
  def managedObjectModel
    if (@managedObjectModel != nil)
        return @managedObjectModel
    end
    modelURL = NSURL.URLWithString Dir.pwd+"/#{DB_NAME}.momd"
    @managedObjectModel = NSManagedObjectModel.alloc.initWithContentsOfURL(modelURL)   
    return @managedObjectModel
  end

  def persistentStoreCoordinator
    if (@persistentStoreCoordinator != nil)
        return @persistentStoreCoordinator
    end
    
    storeURL = NSURL.fileURLWithPath Dir.pwd+"/#{DB_NAME}.sqlite"
   
    error = Pointer.new(:object)
    @persistentStoreCoordinator = NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(self.managedObjectModel)
    if (!@persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration:nil, URL:storeURL, options:nil, error:error))
        puts "Unresolved error #{error}, #{error.userInfo}"
        abort()
    end
    
    return @persistentStoreCoordinator
    
  end

  def managedObjectContext
    if (@managedObjectContext != nil)
        return @managedObjectContext
    end
    
    coordinator = self.persistentStoreCoordinator
    if (coordinator != nil)
        @managedObjectContext = NSManagedObjectContext.alloc.init
        @managedObjectContext.setPersistentStoreCoordinator(coordinator)
    end
    return @managedObjectContext
  end



  def saveContext
    error = Pointer.new(:object)
    managedObjectContext = self.managedObjectContext
    if (managedObjectContext != nil)
        if (managedObjectContext.hasChanges || !managedObjectContext.save(error))
            puts "Unresolved error #{error}, #{error.userInfo}"
            abort()
        end
    end
  end
    
end

`/Developer/usr/bin/momc #{DB_NAME}.xcdatamodeld #{DB_NAME}.momd`
if File.exist? "#{DB_NAME}.sqlite"
  File.delete "#{DB_NAME}.sqlite"
end

db = DB.new
p model = db.managedObjectModel

var = {"YEAR" => 2010}
request = model.fetchRequestFromTemplateWithName("QuestionsWithYear", substitutionVariables:var)

p request.predicate.predicateFormat

db.saveContext

Xcode4 の Model Editor

Xcode には CoreData で操作するモデル作成する GUI エディタがある。こんなやつ。
画像の左に "Fetch Requests" という項目があるが、これは名前が示す通り NSFetchRequest を定義しておけるものだ。ソースコードに書くといろいろややこしく、見づらくなってしまうけど、モデルデータに格納しておけば比較的すっきりと書くことができる。
NSURL *modelURL = [NSURL URLWithString:path];
NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
// モデルデータにある Fetch Request の一覧を取得
NSDictionary *requests = [managedObjectModel fetchRequestTemplatesByName];
// 名前を元に特定の Fetch Request を取得
NSFetchRequest *request = [managedObjectModel fetchRequestTemplateForName:@"MyFetchRequest"];
さて、この Fetch Request であるが、"year == 2011" などの定数を元に作成してもあまり恩恵はない。やはり変数を扱える方が嬉しい。Xcode3 までは Editor のポップアップに VARIABLE という項目があり、簡単に利用する事ができた。Xcode4 からはこの選択項目がなくなり、自分で記入しなければいけないので注意が必要。NSPredicate を使った事がある人なら分かると思うが、変数を扱うには $ 記号を使って変数名を指定してあげれば良い。この時、expression を選択しておくのがポイントだ。
このようにして作成した Fetch Request のテンプレートは fetchRequestTemplateForName: ではなく fetchRequestFromTemplateWithName:substitutionVariables: を使って呼び出す。
NSDictionary *variables = [NSDictionary dictionaryWithObject:[NSNumber numberWithInteger:2010] forKey:@"YEAR"];
NSFetchRequest *request = [managedObjectModel fetchRequestFromTemplateWithName:@"QuestionsWithYear" substitutionVariables:variables];

2011年8月16日火曜日

CoreData をちょっと説明する




概要

iOS 上でいくらか面倒な情報を保存、読み出ししなければ行けないような場合、NSUserDefaults や NSKeyedArchiver では直ぐに限界を感じてしまう。ファイルへの読み書きは速度が遅いし、何より特定の項目を検索できないのが辛い。そのような時のために、iOS では CoreData というデータベースを扱えるようになっている。

CoreData の大元

ファイル

CoreData はそのモデルの情報をファイルに格納している。 Xcode 上では .xcdatamodeld というフォーマットで、これは Xcode 上で使うための専用のフォーマットで、iPhone 側に渡す事はない。iPhone 上ではこれをコンパイルして .momd というフォーマットにして渡す。.momd はビルド時に作成されるが、自分で作りたい時は以下のようにしてファイルを作成する。
/Developer/usr/bin/momc Model.xcdatamodeld Model.momd
  
この .momd から .sqlite などの Store が作成されるので、大元の .xcdatamodeld に変更を加えなくて済むように事前によく設計した方が良い。簡単な変更ならバージョン管理機能でほとんどコードをいじらずにデータベースをバージョンアップできる。

クラス

NSManagedObjectModel
上記のモデルデータを扱うためのクラス。
NSPersistentStoreCoordinate
Store つまりDB本体を扱うためのクラス。NSManagedObjectModel をもとに DB を作成する。
NSManagedObjectContext
メモリ上のDBという感じのクラス。データの変更などはここに蓄えられ、アプリの終了時などにまとめて保存する。
データベースとのやり取りにおいて幾層にも抽象化されていて実態が分かりづらいけど、面倒な事はみんな CoreData がやってくれるのでコードに専念できる。
つづく...

2011年8月9日火曜日

テストデータの利用

テストデータを利用するために Bundle に含めるのだけど、[NSBundle mainBundle] だとテスト用の Bundle ではなくてアプリの方の Bundle を参照するのでよろしくない。
こういう時は、[NSBundle bundleForClass:[MyAppTests class]] などして Bundle を指定してあげるのが良さそうだ。
NSBundle *bundle = [NSBundle bundleForClass:[MyAppTests class]];

//Accurateness test
NSString *path = [bundle pathForResource:@"test_data" ofType:@"txt"];
NSString *str = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
STAssertTrue([MyApp myMethod:str], @"myMethod");