トップ 最新 追記

Cocoa練習帳

iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど

2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|

2012-04-01 設立記念日

本日は、Bitz Co., Ltd.の設立記念日。前身の個人事業所から数えると14周年。法人成りしてからは、9周年となります。

これも皆様のご支援のおかげだと思います。ありがとうございます。

起業10年後の生存確率は5%という記事を読んだ事があります。真偽のほどは分かりませんが、何れにしても、事業を継続してゆくのには困難を伴いますので、10周年を迎えられる様に頑張りたいと思っています。

宜しくお願いします。


2012-04-02 [iOS]Looping Recorderの基礎(Audio Unit (4))

Remote IO UnitのRemote Inputで得られるデータについて、調べてみた。

マイク等をつないでいない、素の状態の入力と出力のチャンネル数は、Audio Session Serveiceで得る事ができる。

AudioSessionInitialize(NULL, NULL, NULL, NULL);
UInt32 nChannels = 0;
UInt32 size = sizeof(nChannels);
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
                        &size,
                        &nChannels);
NSLog(@"Input nChannels:%u", (unsigned int)nChannels);
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareOutputNumberChannels,
                        &size,
                        &nChannels);
NSLog(@"Output nChannels:%u", (unsigned int)nChannels);

シミュレータの結果は、以下のとおり。

2012-04-03 06:22:05.331 DemoAudio[8706:10703] Input nChannels:2
2012-04-03 06:22:05.332 DemoAudio[8706:10703] Output nChannels:2

入力も出力も2チャンネル。ステレオのようだ。実機での結果は、以下のとおり。

2012-04-03 06:31:17.136 DemoAudio[7496:707] Input nChannels:17072444
2012-04-03 06:31:17.142 DemoAudio[7496:707] Output nChannels:2

あれ、入力のチャンネル数がおかしい。

シミュレータと実機では得られる入力データが異なった。前回説明したが、シミュレータでは以下のとおり。

2012-04-01 01:45:33.482 DemoAudio[7045:10703] -[AudioUnitViewController record:]
2012-04-01 01:45:34.925 DemoAudio[7045:17503] MyAURenderCallack, inNumberFrames:512
2012-04-01 01:45:34.926 DemoAudio[7045:17503] ioData: mNumberBuffers(1)
2012-04-01 01:45:34.928 DemoAudio[7045:17503] ioData->mBuffers[0]: mNumberChannels(2), mDataByteSize(2048)

実機では、こうなった。

2012-04-03 06:43:20.907 DemoAudio[7496:707] -[AudioUnitViewController record:]
2012-04-03 06:43:21.316 DemoAudio[7496:4203] MyAURenderCallack, inNumberFrames:1024
2012-04-03 06:43:21.319 DemoAudio[7496:4203] ioData: mNumberBuffers(2)
2012-04-03 06:43:21.329 DemoAudio[7496:4203] ioData->mBuffers[0]: mNumberChannels(1), mDataByteSize(4096)
2012-04-03 06:43:21.333 DemoAudio[7496:4203] ioData->mBuffers[1]: mNumberChannels(1), mDataByteSize(4096)

どちらも2チャンネルで、シミュレータはインターリーブ、実機は非インターリーブのようにみえる。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub

_ 関連情報

Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
iPhone Core Audioプログラミング(永野 哲久 著)
とれも参考にさせていただきました。

2012-04-03 [iOS]Looping Recorderの基礎(Audio Unit (5))

Remote IO UnitのRemote Inputについて疑問が多いので、色々と調べてみたいが、実際に動作させてみて分かるという所もあるので前に進める。

音楽CDは、44.1kHz / 2ch 16bit。

Audio Unitの標準サンプル単位AudioUnitSampleTypeは、iOSでは8.24固定小数点。OS Xでは、Float32らしい。つまり、32bit。

オーディオ正準形のサンプル単位AudioSampleTypeは、16bit符号付き整数。

Remote Outputの入出力の形式は、『iPhone Core Audioプログラミング』によると、iPhone OS 2.2.1までは32bit。iPhone OS 3.0以降では16bitらしい。

以上の情報を参考にして、扱うサンプルは、Audio Unit標準サンプル単位のモノラルにする事にした。

@interface AudioUnitViewController : UIViewController
	:
@property (nonatomic, assign) AudioUnitSampleType           *buffer;
@property (nonatomic, assign) uint32_t                      startingSampleCount;
@property (nonatomic, assign) uint32_t                      maxSampleCount;
	:
@end

バッファに書き込むメソッドを用意する。

- (void)write:(UInt32)inNumberFrames data:(AudioBufferList *)ioData
{
    uint32_t    available = self.maxSampleCount - self.startingSampleCount;
    if (available < inNumberFrames) {
        inNumberFrames = available;
    }
    memcpy(self.buffer + self.startingSampleCount, ioData->mBuffers[0].mData, sizeof(AudioUnitSampleType) * inNumberFrames);
    self.startingSampleCount = self.startingSampleCount + inNumberFrames;
    if (self.maxSampleCount <= self.startingSampleCount) {
        [self stop:nil];
    }
}

録音のコールバック関数に渡されたデータをこのバッファに書き込む。

static OSStatus MyAURenderCallack(void *inRefCon,
                                  AudioUnitRenderActionFlags *ioActionFlags,
                                  const AudioTimeStamp *inTimeStamp,
                                  UInt32 inBusNumber,
                                  UInt32 inNumberFrames,
                                  AudioBufferList *ioData)
{
    AudioUnitViewController *viewController = (AudioUnitViewController *)inRefCon;
    [viewController write:inNumberFrames data:ioData];
    return noErr;
}

ちょっと、乱暴なコードとなったが、次回は、このバッファのデータを再生してみる。さて、どうなるか。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub

_ 関連情報

Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
iPhone Core Audioプログラミング(永野 哲久 著)
とれも参考にさせていただきました。

2012-04-04 [iOS]Looping Recorderの基礎(Audio Unit (6))

録音したデータを再生しようとしたが、上手くいかなかったので、アプリケーション内部で生成したサイン波の再生から、一歩ずつ進める事にした。

再生部を検討していて、録音部で使用したAudio Unit Processing Graph Servicesの使用方法は見直した方が良さそうだったので、再生部はAudio Unitを直接生成した。ヘッダーの内容は以下のとおり。

再生はAudioUnitを直接生成した。

@interface AudioUnitViewController : UIViewController
	:
@property (nonatomic, assign) AudioUnit                     audioUnit;
@property (nonatomic, assign) double                        phase;
@property (nonatomic, assign) Float64                       sampleRate;
@property (nonatomic, assign) BOOL                          isPlaying;
	:
@end

Audio Unitの生成メソッドだ。

- (void)prepareAudioUnit
{
    AudioComponentDescription   cd;
    cd.componentType            = kAudioUnitType_Output;
    cd.componentSubType         = kAudioUnitSubType_RemoteIO;
    cd.componentManufacturer    = kAudioUnitManufacturer_Apple;
    cd.componentFlags           = 0;
    cd.componentFlagsMask       = 0;
 
    AudioComponent  component = AudioComponentFindNext(NULL, &cd);
    AudioComponentInstanceNew(component, &__audioUnit);
    AudioUnitInitialize(self.audioUnit);
    AURenderCallbackStruct  callbackStruct;
    callbackStruct.inputProc = MyPlayAURenderCallack;
    callbackStruct.inputProcRefCon = self;
    AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callbackStruct, sizeof(AURenderCallbackStruct));
 
    self.phase = 0.0;
    self.sampleRate = 44100.0;
    
    AudioStreamBasicDescription audioFormat = [self auCanonicalASBDSampleRate:self.sampleRate channel:2];
    
    AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioFormat, sizeof(AudioStreamBasicDescription));
}

再生のコールバック関数。サイン波を設定している。

