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");

2011年7月12日火曜日

UITableView のセクションヘッダー

UITableView のセクションヘッダーに関するメソッドは以下の3つ
  • – tableView:titleForHeaderInSection:
  • – tableView:viewForHeaderInSection:
  • – tableView:heightForHeaderInSection:
タイトルを変更するだけならtableView:titleForHeaderInSection: を実装すればよく、独自の View を表示したいなら tableView:viewForHeaderInSection: でヘッダーの高さを返し、tableView:heightForHeaderInSection: で表示したい UIView を返す。これで TableView のスクロールに追従する Viewを挿入できる。


2011年7月5日火曜日

GameCenter 以前の GameKit

GameKit はもともと iOS 3.0 の頃から実装されていた。この頃は Peer To Peer とボイスチャットのみを扱う物であった。サーバが提供されていなかったため、LAN または bluetooth 上のデバイスしか検出できず(サーバたてれば別とは思う)、当時の利用方法としては Game よりも目の前の相手へのデータの転送などに使われていたのではないでしょうか。これが iOS 4.1 になって GameCenter が追加され Game アプリを作成する上で非常に大きな存在となった。ちなみに、4.0 にも API は存在するけど 4.1 で大幅な変更が加えられたため利用できない。






Peer to Peer

GKSession


GKSession *session = [[GKSession alloc] initWithSessionID:@"hoge" displayName:nil sessionMode:GKSessionModePeer];
session.delegate = self; 
[session setDataReceiveHandler:self withContext:nil]; 
session.available = YES;
Delegate は以下の通り。available = YES すると相手の session:peer:didChangeState が呼び出される。
  • session:peer:didChangeState:
  • session:didReceiveConnectionRequestFromPeer:
  • session:connectionWithPeerFailed:withError:
  • session:didFailWithError:
setDataReceiveHandler:withContext: を実行しているので以下のメソッドも定義する必要がある。
  • receiveData:fromPeer:inSession:
相手の Peer ID が分かれば connectToPeer:withTimeout:、 acceptConnectionFromPeer:error: の流れで接続する。 相手先にデータを送るには sendDataToAllPeers:withDataMode:error: または sendData:toPeers:withDataMode:error: を使う。 ここでいくつか注意をドキュメントから抜粋。
  1. 一度に送れるデータのサイズは 87KB 以下。
  2. 1KB以下であると良好なパフォーマンスが得られる。
  3. モードはGKSendDataReliable なら受信が成功するまで繰り返し、GKSendDataUnreliable なら一度だけ送る。
    GKSendDataReliable でも大きなデータ(画像など)を送っているとデータの欠落などが発生しエラーも発生しないみたい(未確認)なので、うまくいかない場合は自分で送受信でチェックを入れる

サンプル GKRocket にある SessionManager クラスを使うといろいろとやってくれて便利。




GKPeerPickerController

次のコードだけで Bluetooth による Peer to Peer ネットワークを作成してくれる。デフォルトでは Bluetooth のみ対応だけど、connectionTypesMask を設定すれば LAN も探してくれる。

GKPeerPickerController*		picker;  picker = [[GKPeerPickerController alloc] init]; 
picker.delegate = self; [picker show]; 
あとはデリゲートで対応する。peerPickerController:sessionForConnectionType: で GKSession 作って返す必要があるからそれだけ注意。
  • peerPickerController:sessionForConnectionType:
  • peerPickerController:didConnectPeer:toSession:
  • peerPickerControllerDidCancel:
GKPeerPicker を使うと Bluetooth を ON にするダイアログを表示してくれるし、connect と accept も自動でやってくれるので楽チン。





Voice Chat

GKVoiceChatService


iOS4.1 からの GKVoiceChat と混同しない。GKVoiceChat もそうであるが、3G 環境などの Wi-Fi 以外の環境では動作しない。

setup

マイクとスピーカーを使うので AudioSession を扱う必要がある。AudioSession とは Device のマイクやスピーカーの状態や挙動を管理してくれる物で、サイレントモードで音を再生するかどうかを設定したりできる。
AudioSession には C の関数の他に AVFoundation クラスが用意されているの好きな方を使えば良い。
NSError *myErr;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setPreferredHardwareSampleRate:8000.0 error:&myErr];
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&myErr];
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&myErr];

[GKVoiceChatService defaultVoiceChatService].client = self; //GKChatServiceClient protocol
[[GKVoiceChatService defaultVoiceChatService] setInputMeteringEnabled:YES]; 
[[GKVoiceChatService defaultVoiceChatService] setOutputMeteringEnabled:YES];
client は (NSString*)participantID 定義してある必要がある。これによってチャット相手を識別するのだけど、GKSession と併せて使うことになるだろうから peerID を用いれば良い。
- (NSString *)participantID
{ 
	return self.session.peerID;
}



start chat service

startVoiceChatWithParticipantID:error: メソッドで相手を捜し出し invite メッセージを送ってくれる。
受ける側は voiceChatService:didReceiveInvitationFromParticipantID:callID: を実装して、中で acceptCallID:error: を呼べば良い。

これ以降は音声入力があると voiceChatService:sendData:toParticipantID: が呼ばれるので、中で GKSession の sendData:toPeers:withDataMode:error: で相手にデータを送る。
受ける側は receiveData:fromPeer:inSession:context: 内で GKVoiceChatService の receivedData:fromParticipantID: を呼べばスピーカーから音が出る。

ソースコード書いた方が早かったかな?GKRocket の中の SessionManager クラスがとても参考になる。