static OSStatus MyPlayAURenderCallack (
                                       void                        *inRefCon,
                                       AudioUnitRenderActionFlags  *ioActionFlags,
                                       const AudioTimeStamp        *inTimeStamp,
                                       UInt32                      inBusNumber,
                                       UInt32                      inNumberFrames,
                                       AudioBufferList             *ioData
                                       )
{
    AudioUnitViewController *viewController = (AudioUnitViewController *)inRefCon;
    float   freq = 440 * 2.0 * M_PI / viewController.sampleRate;
    double  phase = viewController.phase;
    AudioUnitSampleType *outL = ioData->mBuffers[0].mData;
    AudioUnitSampleType *outR = ioData->mBuffers[1].mData;
    for (int i = 0; i < inNumberFrames; i++) {
        float   wave = sin(phase);
        AudioUnitSampleType sample = wave * (1 << kAudioUnitSampleFractionBits);
        *outL++ = sample;
        *outR++ = sample;
        phase = phase + freq;
    }
    viewController.phase = phase;
    return noErr;
}

色々、デバッグ出力を入れている為か、スムーズでないか、鳴るようになった。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub

_ 関連情報

Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
iPhone Core Audioプログラミング(永野 哲久 著)
とれも参考にさせていただきました。

2012-04-05 [iOS]Looping Recorderの基礎(Audio Unit (7))

一応、録音した音が再生できた。だが、かなり荒いコードだ。そもそも、iPhoneシミュレータでAudio Unitを使用する場合、今のXcodeのバージョンだと問題があるようで、エラーメッセージが表示されていて、意図した動きをしない。自分のバグの可能性もあるが。実機で動作できたが、モノラル/ステレオの扱い等がいい加減なコードだ。でも、第一歩としては、今回はこれでヨシとしよう。

録音したデータをバッファに保存するコードだ。

- (void)write:(UInt32)inNumberFrames data:(AudioBufferList *)ioData
{
    uint32_t    available = self.maxSampleCount - self.startingSampleCount;
    if (available < inNumberFrames) {
        inNumberFrames = available;
    }
    memcpy(self.buffer + self.startingSampleCount, ioData->mBuffers[0].mData, sizeof(AudioUnitSampleType) * inNumberFrames);
    self.startingSampleCount = self.startingSampleCount + inNumberFrames;
    if (self.maxSampleCount <= self.startingSampleCount) {
        [self stop:nil];
    }
}
	:
static OSStatus MyAURenderCallack(void *inRefCon,
                                  AudioUnitRenderActionFlags *ioActionFlags,
                                  const AudioTimeStamp *inTimeStamp,
                                  UInt32 inBusNumber,
                                  UInt32 inNumberFrames,
                                  AudioBufferList *ioData)
{
    AudioUnitViewController *viewController = (AudioUnitViewController *)inRefCon;
    [viewController write:inNumberFrames data:ioData];
    return noErr;
}

ループ再生するコードだ。

- (void)read:(UInt32)inNumberFrames data:(AudioBufferList *)ioData
{
    uint32_t    available = self.maxSampleCount - self.startingSampleCount;
    uint32_t    num = inNumberFrames;
    if (available < num) {
        num = available;
    }
    memcpy(ioData->mBuffers[0].mData, self.buffer + self.startingSampleCount, sizeof(AudioUnitSampleType) * num);
    self.startingSampleCount = self.startingSampleCount + num;
    if (self.maxSampleCount <= self.startingSampleCount)
        self.startingSampleCount = 0;
    if (num < inNumberFrames) {
        num = inNumberFrames - num;
        memcpy(ioData->mBuffers[0].mData, self.buffer + self.startingSampleCount, sizeof(AudioUnitSampleType) * num);
        self.startingSampleCount = self.startingSampleCount + num;
    }
    memcpy(ioData->mBuffers[1].mData, ioData->mBuffers[0].mData, sizeof(AudioUnitSampleType) * inNumberFrames);
}
	:
static OSStatus MyPlayAURenderCallack (
                                       void                        *inRefCon,
                                       AudioUnitRenderActionFlags  *ioActionFlags,
                                       const AudioTimeStamp        *inTimeStamp,
                                       UInt32                      inBusNumber,
                                       UInt32                      inNumberFrames,
                                       AudioBufferList             *ioData
                                       )
{
    AudioUnitViewController *viewController = (AudioUnitViewController *)inRefCon;
    [viewController read:inNumberFrames data:ioData];
    return noErr;
}

例えば、鍵盤楽器のようなアプリケーションを作る場合は、Audio Unitのレスポンスの早さは魅力的だと思う。そうでなければ、ちょっと、面倒かな。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub

_ 関連情報

Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
iPhone Core Audioプログラミング(永野 哲久 著)
とれも参考にさせていただきました。

2012-04-06 [Web]INTER-Mediatorについて

INTER-Mediatorは、データベースとの連動が簡単に行える、Webのフレームワークだ。

MySQLにデモ用のデータベースを用意する。

$ mysqladmin -u root -p create DEMOCompactDisc
Enter password: 
$ mysql -u root -p mysql
Enter password: 
	:
mysql> GRANT ALL PRIVILEGES ON DEMOCompactDisc.* TO demo
    -> IDENTIFIED BY 'test';
 
mysql> GRANT ALL PRIVILEGES ON DEMOCompactDisc.* TO demo@localhost
    -> IDENTIFIED BY 'test';
 
mysql> FLUSH PRIVILEGES;
 
mysql> quit
 
$ mysql -u demo -p DEMOCompactDisc
Enter password: 
	:
mysql> CREATE TABLE CD (CD_ID INT NOT NULL,
    -> RECORD_LABEL_ID INT,
    -> CD_TITLE TEXT,
    -> PRIMARY KEY (CD_ID));
 
mysql> CREATE TABLE Artist (ARTIST_ID INT NOT NULL,
    -> ARTIST_NAME TEXT,
    -> PRIMARY KEY (ARTIST_ID));
 
mysql> CREATE TABLE Song (SONG_ID INT NOT NULL,
    -> CD_ID INT,
    -> ARTIST_ID INT,
    -> SONG_NAME TEXT,
    -> PRIMARY KEY (SONG_ID));
 
mysql> CREATE TABLE RecordLabel (RECORD_LABEL_ID INT NOT NULL,
    -> RECORD_LABEL_NAME TEXT,
    -> PRIMARY KEY (RECORD_LABEL_ID));
 
mysql> INSERT INTO Artist (ARTIST_ID, ARTIST_NAME) VALUES (1, "Robert Leroy Johnson");
 
mysql> select * from Artist;
+-----------+----------------------+
| ARTIST_ID | ARTIST_NAME          |
+-----------+----------------------+
|         1 | Robert Leroy Johnson |
+-----------+----------------------+
 
mysql> quit
$

著者は、MacBook上のApacheを利用したが、PHPを有効にしておく事。

INTER-Mediatorのサイトからダウンロードした「develop-im」フォルダ内の「INTER-Mediator」が本体なので、これをデモ用のサイト配下にコピーする。

「index.php」ファイルを作成する。

<?php
require_once('INTER-Mediator/INTER-Mediator.php');
 
IM_Entry(
	array(
		array(
			'name' => 'Artist',
			'key' => 'ARTIST_ID',
		),
	),
	null,
	array(
		'db-class' => 'PDO',
		'dsn' => 'mysql:unix_socket=/tmp/mysql.sock;dbname=DEMOCompactDisc;',
		'user' => 'demo',
		'password' => 'test',
	),
	false);
?>

HTMLファイルを用意する。

<!DOCTYPE  PUBLIC "-//W3C//DTD X 1.0 Transitional//EN" "http://www.w3.org/TR/x1/DTD/x1-transitional.dtd">
< xmlns="http://www.w3.org/1999/x">
	<head>
		<title>INTER-Mediator - DEMOCompactDisc</title>
		<meta http-equiv="content-type" content="text/;charset=UTF-8" />
		<script type="text/javascript" src="index.php"></script>
	</head>
	<body onLoad="INTERMediator.construct(true);">
		<h1>INTER-Mediator DEMOCompactDisc</h1>
		<table border="1">
			<thead>
				<tr>
					<th>アーティスト</th>
				</tr>
			</thead>
			<tbody>
				<tr>
					<td class="IM[Artist@ARTIST_NAME]"></td>
				</tr>
			</tbody>
		</table>
	</body>
</html>

基本的に、TDタグにcassとしてIM[Artist@ARTIST_NAME]と指定すれば、ArtistテーブルのARTIST_NAMEが表示されている事が確認できると思う。

画像の説明

簡単でしょ!

_ 関連情報

INTER-Mediator
INTER-Mediatorのサイト。

2012-04-07 [iOS]ページ・ビュー・コントローラについて

既存の紙の書籍の電子化は、ePubでiBooks。電子化ならではは、独自にアプリケーション化という流れがはっきりしてきたのか、独自にアプリケーション化を助けるフレーウワークとして、iOS 5からUIPageViewControllerが用意された。

新規プロジェクトとしてPage-Based Applicationが用意された。

新規プロジェクト

興味深いのは、ModelControllerクラス。以前、著者はモデルを管理するクラスとしてDocumentクラスについて説明したが、それに対応するクラスということか。

ModelController

各ページは、DataViewControllerで表示している。ページに対応するデータは、ModelControllerが管理している。

iPhoneの場合

iPhoneの場合は、前ページと後ページの同時に2ページ表示される。iPadでは、裏も表示されるので最大4ページ。これをフレームワークで管理してくれるということのようだ。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Books - GitHub

_ 関連情報

UIPageViewController Class Reference
Apple Developerのサイト。

2012-04-08 [OSX][iOS][Aruduino]銀座 Arduino ハンズオン (3回目)

AppleStore銀座で、三階目のAruduinoハンズオンセミナー初級編が開催されたので参加してきた。

今回こそは、完璧に理解してやるぞ!と意気込んだが、会場内だけで解決するのは無理なようだ。

その後の復習の結果をここで報告できたらと考えている。

Arduino

_ 関連情報

Arduino
本家のホームページです。
Arduino 日本語リファレンス
助かります。ありがとうございます。
TETRASTYLE-dev-BLOG
iOSとArduinoの情報が豊富です。

2012-04-09 [iOS]電子書籍への道:PDF表示

_ [iOS]電子書籍への道:PDF表示

前回の『[iOS]ページ・ビュー・コントローラについて』で作成した(殆ど、雛形のままだが)サンプル・プログラムに、Apple Developerサイトで交際されているサンプル・プログラムZoomingPDFViewを組み込んで、PDF表示に対応させようと考えている。

Think ITの連載「iOSでつくる2011アプリ開発状況」の「第4回 出版プラットフォームとしてのiOS」によるとiOSでPDFを表示させる為には、アプリケーション側での工夫が必要という事だ。Core Grahicsを使って、ViewのdrowRect:で描画すると、各代表辞された場合にぼやけてしまう。レイヤーに描画すれば拡大してもぼやけないが、描画が遅くなってしまう。そこで、拡大表示する場合は拡大前に作成した画像を表示させておいて、後で、奇麗な描画結果に差し替えるという対応が必要となるが、それのサンプルがZoomingPDFViewということのようだ。

続きは次回で。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Books - GitHub

_ 関連情報

第4回 出版プラットフォームとしてのiOS
Think IT 連載: iOSでつくる2011アプリ開発状況から。
Quartz 2d Programming Guide
Acroll View Programming Guide for iOS
ZoomingPEFViewer
Apple Developerサイトの情報。

2012-04-10 [iOS]電子書籍への道:PDF表示(その2)

先日紹介した『[iOS]ページ・ビュー・コントローラについて』のサンプル・プログラムにApple Developerサイトのサンプル・プログラムZoomingPDFViewを組み込んだだけなので、細かくは説明しない。

組み込む際に変更した箇所は、AtoryBoard対応や、最新のObjective-Cの記法への変更ぐらいで、ほぼ、そのまま。

電子書籍

次回以降で、このサンプル・プログラムを電子書籍アプリケーションの雛形として完成度を高めて行きたいと考えている。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Books - GitHub

_ 関連情報

第4回 出版プラットフォームとしてのiOS
Think IT 連載: iOSでつくる2011アプリ開発状況から。
Quartz 2d Programming Guide
Acroll View Programming Guide for iOS
ZoomingPEFViewer
Apple Developerサイトの情報。

2012-04-14 [iOS]電子書籍への道:PDF表示(その3)

前回のサンプル・プログラムをリソースとして持っているPDF文書からページ数を取得して、頁に対応したページを表示するように拡張する。

Page-Based Applicationの雛形は、データを管理するクラスModelControllerが用意されている。また、iOSには複数の文書を扱う仕組みが用意されているので、いずれは、それへの対応という事になるが、今回は自分が単一文書を扱う場合に用意している独自のDocumentクラスでPDF書類を管理する事にした。

本当は、PDF文書の扱いはDocumentクラスで抽象化すべきと思うが、サンプル・コードという事で、単にプロパティとして持っている事で許して欲しい。

@interface Document : NSObject
	…
@property (nonatomic, assign) CGPDFDocumentRef  pdf;
	…
@end

そして、各コントローラクラスで、Documentクラスをプロパティとして参照するようにする。例えば、初期化をinitメソッドで行う場合は、以下のとおり。

#import "AppDelegate.h"
	…
- (id)init
{
    self = [super init];
    if (self) {
        AppDelegate	*appl = nil;
        appl = (AppDelegate *)[[UIApplication sharedApplication] delegate];
        self.document = appl.document;
    }
    return self;
}

ページ・ビュー・コントローラの利用では、各クラスの関係や、モデルの初期化のタイミングは少し悩んだので、分かりやすいように掻い摘んで説明する。

ADCのサンプルZoomingPDFViewでは、決めうちで、リソースで持っているPDF文書の1頁だけを表示するに固定されているので、PDFScrollViewの初期時にPDF文書関連の初期化を行っていたが、それを止めて、外部から指定されたタイミングで指定された頁を表示する様に変更する。

@interface PDFScrollView : UIScrollView 
	…
- (void)setIndexOfPDF:(NSUInteger)index;
- (NSUInteger)getIndexOfPDF;
	…
@end
 
@implementation PDFScrollView
	…
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
    }
    return self;
}
 
- (void)awakeFromNib
{
    self.decelerationRate = UIScrollViewDecelerationRateFast;
    self.delegate = self;
 
    AppDelegate	*appl = nil;
    appl = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    self.document = appl.document;
}
 
- (void)setIndexOfPDF:(NSUInteger)index
{
    self.page = CGPDFDocumentGetPage(self.document.pdf, index + 1);
    CGPDFPageRetain(self.page);
    
    CGRect pageRect = CGPDFPageGetBoxRect(self.page, kCGPDFMediaBox);
    self.pdfScale = self.frame.size.width/pageRect.size.width;
    pageRect.size = CGSizeMake(pageRect.size.width * self.pdfScale, pageRect.size.height * self.pdfScale);
    
    UIGraphicsBeginImageContext(pageRect.size);
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
    CGContextFillRect(context,pageRect);
    
    CGContextSaveGState(context);
    CGContextTranslateCTM(context, 0.0, pageRect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
     
    CGContextScaleCTM(context, self.pdfScale, self.pdfScale);	
    CGContextDrawPDFPage(context, self.page);
    CGContextRestoreGState(context);
    
    UIImage *backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    
    self.backgroundImageView = [[UIImageView alloc] initWithImage:backgroundImage];
    self.backgroundImageView.frame = pageRect;
    self.backgroundImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self addSubview:self.backgroundImageView];
    [self sendSubviewToBack:self.backgroundImageView];
    
    self.pdfView = [[TiledPDFView alloc] initWithFrame:pageRect andScale:self.pdfScale];
    [self.pdfView setPage:self.page];
    
    [self addSubview:self.pdfView];
}
 
- (NSUInteger)getIndexOfPDF
{
    return (NSUInteger)CGPDFPageGetPageNumber(self.page);
}
…
@end

雛形ではカレンダーのデータを保持していたDataViewControllerクラスを表示している頁番号とPDFScrollViewクラスを扱うように変更する。

@interface DataViewController : UIViewController
	…
@property (strong, nonatomic) Document                  *document;
@property (nonatomic, assign) NSUInteger                index;
	…
- (void)setIndexOfPDF:(NSUInteger)index;
- (NSUInteger)getIndexOfPDF;
@end
 
@implementation DataViewController
	…
- (void)setIndexOfPDF:(NSUInteger)index
{
    self.index = index;
    PDFScrollView   *pdfScrollView = (PDFScrollView *)self.view;
    [pdfScrollView setIndexOfPDF:index];
}
 
- (NSUInteger)getIndexOfPDF
{
    return self.index;
}
	…
@end

頁に対応するビュー・コントローラDataViewControllerは、モデル・コントーラで管理される?ということで、PDF表示に対応させる。

@implementation ModelController
	…
- (DataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard
{   
    if (CGPDFDocumentGetNumberOfPages(self.document.pdf) < index)
        return nil;
     
    DataViewController *dataViewController = [storyboard instantiateViewControllerWithIdentifier:@"DataViewController"];
    [dataViewController setIndexOfPDF:index];
    return dataViewController;
}
 
- (NSUInteger)indexOfViewController:(DataViewController *)viewController
{   
    return viewController.index;
}
 
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSUInteger index = [self indexOfViewController:(DataViewController *)viewController];
    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }
     
    index--;
    return [self viewControllerAtIndex:index storyboard:viewController.storyboard];
}
 
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    NSUInteger index = [self indexOfViewController:(DataViewController *)viewController];
    if (index == NSNotFound) {
        return nil;
    }
    
    index++;
    if (index == CGPDFDocumentGetNumberOfPages(self.document.pdf))
        return nil;
    return [self viewControllerAtIndex:index storyboard:viewController.storyboard];
}
	…
@end

このサンプルをGitHubから取得して、実行すると、強制的に著者がCocoa勉強会で発表した文書を読まされる事になる。

Books

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Books - GitHub

_ 関連情報

第4回 出版プラットフォームとしてのiOS
Think IT 連載: iOSでつくる2011アプリ開発状況から。
Quartz 2d Programming Guide
Acroll View Programming Guide for iOS
ZoomingPEFViewer
Apple Developerサイトの情報。
Programming with Quartz: 2D and PDF Graphics in Mac OS X
OS XとiOSのCore Graphicsの解説書。

2012-04-16 [iOS]電子書籍への道:PDF表示(その4)

前回までの内容で、一つ、大きな過ちを犯している事に気がついた。プロジェクトを新規作成する際にDevcesをUniversalでなくiPhoneを選んでしまった。

結局、新たにプロジェクトを作成して、それに変更個所を反映して、iPad対応を行ったが、iPad対応で手を加えるべき場所をあげてみる。

iPad用のStoryboardを用意する。今回の場合、新規のPage-Based Applicationを作成して、そこから、iPad用のStoryboardをコピーすればよい。

Storyboard

TargetsでDevicesをUniversalに変更。

Universal

するとiPad Deployment Infoが現れるので、それに、先ほど追加してiPad用のStoryboardを設定する。

Deployment Info

iPad用Storyboardコピー用に生成したプロジェクトで、iPhoneとiPadで条件わけしているコードを反映する。

ただ、自分で試してみたのだが、iPadで横置きにした際に、見開き表示になるはずだが、上手くいかなかったので、新規プロジェクトに変更個所を反映する対応にしてしまった。

run

まだ、多少の不具合があるが、続きは次回。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Books - GitHub

_ 関連情報

第4回 出版プラットフォームとしてのiOS
Think IT 連載: iOSでつくる2011アプリ開発状況から。
Quartz 2d Programming Guide
Acroll View Programming Guide for iOS
ZoomingPEFViewer
Apple Developerサイトの情報。
Programming with Quartz: 2D and PDF Graphics in Mac OS X
OS XとiOSのCore Graphicsの解説書。

2012-04-17 関東第52回Cocoa勉強会のご案内

_ 関東第52回Cocoa勉強会の日程が決定いたしました。

日時: 2012/05/19(土) 13:00-17:00

会場:新宿三丁目 新宿伊藤ビル 4F

集合:現地

会費:500円

見学者は以下のフォームで募集しています。

http://www.cocoa-study.com/mail/


2012-04-19 [iOS]Tweeting(アカウント管理)

iOS5からTwitter frameworkが用意されるようになり便利になった。ただ、機能的には簡素な部分があるので、他のオープンソースのフレームワークの方が便利な事もあるが。

Twitter.frameworkとAccounts.frameworkをプロジェクトに追加し、それぞれのヘッダー・ファイルをimportする。

OS X Lionには、Twitter/Accounts frameworkが用意されていない為か、シミュレータではtweetを投稿できない。iOSではTwitterのアカウント管理の機能がOS側に用意されている。アカウントの変更の通知を受ける為と、投稿可能かどうかを知るため、以下のコードを追加する。


- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(canTweetStatus)
                                                 name:ACAccountStoreDidChangeNotification
                                               object:nil];
}
 
- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    [self canTweetStatus];
}
 
- (void)canTweetStatus {
    if ([TWTweetComposeViewController canSendTweet]) {
        self.tweetStatusLabel.text = @"can send tweet";
    }
    else {
        self.tweetStatusLabel.text = @"can't send tweet";
    }
}

OS Xのシミュレータでは"can send tweet"、iOSの実機では"can't send tweet"と表示されるはずだ。

このサンプル・コードを実機で起動した状態で、iOSのTwitterアカウントに変更を加えたら、どうなるだろうか?試してみよう。

設定で、Twitterアカウントを削除してみたが、通知はやってこないようだ。一旦、アプリケーションを終了させて、起動させると"can't send tweet"と表示された。

OSのTwitter機能を利用する場合、ユーザに利用していいのか確認のダイアログが表示されたが、何をやったら表示されるのだろうか?投稿可能かどうかのメソッドを呼ぶだけでは、この確認は表示されないようだ。そこで、投稿機能を追加してみた。


- (IBAction)tweet:(id)sender
{
    TWTweetComposeViewController *tweetViewController = [[TWTweetComposeViewController alloc] init];
    
    [tweetViewController setInitialText:@"hello, world"];
    
    [tweetViewController setCompletionHandler:^(TWTweetComposeViewControllerResult result) {
        NSString *output;
        
        switch (result) {
            case TWTweetComposeViewControllerResultCancelled:
                output = @"Tweet cancelled.";
                break;
            case TWTweetComposeViewControllerResultDone:
                output = @"Tweet done.";
                break;
            default:
                break;
        }
        
        [self performSelectorOnMainThread:@selector(displayText:)
                               withObject:output
                            waitUntilDone:NO];
        
        [self dismissModalViewControllerAnimated:YES];
    }];
    
    [self presentModalViewController:tweetViewController animated:YES];
}
 
- (void)displayText:(NSString *)text
{
    self.tweetStatusLabel.text = text;
}

おっと、先ほどアカウント情報を削除していたので、設定を催促するダイアログが表示された!

NoTwitterAccounts

今回は、既に設定済みの場合を確認したいので、アプリケーションを終了させて、アカウント情報を設定後に、再起動させてみた。

何も言われず投稿できてしまった。また、設定のTwitterを確認したところ、許可するアプリケーションの一覧にこのサンプルコードが追加されてる。あれ、ユーザに確認がこなかったけ?とりあえず、許可をOffしてみる。

すると、確認のダイアログが表示された。予想通りの動きでない部分があるが、この確認をフレームワーク側で勝手にやってくれるのかどうかを確認したかったので、よしとしよう。

TwitterIsNotEnabled

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Tweets - GitHub

_ 関連情報

iOS Twitter framework
Twitter Developersサイトの情報。
Tweeting
Developerサイトのサンプル・コード

2012-04-20 [iOS]Tweeting(アカウント管理 2)

iOS5からのTwitter/Accounts frameworkの凄いところは、複数のアカウントを扱える事だ。DeveloperサイトのサンプルコードTweetingのコードに手を加えて、iOS機器に登録されているアカウントの全てでtweetするようにしてみた。


- (IBAction)tweet2:(id)sender
{
    ACAccountStore *accountStore = [[ACAccountStore alloc] init];
	
    ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
	
    [accountStore requestAccessToAccountsWithType:accountType withCompletionHandler:^(BOOL granted, NSError *error) {
        if(granted) {
            NSArray *accountsArray = [accountStore accountsWithAccountType:accountType];
			
            for (NSUInteger i = 0; i < [accountsArray count]; i++) {
                ACAccount *twitterAccount = [accountsArray objectAtIndex:i];
                NSLog(@"account: %@", twitterAccount);
				
                TWRequest *postRequest = [[TWRequest alloc]
                    initWithURL:[NSURL URLWithString:@"http://api.twitter.com/1/statuses/update.json"]
                    parameters:[NSDictionary dictionaryWithObject:@"hello, world" forKey:@"status"]
                    requestMethod:TWRequestMethodPOST];
				
                [postRequest setAccount:twitterAccount];
				
                [postRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                    NSString *output = [NSString stringWithFormat:@"HTTP response status: %i",
                        [urlResponse statusCode]];
                    NSLog(@"%@", output);
                    [self performSelectorOnMainThread:@selector(displayText:) withObject:output waitUntilDone:NO];
                }];
            }
        }
	}];
}

登録されているアカウント分、同じ内容のtweetが投稿されている事が確認できると思う。無意味な内容なので迷惑だと思うが。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Tweets - GitHub

_ 関連情報

iOS Twitter framework
Twitter Developersサイトの情報。
Tweeting
Developerサイトのサンプル・コード

2012-04-21 [iOS]Tweeting(パブリックタイムライン)

今回は、DeveloperサイトのサンプルコードTweetingのコードのままだ。

パブリックタイムラインを取得して、それをNSLog()でデバッグ出力するだけだ。

- (IBAction)timeline:(id)sender
{
    TWRequest *postRequest = [[TWRequest alloc] initWithURL:[NSURL URLWithString:@"http://api.twitter.com/1/statuses/public_timeline.json"] parameters:nil requestMethod:TWRequestMethodGET];
	
	[postRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
		NSString *output;
		
		if ([urlResponse statusCode] == 200) {
			NSError *jsonParsingError = nil;
			NSDictionary *publicTimeline = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonParsingError];
			output = [NSString stringWithFormat:@"HTTP response status: %i\nPublic timeline:\n%@", [urlResponse statusCode], publicTimeline];
		}
		else {
			output = [NSString stringWithFormat:@"HTTP response status: %i\n", [urlResponse statusCode]];
		}
		
		[self performSelectorOnMainThread:@selector(msgBox:) withObject:output waitUntilDone:NO];
	}];
}
 
- (void)msgBox:(NSString *)text
{
    self.msgBoxTextView.text = text;
}

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Tweets - GitHub

_ 関連情報

iOS Twitter framework
Twitter Developersサイトの情報。
Tweeting
Developerサイトのサンプル・コード

2012-04-22 [iOS]地図を扱う

地図を表示するのは簡単だ。MapKitフレームワークをプロジェクトに追加し、MapKit/MapKit.hをインポートする。そして、以下のコードで地図は表示される。

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    CGRect  rect = CGRectMake(10.0, 80.0, 300.0, 300.0);
    self.mapView = [[MKMapView alloc] initWithFrame:rect];
    [self.view addSubview:self.mapView];
}
 
- (void)viewDidUnload
{
    self.mapView = nil;
 
    [super viewDidUnload];
}

viewDidLoadメソッドに以下のコードを追加すると、指定した緯度経度が表示される。

    MKCoordinateRegion  region = {{34.406944, 133.195462}, {1.0, 1.0}};
    [self.mapView setRegion:region animated:NO];

以下のコードで、ピンが追加される。

@interface ViewController : UIViewController 
@property (strong, nonatomic) MKMapView *mapView;
@end
 
@interface Annotation : NSObject 
@property (strong, nonatomic) NSString                  *name;
@property (assign, nonatomic) CLLocationCoordinate2D    coordinate;
- (id)initWithName:(NSString *)name latitude:(double)latitude longitude:(double)longitude;
@end
 
@implementation Annotation
@synthesize name = _name;
@synthesize coordinate = _coordinate;
- (id)initWithName:(NSString *)name latitude:(double)latitude longitude:(double)longitude
{
    if ((self = [super init]) != nil) {
        CLLocationCoordinate2D    coordinate;
        coordinate.latitude = latitude;
        coordinate.longitude = longitude;
        self.coordinate = coordinate;
        self.name = name;
    }
    return self;
}
 
- (NSString *)title
{
    return self.name;
}
 
- (void)dealloc
{
    self.name = nil;
}
@end
 
@implementation ViewController
@synthesize mapView = _mapView;
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    CGRect  rect = CGRectMake(10.0, 80.0, 300.0, 300.0);
    self.mapView = [[MKMapView alloc] initWithFrame:rect];
    [self.view addSubview:self.mapView];
    
    MKCoordinateRegion  region = {{34.406944, 133.195462}, {1.0, 1.0}};
    [self.mapView setRegion:region animated:NO];
    
    Annotation  *annotation = [[Annotation alloc] initWithName:@"土堂小学校"
                                                     latitude:34.406944
                                                    longitude:133.195462];
    [self.mapView addAnnotation:annotation];
}
 
- (void)viewDidUnload
{
    self.mapView = nil;
 
    [super viewDidUnload];
}
 
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation
{
    if (annotation == self.mapView.userLocation) {
        return nil;
    }
     
    MKPinAnnotationView *pinAnnotationView = (MKPinAnnotationView *)[self.mapView dequeueReusableAnnotationViewWithIdentifier:@"pin"];
    if (pinAnnotationView == nil) {
        pinAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"pin"];
    }
    else {
        pinAnnotationView.annotation = annotation;
    }
    
    pinAnnotationView.pinColor = MKPinAnnotationColorRed;
    pinAnnotationView.animatesDrop = YES;
    pinAnnotationView.canShowCallout = YES;
    return pinAnnotationView;
}

実行。

Maps

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Maps - GitHub

2012-04-23 [iOS]データ管理(その1)

独自にデータ管理をDocumentクラスで行っている著者としては、CoreDataとの付き合いは悩む部分がある。今回は曖昧にしていた部分をしっかり理解できる様、シンプルな構成のプロジェクトにCoreDataを組み込んでゆく。

プリコンパイル・ヘッダーDatum-Prefix.pchにCoreDataのヘッダーを追加する。

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <CoreData/CoreData.h>
#endif

アプリケーションのコントローラとなるAppDelegateに、CoreData関連のプロパティとメソッドを追加する。

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
 
- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

そして、AppDelegateにCoreData関連のコードを追加する。

アプリケーション終了時に保存。

- (void)applicationWillTerminate:(UIApplication *)application
{
    [self saveContext];
}

突然、アプリケーションが停止される事を考えて、バックグラウンドに回った際にも保存する等、工夫する余地はあると思う。

保存のメソッドだ。

- (void)saveContext
{
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        } 
    }
}

これは雛形で用意されるメソッドそのままだが、abort()関数の呼び出しは、マズイと思う。リジェクトもされるのでは?

雛形のコードは巧妙で、プロパティ呼び出しが、メソッド呼び出しになっていて、必要なタイミングで生成されるようになっている。

- (NSManagedObjectContext *)managedObjectContext
{
    if (__managedObjectContext != nil) {
        return __managedObjectContext;
    }
    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        __managedObjectContext = [[NSManagedObjectContext alloc] init];
        [__managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return __managedObjectContext;
}
 
- (NSManagedObjectModel *)managedObjectModel
{
    if (__managedObjectModel != nil) {
        return __managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Datum" withExtension:@"momd"];
    __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return __managedObjectModel;
}
 
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil) {
        return __persistentStoreCoordinator;
    }
     
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Data.sqlite"];
    
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return __persistentStoreCoordinator;
}

SQLiteファイルの保存先を返すメソッド。ただ、Documentディレクトリでいいのか?隠すように場所でもいいのでは?という疑問はある。

- (NSURL *)applicationDocumentsDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

モデルを定義したファイルDatum.xdatamodeldは、とりあえず、雛形で用意されるファイルのコピーだ。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Datum - GitHub

_ 関連情報

Core Data Programming Guide
Developerサイトの情報。

2012-04-24 [iOS]データ管理(その2)

MainStoryboard.storyboardの中身を空にして、Navigation Controllerを配置する。デフォルトで、テーブル・ビューが組み込まれているので、これを利用する。

Root View ControllerのCustom Classを自分のViewControllerに変更する(説明の順番が前後してしまったが、自分のViewControllerの親クラスをUITableViewControllerに変更しておく必要はあると思う)。

RootViewContoller

そして、Table View CellのIdentifierをCellに変更しておく。このIdentifierを後のコードで利用するので。

TableViewCell

独自のViewControllerクラスのヘッダーをテーブル・ビューとCoreDataに対応させる。


@interface ViewController : UITableViewController &tl;NSFetchedResultsControllerDelegate≷
 
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
 
@end

AppDelegateクラスの、アプリケーション起動時に呼ばれるメソッドで、管理オブジェクト・コンテキストをビュー・コントローラーに渡すコードを追加する。


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
    ViewController *controller = (ViewController *)navigationController.topViewController;
    controller.managedObjectContext = self.managedObjectContext;
    return YES;
}

ViewControllerクラスの、CoreDataとテーブル・ビュー対応のコードは、雛形のコードをほぼそのまま流用している。現在のXcodeの新規プロジェクトで生成される雛形のコードでは、NSFetchedResultsControllerが使われていて、データとテーブル・ビューの連動が自動で行われて便利だが、独自のデータ管理の仕組みを持っている場合、それを介さないでコントロールされるので、これについては、今後の課題と考えている。

@interface ViewController ()
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
@end
 
@implementation ViewController
 
@synthesize fetchedResultsController = __fetchedResultsController;
@synthesize managedObjectContext = __managedObjectContext;
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.navigationItem.leftBarButtonItem = self.editButtonItem;
    
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
        target:self action:@selector(insertNewObject:)];
    self.navigationItem.rightBarButtonItem = addButton;
}
 
…
 
- (void)insertNewObject:(id)sender
{
    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
    NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name]
        inManagedObjectContext:context];
    
    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
    
    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
}
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id  sectionInfo = [[self.fetchedResultsController sections]
        objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}
 
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}
 
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
        [context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
        
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }   
}
 
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
    return NO;
}
 
- (NSFetchedResultsController *)fetchedResultsController
{
    if (__fetchedResultsController != nil) {
        return __fetchedResultsController;
    }
    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event"
        inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    
    [fetchRequest setFetchBatchSize:20];
    
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];
    NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
    
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
        initWithFetchRequest:fetchRequest
        managedObjectContext:self.managedObjectContext
        sectionNameKeyPath:nil
        cacheName:@"Master"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;
    
	NSError *error = nil;
	if (![self.fetchedResultsController performFetch:&error]) {
	    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
	    abort();
	}
    
    return __fetchedResultsController;
}    
 
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}
 
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
 
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
                atIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
 
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}
 
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = [[object valueForKey:@"timeStamp"] description];
}
@end
Run

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Datum - GitHub

_ 関連情報

Core Data Programming Guide
Developerサイトの情報。

2012-04-25 [iOS]データ管理(その3)

独自のデータ管理の仕組みの用意している者にとっては、少し厄介なNSFetchedResultsControllerについて、すこし、考えてみる。

NSFetchedResultsControllerはテーブル・ビューでの利用を想定したコントローラのクラスだ。

ViewControllerクラスのNSFetchedResultsControllerを生成しているコードは以下の通り。

- (NSFetchedResultsController *)fetchedResultsController
{
    if (__fetchedResultsController != nil)    return __fetchedResultsController;
     
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event"
        inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    [fetchRequest setFetchBatchSize:20];
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];
    NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];    
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
        initWithFetchRequest:fetchRequest
        managedObjectContext:self.managedObjectContext
        sectionNameKeyPath:nil
        cacheName:@"Master"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;
    
	NSError *error = nil;
	if (![self.fetchedResultsController performFetch:&error]) {
	    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
	    abort();
	}    
    return __fetchedResultsController;
}

対象となるデータ(管理オブジェクト)を『Event』に、整列のキーを『timeStamp』と設定して、生成する。

そして、NSFetchedResultsControllerからテーブルに設定する項目を得ているので、NSFetchedResultsControllerは一覧表示する項目を管理するものといえる。

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id  sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = [[object valueForKey:@"timeStamp"] description];
}

そして、データが更新される。つまり、管理オブジェクト・コンテキストに変更が発生すると、委任の仕組みでそれを通知してくる。

- (void)controller:(NSFetchedResultsController *)controller
    didChangeSection:(id )sectionInfo
    atIndex:(NSUInteger)sectionIndex
    forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
- (void)controller:(NSFetchedResultsController *)controller
    didChangeObject:(id)anObject
    atIndexPath:(NSIndexPath *)indexPath
    forChangeType:(NSFetchedResultsChangeType)type
    newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
                atIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Datum - GitHub

_ 関連情報

Core Data Programming Guide
Developerサイトの情報。

2012-04-26 [iOS]GPSとGPX(その1)

GPSの情報を扱う形式に、GPX(GPS eXchange Format)というものがある。これを扱う為のフレームワークがMITライセンスで後悔されていたので、使ってみた。

iOS GPX Framework
http://gpxframework.com/

GitHubでの説明は、これをフレームワーク化して組み込み手順となっていたが、あまり、Xcodeに知らないものを組み込みたくないので、ソース一式をプロジェクトに追加した。

folder

ヘッダーファイルの検索パスに、このフレームワークのソースを追加する。

project

これで以下のようなサンプル・コードのビルドが通るようになったと思う。

#import <GPX/GPX.h>
...
GPXRoot *root = [GPXRoot rootWithCreator:@"Sample Application"];
    
GPXWaypoint *waypoint = [root newWaypointWithLatitude:35.658609f longitude:139.745447f];
waypoint.name = @"Tokyo Tower";
waypoint.comment = @"The old TV tower in Tokyo.";
    
GPXTrack *track = [root newTrack];
track.name = @"My New Track";
    
[track newTrackpointWithLatitude:35.658609f longitude:139.745447f];
[track newTrackpointWithLatitude:35.758609f longitude:139.745447f];
[track newTrackpointWithLatitude:35.828609f longitude:139.745447f];

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/WayPoints - GitHub

_ 関連情報

iOS GPX Framework
GitHub
This is a iOS framework for parsing/generating GPX files. This Framework parses the GPX from a URL or Strings and create Objective-C Instances of GPX structure.

2012-04-27 [iOS]GPSとGPX(その2)

前回は、iOS GPX Frameworkの使い方は何となく分かったが、それをGPXファイルとして保存するには、どうすればいいのか分からなくて困っていた。ソースコードを眺めてみて何となく分かってきた。GPXElementクラスが基本の様相だ。それのプロパティgpxはNSString型だ。どうやら、ここからGPXファイルのデータを取得するようだ。

以下は公式サイトのサンプル・コードそのものだ。

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    GPXRoot *root = [GPXRoot rootWithCreator:@"Sample Application"];
    
    GPXWaypoint *waypoint = [root newWaypointWithLatitude:35.658609f longitude:139.745447f];
    waypoint.name = @"Tokyo Tower";
    waypoint.comment = @"The old TV tower in Tokyo.";
    
    GPXTrack *track = [root newTrack];
    track.name = @"My New Track";
    
    [track newTrackpointWithLatitude:35.658609f longitude:139.745447f];
    [track newTrackpointWithLatitude:35.758609f longitude:139.745447f];
    [track newTrackpointWithLatitude:35.828609f longitude:139.745447f];
    
    NSLog(@"%@", root.gpx);
}

以下は、デバッグ出力の例だ。ちゃんと、GPXになっているようだ。

<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="Sample Application">
	<wpt lat="35.658607" lon="139.745453">
		<name>Tokyo Tower</name>
		<cmt>The old TV tower in Tokyo.</cmt>
	</wpt>
	<trk>
		<name>My New Track</name>
		<trkseg>
			<trkpt lat="35.658607" lon="139.745453">
			</trkpt>
			<trkpt lat="35.758610" lon="139.745453">
			</trkpt>
			<trkpt lat="35.828609" lon="139.745453">
			</trkpt>
		</trkseg>
	</trk>
</gpx>

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/WayPoints - GitHub

_ 関連情報

iOS GPX Framework
GitHub
This is a iOS framework for parsing/generating GPX files. This Framework parses the GPX from a URL or Strings and create Objective-C Instances of GPX structure.

関連する有益な情報を見つけたので、追加する。

iOSシミュレータで使えるGPXファイルジェネレーター

2012-04-28 [iOS][Web]iPhoneアプリケーションとサーバ間の通信(その1)

Ruby on Rails環境があると便利と聞いたし、OS Xにはインストール済みと聞いたので確認した。

$ ruby -v
ruby 1.8.7 (2010-01-10 patchlevel 249) [universal-darwin11.0]
$ gem -v
1.3.6
$ rails -v
Rails is not currently installed on this system. To get the latest version, simply type:
 
    $ sudo gem install rails
 
You can then rerun your "rails" command.

最新のOS Xではrailsは自分でインストールとの事のようなので、素直にこのコマンドを実行した。

$ sudo gem install rails
Password:
********************************************************************************
 
 \ MultiJson.encode is deprecated and will be removed in the next major version.
 \ Use MultiJson.dump instead.
 
  MultiJson.decode is deprecated and will be removed in the next major version.
  Use MultiJson.load instead.
 
  MultiJson.engine is deprecated and will be removed in the next major version.
  Use MultiJson.adapter instead.
 
  MultiJson.engine= is deprecated and will be removed in the next major
 \ version. Use MultiJson.use instead.
 
  MultiJson.default_engine is deprecated and will be removed in the next major
  version. Use MultiJson.default_adapter instead.
 
********************************************************************************
Building native extensions.  This could take a while...
Depending on your version of ruby, you may need to install ruby rdoc/ri data:
 
<= 1.8.6 : unsupported
 = 1.8.7 : gem install rdoc-data; rdoc-data --install
 = 1.9.1 : gem install rdoc-data; rdoc-data --install
>= 1.9.2 : nothing to do! Yay!
Successfully installed i18n-0.6.0
Successfully installed multi_json-1.3.2
Successfully installed activesupport-3.2.3
Successfully installed builder-3.0.0
Successfully installed activemodel-3.2.3
Successfully installed rack-1.4.1
Successfully installed rack-cache-1.2
Successfully installed rack-test-0.6.1
Successfully installed journey-1.0.3
Successfully installed hike-1.2.1
Successfully installed tilt-1.3.3
Successfully installed sprockets-2.1.3
Successfully installed erubis-2.7.0
Successfully installed actionpack-3.2.3
Successfully installed arel-3.0.2
Successfully installed tzinfo-0.3.33
Successfully installed activerecord-3.2.3
Successfully installed activeresource-3.2.3
Successfully installed mime-types-1.18
Successfully installed polyglot-0.3.3
Successfully installed treetop-1.4.10
Successfully installed mail-2.4.4
Successfully installed actionmailer-3.2.3
Successfully installed rake-0.9.2.2
Successfully installed thor-0.14.6
Successfully installed rack-ssl-1.3.2
Successfully installed json-1.7.0
Successfully installed rdoc-3.12
Successfully installed railties-3.2.3
Successfully installed bundler-1.1.3
Successfully installed rails-3.2.3
31 gems installed
Installing ri documentation for i18n-0.6.0...
Installing ri documentation for multi_json-1.3.2...
Installing ri documentation for activesupport-3.2.3...
Installing ri documentation for builder-3.0.0...
Installing ri documentation for activemodel-3.2.3...
Installing ri documentation for rack-1.4.1...
Installing ri documentation for rack-cache-1.2...
Installing ri documentation for rack-test-0.6.1...
Installing ri documentation for journey-1.0.3...
Installing ri documentation for hike-1.2.1...
Installing ri documentation for tilt-1.3.3...
Installing ri documentation for sprockets-2.1.3...
Installing ri documentation for erubis-2.7.0...
Installing ri documentation for actionpack-3.2.3...
Installing ri documentation for arel-3.0.2...
Installing ri documentation for tzinfo-0.3.33...
Installing ri documentation for activerecord-3.2.3...
Installing ri documentation for activeresource-3.2.3...
Installing ri documentation for mime-types-1.18...
Installing ri documentation for polyglot-0.3.3...
Installing ri documentation for treetop-1.4.10...
Installing ri documentation for mail-2.4.4...
Installing ri documentation for actionmailer-3.2.3...
Installing ri documentation for rake-0.9.2.2...
Installing ri documentation for thor-0.14.6...
Installing ri documentation for rack-ssl-1.3.2...
Installing ri documentation for json-1.7.0...
Installing ri documentation for rdoc-3.12...
Installing ri documentation for railties-3.2.3...
Installing ri documentation for bundler-1.1.3...
Installing ri documentation for rails-3.2.3...
Installing RDoc documentation for i18n-0.6.0...
Installing RDoc documentation for multi_json-1.3.2...
Installing RDoc documentation for activesupport-3.2.3...
Installing RDoc documentation for builder-3.0.0...
Installing RDoc documentation for activemodel-3.2.3...
Installing RDoc documentation for rack-1.4.1...
Installing RDoc documentation for rack-cache-1.2...
Installing RDoc documentation for rack-test-0.6.1...
Installing RDoc documentation for journey-1.0.3...
Installing RDoc documentation for hike-1.2.1...
Installing RDoc documentation for tilt-1.3.3...
Installing RDoc documentation for sprockets-2.1.3...
Installing RDoc documentation for erubis-2.7.0...
Installing RDoc documentation for actionpack-3.2.3...
Installing RDoc documentation for arel-3.0.2...
Installing RDoc documentation for tzinfo-0.3.33...
Installing RDoc documentation for activerecord-3.2.3...
Installing RDoc documentation for activeresource-3.2.3...
Installing RDoc documentation for mime-types-1.18...
Installing RDoc documentation for polyglot-0.3.3...
Installing RDoc documentation for treetop-1.4.10...
Installing RDoc documentation for mail-2.4.4...
Installing RDoc documentation for actionmailer-3.2.3...
Installing RDoc documentation for rake-0.9.2.2...
Installing RDoc documentation for thor-0.14.6...
Installing RDoc documentation for rack-ssl-1.3.2...
Installing RDoc documentation for json-1.7.0...
Installing RDoc documentation for rdoc-3.12...
Installing RDoc documentation for railties-3.2.3...
Installing RDoc documentation for bundler-1.1.3...
Installing RDoc documentation for rails-3.2.3...

自分と同様に何がおこるのか不安な人の為に全てのログを載せた。

最初、パスワード入力後、何も返ってこない状況が暫く続いたので不安だったが、上手くいったようだ。

再度、railsコマンドで確認してみる。

$ rails -v
Rails 3.2.3

上手くいったようだ。今回は、ここまで。


2012-04-29 [iOS][Web]iPhoneアプリケーションとサーバ間の通信(その2)

Ruby on Railsは初めてなので、ログ取りにうってつけな状況だ。Railsアプリケーションを配置するディレクトリを用意する。ディレクトリの場所には制約はないようなので、Documentsディレクトリ配下にrailsというディレクトリを作成して、そこにworkbookというアプリケーション環境を生成する。

$ rails new workbook
      create  
      create  README.rdoc
  ....
Enter your password to install the bundled RubyGems to your system: ←パスワード入力
  ....
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.

workbookアプリケーションのディレクトリ配下に移動して、雛形scaffoldを使って、コードを生成する。

$cd workbook/
$ rails generate scaffold person name:string age:integer
  ....
Could not find coffee-script-source-1.3.1 in any of the sources
Run `bundle install` to install missing gems.

自分の場合はエラーとなったので、素直にコメントの操作を実行して、成功するまで、コード生成を繰り返した。

$ bundle install
$ rails generate scaffold person name:string age:integer
Could not find sass-rails-3.2.5 in any of the sources
Run `bundle install` to install missing gems.
  ....
$ bundle install
$ rails generate scaffold person name:string age:integer
  ....
      invoke  scss
      create    app/assets/stylesheets/scaffolds.css.scss
$ rake db:migrate
==  CreatePeople: migrating ===================================================
-- create_table(:people)
   -> 0.0009s
==  CreatePeople: migrated (0.0010s) ==========================================

初心者の自分では、理由は分からないが、『iOSプログラミング逆引きリファレンス108』で説明されているとおり、設定を変更する。

$ cd app/controllers
$ vi application_controller.rb
  ....
$ cat application_controller.rb
class ApplicationController < ActionController::Base
  #protect_from_forgery
end

protect_from_forgeryをコメント・アウトする。

ところが、著者の場合、これでは上手くいかなかった。今の環境では、この手順だとHTMLとJSONのみの対応で、RESTはXMLということになると思うが、これに対応していない。また、驚いたのはpersonというモデルを作成したが、コントローラ名はこれの複数形のpeopleとなっていた。

RESTに対応される為、people_controller.rbで、JSONの記述をコピーして、JSONの部分をXMLに変更する。

具体的にいうと、例えば、

respond_to do |format|
      format.html # index.html.erb
      format.json { render :json => @people }
    end

となっている部分に、RESTに対応させるため、XMLのコードを追加する。

respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @people }  ←追加する。
      format.json { render :json => @people }
    end

Ruby on Railsやscaffoldが素晴らしいのだと思うが、JSONの記述を真似るだけで対応できた。全てを確認した訳ではないが。

これで、準備OK。サーバ側のアプリケーションを起動する。

$ rails server
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-04-29 07:53:27] INFO  WEBrick 1.3.1
[2012-04-29 07:53:27] INFO  ruby 1.8.7 (2010-01-10) [universal-darwin11.0]
[2012-04-29 07:53:27] INFO  WEBrick::HTTPServer#start: pid=1652 port=3000

余談だが、今回の試験で、サーバ側のDBを更新するが、初期状態に戻したくなると思うが、著者は以下の手順で対応した。

$ cd ~/Documents/rails/workbook/db
$ sqlite3 ./development.sqlite3
sqlite> delete from people;  ←全データを削除
sqlite> select * from people;  ←削除された事を確認。
sqlite> update sqlite_sequence set seq=0 where name='people';  ←カウンタをリセット。
sqlite> vacuum;
sqlite> .exit

iPhoneアプリケーション側のコードは、『iOSプログラミング逆引きリファレンス108』そのままなので、詳しくは説明しない。

- (IBAction)sendPost:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people.xml"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/xml" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"POST"];
    NSString    *content = @"Yukio MURAKAMI17";
    [request setHTTPBody:[content dataUsingEncoding:NSUTF8StringEncoding]];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSString    *t = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], s];
        self.textView.text = t;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}

RESTで気になるといえば、上記の場合、値を渡す際に、XML文字列をコードを埋め込む事だ。せっかくならCocoaの辞書型が使えたらと思うが、その場合は、JSONということになるようだ。JSONは、次回に取り上げる予定だ。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/REST - GitHub

_ 関連情報

Ruby on Rails
プロジェクトページ。
CHAPTER 5 Representational State Transfer (REST)
Fieldingの論文。
iOSプログラミング逆引きリファレンス108 ~知りたいことがすぐわかるiPhoneプログラミングテクニック~
全面的に、この書籍を参考にしています。助かります!
URL Loading System Programming Guide
Developerサイトの情報。

2012-04-30 [iOS][Web]iPhoneアプリケーションとサーバ間の通信(その3)

おそらく、Twitterフレームワークの為だと思うが、iOS5からNSJSONSerializationというJSONを扱うクラスが追加された。

このクラスは非常にシンプルで、用意されているメソッドは以下の5つだけだ。

+ JSONObjectWithData:options:error:
JSONデータを配列(NSArray)/辞書(NSDictionary)に変換して返す。
+ JSONObjectWithStream:options:error:error
ストリームから得られたJSONデータを配列(NSArray)/辞書(NSDictionary)に変換して返す。
+ dataWithJSONObject:options:error:
配列(NSArray)/辞書(NSDictionary)をJSONデータに変換して返す。
+ writeJSONObject:toStream:options:error:
配列(NSArray)/辞書(NSDictionary)をJSONデータに変換してストリームに書き込む。
+ isValidJSONObject:
指定されたFondationのidがJSONデータに変換可能か確認する(だと思う)

ようするに、JSONデータとFondationの配列/辞書に相互に変換するメソッド。JSONデータについては、NSDataとストリームの2種類に対応ということだ。

これで、前回のRESTを使ったアプリケーションをJSON対応に変更できるはずだ。ただ、まだ、NSJSONSerializationを使った情報が世の中には少ないのと、railsアプリケーションが返す結果が、著者が想像していたのと事なってので、試行錯誤はあったが。

以下が、試行錯誤したコードだ。

- (IBAction)sendPost:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"POST"];
    NSMutableDictionary *person = [NSMutableDictionary dictionary];
    [person setValue:@"MURAKAMI Yukio" forKey:@"name"];
    [person setValue:@"18" forKey:@"age"];
    NSError *error = nil;
    NSData  *content = [NSJSONSerialization dataWithJSONObject:person options:NSJSONWritingPrettyPrinted error:&error];
    [request setHTTPBody:content];
    NSHTTPURLResponse   *response = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendGet:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people/1.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"GET"];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    NSDictionary    *content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendGetList:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"GET"];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    NSDictionary    *content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendPut:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people/1.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"PUT"];
    NSMutableDictionary *person = [NSMutableDictionary dictionary];
    [person setValue:@"MURAKAMI Yukio" forKey:@"name"];
    [person setValue:@"81" forKey:@"age"];
    NSError *error = nil;
    NSData  *content = [NSJSONSerialization dataWithJSONObject:person options:NSJSONWritingPrettyPrinted error:&error];
    [request setHTTPBody:content];
    NSHTTPURLResponse   *response = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}
 
- (IBAction)sendDelete:(id)sender
{
    NSURL   *url = [NSURL URLWithString:@"http://localhost:3000/people/1.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"DELETE"];
    NSHTTPURLResponse   *response = nil;
    NSError *error = nil;
    NSData  *data = [NSURLConnection sendSynchronousRequest:request
                                          returningResponse:&response
                                                      error:&error];
    NSDictionary    *content = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
    if (!error) {
        NSString    *s = [[NSString alloc] initWithFormat:@"status code: %d\ndata: %@", [response statusCode], content];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
    else {
        NSString    *s = [[NSString alloc] initWithFormat:@"error: %@", error];
        self.textView.text = s;
        NSLog(@"%@", self.textView.text);
    }
}

JSONのContent-Typeは、application/jsonなのですね。RESTの時のapplication/xmlのままにしていたら、railsアプリケーションの方でXMLとして処理をしようとして、エラーになってしまった。

また、成功したら、HTTPステータスは200だと思っていたら、そうではなかった。それは、railsアプリケーションのpeople_controller.rbで以下のように記述しているからだと思う。

  # POST /people
  # POST /people.json
  def create
    @person = Person.new(params[:person])
respond_to do |format| if @person.save format.html { redirect_to @person, :notice => 'Person was successfully created.' } format.xml { render :xml => @person, :status => :created, :location => @person } format.json { render :json => @person, :status => :created, :location => @person } else format.html { render :action => "new" } format.xml { render :xml => @person.errors, :status => :unprocessable_entity } format.json { render :json => @person.errors, :status => :unprocessable_entity } end end end

Rubyを知らず、仕様も確認しないで当てずっぽうだが、POSTの場合、:statusを:createdに設定しているのでHTTPステータスは201(created)を返す。

  # PUT /people/1
  # PUT /people/1.json
  def update
    @person = Person.find(params[:id])
respond_to do |format| if @person.update_attributes(params[:person]) format.html { redirect_to @person, :notice => 'Person was successfully updated.' } format.xml { head :no_content } format.json { head :no_content } else format.html { render :action => "edit" } format.xml { render :xml => @person.errors, :status => :unprocessable_entity } format.json { render :json => @person.errors, :status => :unprocessable_entity } end end end

PUTの場合、:statusを:no_contentに設定しているので、HTTPステータスは204(no content)を返すようだ。何故、エラーとなるのか、少し、驚いてしまった。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/JSON - GitHub

トップ 最新 追記