iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
やはり、Cocoa勉強会は参加すると得る物がある。先日のCocoa勉強会で教えてもらった事だ。
UIWebViewのstringByEvaluatingJavaScriptFromString:を使った方法だと、HTMLコンテンツ側からは、UIWebViewDelegateのデリゲート・メソッドが呼ばれるタイミングでしかイベントを発生させる事ができない。そこで、任意のタイミングでイベントを発生させる方法に挑戦する。ヒントは、以前紹介したFacebook SDKで得られた情報だ。
イベント通知用のURLスキームを追加する。
サンプルでは、webapp-demoとした。
このURLスキームを受けるデリゲート・メソッドを追加する。
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
DBGMSG(@"%s, url(%@)", __func__, url);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"demo"
message:@"WebApp"
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
[alert show];
return YES;
}
それでは、HTMLコンテンツに、このURLスキームを呼ぶコードを追加してみよう。
<a href="webapp://demo/debug?abc=123">call WebApp</a><br />
選択してみる。
アラートが表示された。
2012-07-02 09:26:59.438 WebApp[2366:f803] -[AppDelegate application:openURL:sourceApplication:annotation:], url(webapp-demo://WebApp.demo/debug?abc=123)
最初、URLスキームとしてWebAppを登録していたが、おそらく、なにかとぶつかったのか上手くいかなかった。使用するURLスキームの選択は重要なようだ。
『iPhone デベロッパーズ クックブック』で紹介されていた手順で、一定時間が経過すると自動で閉じるアラートの例だ。
タイマーから呼ばれる、アラートを閉じるメソッド。
- (void)performDismiss:(NSTimer *)theTimer
{
UIAlertView *alertView = [theTimer userInfo];
[alertView dismissWithClickedButtonIndex:0 animated:NO];
}
以前のiOSでは、アプリケーション側からアラートを閉じる状況は少なかった為か、『iPhone デベロッパーズ クックブック』では奇抜な方法という感じの説明だったが、アプリケーションがバックグラウンドに移行出来るようになって、バックグラウンドに遷移する際に、開いていたアラートを閉じるか、そのままにするかをアプリケーション側で判断する事になったので、この手順も普通の処理になったと思う。
アラート表示時にタイマーを仕込む。
- (IBAction)alertDismiss:(id)sender
{
UIAlertView *alertView = [[UIAlertView alloc] init];
alertView.delegate = self;
alertView.title = @"Alert Dismiss";
alertView.message = @"\n\ndismiss alert";
[NSTimer scheduledTimerWithTimeInterval:3.0f
target:self
selector:@selector(performDismiss:)
userInfo:alertView repeats:NO];
[alertView show];
}
自動で閉じる為、ボタンを用意しなかったが、ボタンがあることを前提としてレイアウトになるようで不格好となってしまった。
iOS SDKがCoreDataに対応してからsqliteを直接利用する機会は減ったと思われるが、やはり、知っていた方が便利なので挑戦する。
以前は、Apple DeveloperサイトでSQLiteBooksといるサンプル・コードが入手できたが、今はないようだ。ただ、著者は以前入手しておいたので、それを参考にした。良かった!
sqliteを使用する為には、『libsqlite3.0.dylib』をリンクし、sqlite3.hをインクルードする。
#import <sqlite3.h>
今回は、sqliteとのやり取りを隠す、Databaseクラスを用意した。以下がヘッダーだ。
#import <Foundation/Foundation.h>
#import <sqlite3.h>
@interface Database : NSObject
@property (nonatomic, assign) sqlite3 *database;
- (void)demo;
@end
内容は以下のとおり。初期化と破棄、そして、簡単なSQLの実行。
#import "Database.h"
@interface Database ()
@end
@implementation Database
@synthesize database = _database;
- (id)init
{
DBGMSG(@"%s", __func__);
self = [super init];
if (self) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:@"database.sql"];
if (sqlite3_open([path UTF8String], &_database) != SQLITE_OK) {
sqlite3_close(_database);
self.database = NULL;
DBGMSG(@"Failed to open database with message '%s'.", sqlite3_errmsg(_database));
}
}
return self;
}
- (void)dealloc
{
DBGMSG(@"%s", __func__);
if (sqlite3_close(_database) != SQLITE_OK) {
DBGMSG(@"Error: failed to close database with message '%s'.", sqlite3_errmsg(_database));
}
self.database = NULL;
/* [super dealloc]; */
}
- (void)demo
{
DBGMSG(@"%s", __func__);
if (NULL == self.database) return;
const char *sql = "CREATE TABLE demo ('id' INTEGER PRIMARY KEY, 'name' CHAR(32))";
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(self.database, sql, -1, &statement, NULL) == SQLITE_OK) {
DBGMSG(@"[OK]sqlite3_prepare_v2(), srl: %s", sql);
while (sqlite3_step(statement) == SQLITE_ROW) {
DBGMSG(@"[OK]sqlite3_step()");
}
}
sqlite3_finalize(statement);
sql = "INSERT INTO demo(name) VALUES ('test')";
if (sqlite3_prepare_v2(self.database, sql, -1, &statement, NULL) == SQLITE_OK) {
DBGMSG(@"[OK]sqlite3_prepare_v2(), srl: %s", sql);
while (sqlite3_step(statement) == SQLITE_ROW) {
DBGMSG(@"[OK]sqlite3_step()");
}
}
sqlite3_finalize(statement);
sql = "SELECT id FROM demo";
if (sqlite3_prepare_v2(self.database, sql, -1, &statement, NULL) == SQLITE_OK) {
DBGMSG(@"[OK]sqlite3_prepare_v2(), srl: %s", sql);
while (sqlite3_step(statement) == SQLITE_ROW) {
DBGMSG(@"[OK]sqlite3_step()");
int primaryKey = sqlite3_column_int(statement, 0);
NSLog(@"primaryKey: %d", primaryKey);
}
}
sqlite3_finalize(statement);
}
@end
サンプルでは、demoメソッドを呼ぶボタンを用意して、動作確認した。上手くいったようだ。
2012-07-03 20:20:28.602 Database[7795:f803] -[Database init]
2012-07-03 20:20:31.391 Database[7795:f803] -[Database demo]
2012-07-03 20:20:31.393 Database[7795:f803] [OK]sqlite3_prepare_v2(), srl: CREATE TABLE demo ('id' INTEGER PRIMARY KEY, 'name' CHAR(32))
2012-07-03 20:20:31.461 Database[7795:f803] [OK]sqlite3_prepare_v2(), srl: INSERT INTO demo(name) VALUES ('test')
2012-07-03 20:20:31.464 Database[7795:f803] [OK]sqlite3_prepare_v2(), srl: SELECT id FROM demo
2012-07-03 20:20:31.464 Database[7795:f803] [OK]sqlite3_step()
2012-07-03 20:20:31.465 Database[7795:f803] primaryKey: 1
避けてきた分野に3Dがある。でも、まったく避けていただけでは進歩がないので、理解できていないながらも、簡単な何かをやろうと思う。三角形を描画するだけに挑戦する。
プロジェクトにGLKit.frameworkとOpenGLES.frameworkを追加する。
GLKViewControllerのサブクラスを作成する。
#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>
@interface PrimitiveViewController : GLKViewController
@end
書籍を読みながら、理解していないながらもなんとか記述。
#import "PrimitiveViewController.h"
@interface PrimitiveViewController ()
@property (strong, nonatomic) EAGLContext *context;
@property (assign, nonatomic) GLuint defaultFramebuffer;
@property (assign, nonatomic) GLuint colorRenderbuffer;
@property (assign, nonatomic) GLuint depthRenderbuffer;
- (void)setupGL;
- (void)tearDownGL;
@end
@implementation PrimitiveViewController
@synthesize context = _context;
@synthesize defaultFramebuffer = _defaultFramebuffer;
@synthesize colorRenderbuffer = _colorRenderbuffer;
@synthesize depthRenderbuffer = _depthRenderbuffer;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
self.defaultFramebuffer = 0;
self.colorRenderbuffer = 0;
self.depthRenderbuffer = 0;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
if (!self.context) {
NSLog(@"Failed to create ES context");
}
GLKView *view = (GLKView *)self.view;
view.context = self.context;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
[self setupGL];
}
- (void)viewDidUnload
{
[self tearDownGL];
if ([EAGLContext currentContext] == self.context) {
[EAGLContext setCurrentContext:nil];
}
self.context = nil;
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
- (void)setupGL
{
[EAGLContext setCurrentContext:self.context];
glGenFramebuffersOES(1, &_defaultFramebuffer);
glBindFramebufferOES(GL_FRAMEBUFFER_OES, self.defaultFramebuffer);
glGenRenderbuffersOES(1, &_colorRenderbuffer);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, self.colorRenderbuffer);
glRenderbufferStorageOES(GL_RENDERBUFFER_OES,
GL_RGBA8_OES,
self.view.bounds.size.width,
self.view.bounds.size.height);
glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
GL_COLOR_ATTACHMENT0_OES,
GL_RENDERBUFFER_OES,
self.colorRenderbuffer);
glGenRenderbuffersOES(1, &_depthRenderbuffer);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, self.depthRenderbuffer);
glRenderbufferStorageOES(GL_RENDERBUFFER_OES,
GL_DEPTH_COMPONENT16_OES,
self.view.bounds.size.width,
self.view.bounds.size.height);
glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
GL_DEPTH_ATTACHMENT_OES,
GL_RENDERBUFFER_OES,
self.depthRenderbuffer);
GLenum status = glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) ;
if (status != GL_FRAMEBUFFER_COMPLETE_OES) {
DBGMSG(@"failed to make complete framebuffer object %x", status);
}
}
- (void)tearDownGL
{
[EAGLContext setCurrentContext:self.context];
if (self.defaultFramebuffer) {
glDeleteFramebuffersOES(1, &_defaultFramebuffer);
self.defaultFramebuffer = 0;
}
if (self.colorRenderbuffer) {
glDeleteRenderbuffersOES(1, &_colorRenderbuffer);
self.colorRenderbuffer = 0;
}
if (self.depthRenderbuffer) {
glDeleteRenderbuffersOES(1, &_depthRenderbuffer);
self.depthRenderbuffer = 0;
}
}
#pragma mark - GLKView and GLKViewController delegate methods
/*
- (void)update
{
}
*/
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
glClear(GL_COLOR_BUFFER_BIT);
/* 頂点の定義 */
GLfloat vertices[] = {
-0.5f, -0.5f,
0.5f, -0.5f,
0.0f, 0.5f,
};
/* カラー(RGBA)の定義 */
GLubyte colors[] = {
255, 0, 0, 255,
255, 0, 0, 255,
255, 0, 0, 255,
};
/* 頂点配列とカラー配列を有効 */
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
/* 頂点配列とカラー配列を設定 */
glVertexPointer(2 , GL_FLOAT , 0 , vertices);
glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors);
/* 描画 */
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
}
@end
実行。
描画で必要な状態やデータをオブジェクトという塊で扱っている。前回のコードで利用していたオブジェクトは以下のとおり。
OpenGL ESの状態は、レンダリングコンテキストと呼ばれるものに格納されている。これがあるため、他のアプリケーションから独立して描画する事が出来るのだが、そのために、上記のオブジェクトはバインドと呼ばれるコマンドで、コンテキストと関係づけて利用する。
iOSでは、EAGLContextクラスを通してレンダリングコンテキストを操作する事になる。
/* GLKViewControllerのサブクラス */
/* OpenGL ES 1.1コンテキストを作成 */
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
/* GLKitViewに設定 */
GLKView *view = (GLKView *)self.view;
view.context = self.context;
view.drawableDepthFormat = GLKViewDrawableDepthFormatNone;
/* 現在のコンテキストに設定 */
[EAGLContext setCurrentContext:self.context];
フレームバッファを1個生成して、それを現在のコンテキストに関連づける。
glGenFramebuffersOES(1, &_defaultFramebuffer);
glBindFramebufferOES(GL_FRAMEBUFFER_OES, self.defaultFramebuffer);
レンダバッファを1個生成して、それを現在のコンテキストに関連づける。レンダバッファ領域をビューの大きさ分生成し、それをさっき生成したレンダバッファに色データとして設定する。
glGenRenderbuffersOES(1, &_colorRenderbuffer);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, self.colorRenderbuffer);
glRenderbufferStorageOES(
GL_RENDERBUFFER_OES,
GL_RGBA8_OES,
self.view.bounds.size.width,
self.view.bounds.size.height);
glFramebufferRenderbufferOES(
GL_FRAMEBUFFER_OES,
GL_COLOR_ATTACHMENT0_OES,
GL_RENDERBUFFER_OES,
self.colorRenderbuffer);
フレームバッファの状態に問題がないか確認する。
GLenum status = glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) ;
if (status != GL_FRAMEBUFFER_COMPLETE_OES) {
DBGMSG(@"failed to make complete framebuffer object %x", status);
}
これで準備が整ったので、描画をとりかかる。
レンダバッファの消去。今回は色のレンダバッファのみだ。
glClear(GL_COLOR_BUFFER_BIT);
頂点配列とカラー配列を有効にする。
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
頂点配列を設定する。
GLfloat vertices[] = {
-0.5f, -0.5f,
0.5f, -0.5f,
0.0f, 0.5f,
};
glVertexPointer(2 , GL_FLOAT , 0 , vertices);
第一引数はxとy座標のデータなので2を指定。第二引数は浮動小数点を指定。
色配列を設定する。
GLubyte colors[] = {
255, 0, 0, 255,
255, 0, 0, 255,
255, 0, 0, 255,
};
glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors);
第一引数はRGBAなので4を指定。第二引数はバイト単位を指定。
そして、三角形を描画。
glDrawArrays(GL_TRIANGLES, 0, 3);
以前のプロジェクトでnetstatのログ解析でお世話になっていたコマンドbeforeafter。特に不満はないのだが、著作権の扱いがよく分からない為、ほぼ、同じ動きをするPerlスクリプトを用意してみた。
#!/usr/bin/perl
use strict;
use warnings;
# netstat -s > netstat.before
# ...
# netstat -s > netstat.after
# beforeafter.pl netstat.before netstat.after
if ($#ARGV != 1) {
print 'Usage: beforeafter.pl before_file after_file', "\n";
exit 1;
}
my $filename1 = $ARGV[0];
my $filename2 = $ARGV[1];
open FH1, "<$filename1" or die "error: $filename1: $!\n";
open FH2, "<$filename2" or die "error :$filename2: $!\n";
my $c1 = '';
my $c2 = '';
my $d1 = 0;
my $d2 = 0;
my $separator1 = 0;
my $separator2 = 0;
while (not eof FH1) {
$c1 = getc FH1;
if ($c1 =~ /\s|\t|:|\(|\n/) {
print "$c1";
$separator1 = 1;
}
elsif ($c1 =~ /\D/) {
print "$c1";
$separator1 = 0;
}
else {
if ($separator1 == 0) {
print "$c1";
next;
}
my $n1 = $c1;
$d1 = $n1;
while (not eof FH1) {
$c1 = getc FH1;
if ($c1 =~ /\d/) {
$n1 = $c1;
$d1 = (10 * $d1) + $n1;
}
else {
last;
}
}
while (not eof FH2) {
$c2 = getc FH2;
if ($c2 =~ /\s|\t|:|\(|\n/) {
$separator2 = 1;
}
elsif ($c2 =~ /\D/) {
$separator2 = 0;
}
else {
if ($separator2 == 0) {
next;
}
my $n2 = $c2;
$d2 = $n2;
while (not eof FH2) {
$c2 = getc FH2;
if ($c2 =~ /\d/) {
$n2 = $c2;
$d2 = (10 * $d2) + $n2;
}
else {
last;
}
}
last;
}
}
my $delta = $d2 - $d1;
print "$delta";
if (not eof FH1) {
print "$c1";
}
}
}
close FH1;
close FH2;
# End Of File
使い方は以下のとおり。
$ netstat -s > netstat.before
...何らかの処理をする...
$ netstat -s > netstat.after
$ beforeafter.pl netstat.before netstat.after
グラフ描画ライブラリSimpleChartを制作する際に参考にしたのが、iOSのテーブル・ビュー。
データ一式を渡して、ビューが頑張って表示するという方式もあると思うが、テーブル・ビューは、データそのものは、データ・ソースのプロトコルに対応したクラス側で管理し、テーブル・ビューに対して、件数や内容を返す方式となっている。
SimpleChartが参考にしているS7GraphViewも、基本的にはテーブル・ビューに似た方式だが、配列そのものを渡された表示している等、異なる部分もあるので、これをテーブル・ビュー的な方式にしてみた。
@class SimpleChartView;
@protocol SimpleChartViewDataSource
- (NSUInteger)numberOfPlotsInSimpleChartView:(SimpleChartView *)simpleChartView;
- (NSUInteger)numberOfXValuesInSimpleChartView:(SimpleChartView *)simpleChartView;
- (NSUInteger)simpleChartView:(SimpleChartView *)simpleChartView numberOfYValuesInPlot:(NSUInteger)plotIndex;
- (id)simpleChartView:(SimpleChartView *)simpleChartView XValueAtIndex:(NSUInteger)index;
- (NSNumber *)simpleChartView:(SimpleChartView *)simpleChartView YValueAtPlot:(NSUInteger)plotIndex value:(NSUInteger)valueIndex;
@optional
- (BOOL)simpleChartView:(SimpleChartView *)simpleChartView shouldFillPlot:(NSUInteger)plotIndex;
@end
@protocol SimpleChartViewDelegate
@optional
@end
@interface SimpleChartView : UIView
@property (nonatomic, weak) IBOutlet id dataSource;
@property (nonatomic, strong) IBOutlet NSFormatter *xValuesFormatter;
@property (nonatomic, strong) IBOutlet NSFormatter *yValuesFormatter;
@property (nonatomic, assign) BOOL drawAxisX;
@property (nonatomic, assign) BOOL drawAxisY;
@property (nonatomic, assign) BOOL drawGridX;
@property (nonatomic, assign) BOOL drawGridY;
@property (nonatomic, strong) UIColor *xValuesColor;
@property (nonatomic, strong) UIColor *yValuesColor;
@property (nonatomic, strong) UIColor *gridXColor;
@property (nonatomic, strong) UIColor *gridYColor;
@property (nonatomic, assign) BOOL drawInfo;
@property (nonatomic, copy) NSString *info;
@property (nonatomic, strong) UIColor *infoColor;
- (void)reloadData;
@end
配列データのアクセスで、いちいち、メソッドを呼び出すのは冗長だと思うが、どうだろか。あ、これは内部の話なので、利用側にはかんけいないか。
_ [OSX]MainMenu.xibのメニーにアクションを設定する
某所で、nibのメニューの項目をソースコードで設定する方法の説明があったので自分で試してみた。
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
NSMenuItem *menuItem = [mainMenu itemWithTitle:@"File"];
NSLog(@"%@", menuItem);
NSMenu *subMenu = [menuItem submenu];
menuItem = [subMenu itemWithTitle:@"Close"];
NSLog(@"%@", menuItem);
[menuItem setTarget:self];
[menuItem setAction:@selector(demo:)];
}
- (IBAction)demo:(id)sender
{
NSLog(@"%s", __func__);
}
nibで定義したメニーに対してという事で、NSApplicationのmainMenuメソッドでNSMenuのインスタンスを取得する。
これは、メニューの塊のようなので、itemWithTitle:メソッドでタイトルからNSMenuItemのインスタンスを取得する。
ここで取得できるのは、メニューバーに対する、TOPのFileやEditメニューの塊のようなので、さらに取得できたものに対して、ここでは"Close"メニューを取得する。
これに対して、setTarget:とsetAction:を行えば、File/Closeメニューを選択されると、ここで設定したメソッドが呼ばれるようになる。
2012-07-09 22:14:14.668 Demo[13089:403]
2012-07-09 22:14:14.671 Demo[13089:403]
2012-07-09 22:14:21.790 Demo[13089:403] -[AppDelegate demo:]
CoreDataのエンティティは、そのままの状態だと管理オブジェクトクラスで操作する事になる。
/* 値の取得 */
[管理オブジェクト valueForKey:@"キー名"]
/* 値の設定 */
[管理オブジェクト setValue:値 forKey:@"キー名"];
規模が大きくなり、これだと煩雑だったり、単純な属性の参照/設定以上の事を行いたい場合、カスタム管理オブジェクトクラスの作成を考えると思う。
サンプル・コードのエンティティ属性を読み書きしている箇所を以下のように変更する。
[newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
↓
newManagedObject.timeStamp = [NSDate date];
cell.textLabel.text = [[object valueForKey:@"timeStamp"] description];
↓
cell.textLabel.text = [object.timeStamp description];
このカスタム管理オブジェクトクラスは、管理オブジェクトモデルのエンティティから生成という手段のみで作成される。つまり、新規作成のみで作成済みの場合は上書き生成となる。
もし、カスタム管理オブジェクトクラスに自分独自のカスタマイズを施している場合、上書き生成は困るのでどうすればいいのか?岸川さんの日記『CoreData の NSManagedObject のサブクラスを変更する場合はカテゴリを使うと便利』が参考になった。
例えば、カスタム管理オブジェクトクラスのファイル名が、Event.[hm] の場合、独自のカスタマイズ・コードをこのファイルに記述するのではなくて、例えば、Event_description.[hm] という名前のカテゴリを追加で作成する。
#import "Event.h"
@interface Event(description)
- (NSString *)description; /* override */
@end
@implementation Event(description)
- (NSString *)description
{
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy/MM/dd HH:mm:ss"];
NSString *str = [formatter stringFromDate:self.timeStamp];
return str;
}
@end
サンプルでは、EventクラスのtimeStampプロパティのdescriptionをセルに設定していたが、カテゴリでオーバーライドしたEventクラスのdescriptionを設定するように変更する。
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Event *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [object description];
}
実行。拡張した形式で表示されている。
先日のmosa entrance #21で思いついた事だ。
以前、C++を使っていた頃。そう、AT&Tからcfrontという名前でC言語のプリプロセッサとしてリリースされていた事の話。C++はnewしなくても変数宣言して使えて、この自動変数の寿命にタイミング、構築子/破滅子でデバッグ出力させる事によって、関数トレースをさせていた。
Objective-CのARCは、alloc/initされたクラスのインスタンスが、自動変数のような振る舞いをするので、もしかしたら、同じ事が出来るのではと思って、試してみた。
@interface DebugMessage : NSObject
- (id)initWithString:(NSString *)aString;
@end
@interface DebugMessage ()
@property (strong, nonatomic) NSString *dbgMsg;
@end
@implementation DebugMessage
@synthesize dbgMsg = _dbgMsg;
- (id)init
{
return [self initWithString:@""];
}
- (id)initWithString:(NSString *)aString
{
self = [super init];
if (self) {
self.dbgMsg = [[NSString alloc] initWithString:aString];
if (self.dbgMsg) {
NSLog(@"[BEGIN]%@", self.dbgMsg);
}
}
return self;
}
- (void)dealloc
{
if (self.dbgMsg) {
NSLog(@"[END]%@", self.dbgMsg);
}
self.dbgMsg = nil;
/* [super dealloc]; */
}
@end
#ifdef DEBUG
#define DBGMSG(...) NSLog(__VA_ARGS__)
#define TRC(aString) DebugMessage *dbgmsg = [[DebugMessage alloc] initWithString:aString]
#else /* DEBUG */
#define DBGMSG(...)
#define TRC(aString)
#endif /* DEBUG */
このTRC()マクロを以下のように宣言すれば!
- (void)viewDidLoad
{
TRC(@"ViewController # - viewDidLoad");
[super viewDidLoad];
}
-viewDidLoadメソッドの呼び出し前後で、トレースのメッセージが出力されている!
2012-07-12 19:29:46.204 DebugMessage[22897:f803] [BEGIN]ViewController # - viewDidLoad
2012-07-12 19:29:46.204 DebugMessage[22897:f803] [END]ViewController # - viewDidLoad
ただ、実際にサンプル・コードをビルドして実行すると分かるのだが、ARCが使えるビルド環境だと、マクロで宣言した変数dbgmsgが使われていないとか、だったら、メソッド呼び出しで変数は宣言しないようにしたら、今度は戻り値を受け取っていないとか、ビルド時に警告メッセージが出力されてしまう。
これは、大失敗のようだ。
Chrome Tech Talk Night #3に参加してきた。
今回のChrome Tech Talk Night #3は、先日開催されたGoogle I/O 2012のChrome Extension関連を抜き出して、日本で再演する、という意図で開催されたイベントだ。
そもそも、Extensionsとは?だが、HTML/CSS/JavaScriptを使って、クライアント側のWebブラウザを拡張するモジュールの事で、著名なものとして、Safari ExtensionsとChrome Extensionsがある。
APIを統一して欲しい!という気持ちがあるが、近年のスマートフォンのネイティブ・アプリケーションのことを考えると、APIの統一は制作者側の問題で、ユーザにとってはどうでもよく、より良いユーザ体験を実現する為には、そのプラットフォームの能力を最大限発揮できるAPIが望ましいので、仕方がないのかな?
さて、今回のセッションの内容を簡単にまとめると、以下のとおり。
以前のExtensionsのAPIだと、普通のExtensions開発者へのセキュリティやリソース管理に対する負担が多きかったと考え、次のManifest Ver.2では改善する。ただし、徐々にだが来年、今のManifest Ver.1の対応は止める。つまり、動かなくなるという事。
これは、将来、スマートフォン版ChromeのExtensions対応も考慮に入れた判断。
次に、Chrome Appsが変わるというお話。開発中だが、ネイティブ・アプリと遜色ないChromeアプリケーションが開発できるように努力しているということ。
二つともだが、今回の変更等で都合が悪い開発者があったらフィードバックが欲しい。今後の開発の参考にしたいという事だ。
グラフ描画ライブラリSimpleChart。まだ手を加えたい箇所があるが終わらないので、今の状態でGitHubのREADMEを作成し、正式公開とする。
SimpleChartは、SimpleChartView.hとSimpleChartView.cの2つのファイルで構成されていて、公開しているプロジェクトがサンプル・プログラムになっているので、使い方はサンプル・コードで確認して欲しい。
そもそも、クラス宣言にPublic/Privateがあるのは何に対してか?という気がしてきたのだが、Objective-Cで非公開。C言語でいうところのモジュール(ファイル)に閉じた変数/関数を宣言するにはどうするのか?が今回のお話。
ヘッダーファイル。
@interface MyClass : NSObject
@property (nonatomic, strong) クラス名 *公開プロパティ;
@property (nonatomic, assign, readonly) float value;
- (void)公開メソッド;
@end
.mファイル。
@interface MyClass ()
@property (nonatomic, strong) クラス名 *非公開プロパティ;
@property (nonatomic, assign, readwrite) float value;
- (void)非公開メソッド;
@end
@implementation Lisp
@synthesiz 公開プロパティ = _公開プロパティ;
@synthesiz 非公開プロパティ = _非公開プロパティ;
@synthesiz value = _value;
- (void)公開メソッド
{
}
- (void)非公開メソッド
{
}
@end
非公開にしたいインスタンス変数、プロパティ、メソッドを.mファイル内の名前が空のカテゴリで宣言すれば、非公開のインスタンス変数、プロパティ、メソッドになる。その際、ヘッダーファイルではreadonlyで宣言し、内部ではreadwriteにするという事も出来る。
著者は、これを匿名(無名)カテゴリと思っていたのだが、正確にはクラス拡張と呼ぶらしい。
以前、『[iOS][Web]iPhoneアプリケーションとサーバ間の通信』の回でRuby on Railsを取り上げたが、今回は、Railsそのものを取り上げる。
以前の回では、~/Doccuments/rails/配下に、workbookというアプリケーション環境を生成したが、今回は、その並びにweblogというアプリケーション環境を生成する。
$ cd ~/Doccuments/rails/
$ rails new weblog
$ cd weblog
$ ls
Gemfile app doc script
Gemfile.lock config lib test
README.rdoc config.ru log tmp
Rakefile db public vendor
$ rake db:create
TOPページを静的コンテンツから、動的なものに変更する。
$ rails generate controller home index
$ vi app/views/home/index.html.erb
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<h1>Hello, Rails!</h1> ←追加
$ mv public/index.html public/old.index.html
$ vi config/routes.rb
Weblog::Application.routes.draw do
get "home/index"
root :to => 'home#index' ←追加
end
サーバ起動。
$ rails server
「http://localhost:3000/」にアクセスすると、「index.html.erb」に追加した文言が表示されているはずだ。
画像を使ったサービスの一つとして15パズルというスライディング・ブロック・パズルの一種の制作を考えている。
実装方法については、Cocoa勉強会(関西)で知り合いましたSTUDIO SHIN北村真二さんの記事『実践!iOSで作るゲームアプリ』を参考にした。北村さん、ありがとう!
まず、クラス構造だが、当初は、MVCのモデルにあたるDocumentクラスは、コントローラにあたるViewControllerでゲームの制御を行う事を考えていたが、ゲーム本来の制御とアプリの枠側とは切り離したいだとか、iPhone/iPadの両方に対応させる場合、機器の違いを吸収するViewControllerには持たせない方がいいのでは?と考え、ゲーム専用のコントローラクラスを用意しることにした。結局、北村さんの記事のとおりという事になってしまった。
今回の発表で作成したのは、画面に16個のマスを表示し、1個の駒をドラッグで移動するというところまでだ。
ビューコントローラでゲームの制御を行うのでなく、ゲーム用のコントローラを用意することにしたので、ビューコントローラでは描画が必要になったらゲームコントローラを生成するだけだ。
@implementation ViewController
...
- (void)viewDidLoad
{
[super viewDidLoad];
self.gameController = [[GameController alloc] initWithView:(GameBoardView *)self.view];
}
...
@end
ゲーム盤に関係するビュー等の生成は、GameBoardViewクラスと考えていたが、実際にプログラミングしてみると、ビューからは他のビューの生成が完了したか分からない事に気がついた。なので、GameControllerがGameBoardViewに初期化のタイミングを教える事にした。
@implementation GameController
...
- (id)initWithView:(GameBoardView *)view
{
self = [super init];
if (self) {
self.gameBoardView = view;
[self.gameBoardView setupWithDelegate:self];
}
return self;
}
...
@end
GameBoardViewに対するユーザ操作はデリゲートでGameControllerに伝えられる。なので、タッチダウンされたら何がタッチされたのか覚えておいて、ドラッグ中は駒を移動させ、タッチアップで駒をマスの位置に移動させた。
@implementation GameController
...
- (void)gameBoardViewTouchDown:(GameBoardView *)gameBoardView location:(CGPoint)touchPt taps:(int)taps event:(UIEvent*)event
{
GameSquare *square = [self.gameBoardView squareAtPoint:touchPt];
GamePieceView *pieceView = [self.gameBoardView pieceViewAtPoint:touchPt];
if (pieceView) {
self.pieceView = pieceView;
self.startLocation = touchPt;
}
}
- (void)gameBoardViewTouchMove:(GameBoardView *)gameBoardView location:(CGPoint)touchPt taps:(int)taps event:(UIEvent*)event
{
if (self.pieceView) {
CGRect frame = [self.pieceView frame];
frame.origin.x += touchPt.x - self.startLocation.x;
frame.origin.y += touchPt.y - self.startLocation.y;
self.startLocation = touchPt;
[self.pieceView setFrame:frame];
}
}
- (void)gameBoardViewTouchUp:(GameBoardView *)gameBoardView location:(CGPoint)touchPt taps:(int)taps event:(UIEvent*)event
{
if (self.pieceView) {
GameSquare *square = [self.gameBoardView squareAtPoint:touchPt];
CGRect frame = [square frame];
[self.pieceView setFrame:frame];
}
self.pieceView = nil;
}
...
@end
15個の駒が全て揃った際は、タッチアップの際に、移動可能かどうかの判定をして、可能なら移動する。その際にアニメーションで、ということになる予定だ。
想定通り、マスが描画さいれているか確認するため、数字を印字しているが、反転してしまっている。デバッグ目的の簡易な実装なので、気にしないで欲しい。
駒の移動にアニメーションを施す。
前回のサンプル・コードでは、駒のフレームに、移動先のマスのフレームを設定していた。これをアニメーションに変更する。
ゲームコントローラで、駒の移動をGamePieceViewのメソッドmoveWithSquare:に変更する。
@implementation GameController
...
- (void)gameBoardViewTouchUp:(GameBoardView *)gameBoardView location:(CGPoint)touchPt taps:(int)taps event:(UIEvent*)event
{
if (self.pieceView) {
GameSquare *square = [self.gameBoardView squareAtPoint:touchPt];
[self.pieceView moveWithSquare:square]; ←変更
}
self.pieceView = nil;
}
...
@end
moveWithSquare:メソッドの処理内容は以下の通り。
@implementation GamePieceView
...
- (void)moveWithSquare:(GameSquare *)square
{
CGRect frame = [square frame];
[self moveFrame:frame];
}
- (void)moveFrame:(CGRect)frame
{
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
CGPoint fromPt = self.layer.position;
CGPoint toPt = CGPointMake(frame.origin.x + (frame.size.width / 2.0),
frame.origin.y + (frame.size.height / 2.0));
theAnimation.fromValue = [NSValue valueWithCGPoint:fromPt];
theAnimation.toValue = [NSValue valueWithCGPoint:toPt];
theAnimation.delegate = self;
[self.layer addAnimation:theAnimation forKey:@"animatePosition"];
self.layer.frame = frame;
}
...
@end
ドラッグして指を離すと、アニメーションしてマスに駒がはまるようになったと思う。
1個の駒の動作が完成すれば、15個も大丈夫はずだ。
ただ、駒に印がないと区別がつかないので、数字のラベルを貼付けた。
また、既に駒があるマスには移動しないように、マスにフラグを持たせた。でも、ちょっと、フラグの扱いが汚いね。今後の課題としよう。
駒に番号のラベルを貼付けた。
@implementation GamePieceView
...
- (void)_init
{
static NSInteger count = 0;
self.delegate = nil;
UILabel *label = [[UILabel alloc]
initWithFrame:CGRectMake(self.bounds.origin.x + 2.0,
self.bounds.origin.y + 2.0,
self.bounds.size.width - 4.0,
self.bounds.size.height - 4.0)];
label.text = [[NSString alloc] initWithFormat:@"%d", count++];
label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor orangeColor];
[self addSubview:label];
}
...
@end
マスにフラグを持たせた。
@interface GameSquare : NSObject
...
@property (nonatomic, assign) BOOL isEmpty;
...
@end
駒を15個配置。
@implementation GameBoardView
...
- (void)setupWithDelegate:(id)delegate
{
....
self.squaresArray = [[NSMutableArray alloc] init];
for (int i=0; i < 16; i++) {
GameSquare *square = [[GameSquare alloc] initWithFrame:rect[i]];
square.index = i;
if (i != 15) {
square.isEmpty = NO; ←フラグを設定
}
else {
square.isEmpty = YES; ←フラグを設定
}
[self.squaresArray addObject:square];
}
self.pieceViewArray = [[NSMutableArray alloc] init];
for (int i=0; i < 15; i++) { ←駒を15個配置。
GamePieceView *pieceView = [[GamePieceView alloc] initWithFrame:rect[i]];
pieceView.delegate = delegate;
[self addSubview:pieceView];
[self.pieceViewArray addObject:pieceView];
}
}
...
@end
異動先のマスに駒があったら、元に戻る。
@interface GameController ()
@property(nonatomic, weak) GameSquare *square;
...
@end
- (void)gameBoardViewTouchDown:(GameBoardView *)gameBoardView location:(CGPoint)touchPt taps:(int)taps event:(UIEvent*)event
{
GameSquare *square = [self.gameBoardView squareAtPoint:touchPt];
if (square) {
self.square = square; ←元のマスを覚えておく
}
....
}
- (void)gameBoardViewTouchUp:(GameBoardView *)gameBoardView location:(CGPoint)touchPt taps:(int)taps event:(UIEvent*)event
{
if (self.pieceView) {
GameSquare *square = [self.gameBoardView squareAtPoint:touchPt];
if (square.isEmpty) { ←駒がなかったら移動
[self.pieceView moveWithSquare:square];
self.square.isEmpty = YES;
square.isEmpty = NO;
}
else { ←元のマスに戻る
[self.pieceView moveWithSquare:self.square];
}
}
self.pieceView = nil;
}
...
@end
IULabelやUITextFieldのadjustsFontSizeToFitWidthプロパティをYESに設定すればいい。
その際、最小のフォントサイズを適切な値に設定するのが重要だと思うが、OS側で余りにも小さなサイズはと考えているのか、これ以上小さくならないサイズがあるようだ。
UIViewのサブクラスを用意して、それで背景を描画する方法があると思うが、簡単な方法を。
ビューの背景色となるUIColorはRGBのようなものだけでなく、画像パターンも指定できる。
なので、以下のような画像を用意して、
ビューの背景色として指定すれば、
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage *backgroundImage = [UIImage imageNamed:@"background.png"];
self.view.backgroundColor = [UIColor colorWithPatternImage:backgroundImage];
}
背景画像として表示される。
ただし、この方法だとメモリ使用量が増えるようで、メモリ使用量を抑えたい場合は、この方法は難しいようだ。
独自スキームとクリップボードを使ったアプリケーション間通信を試してみた。
URLスキーム「IPCServer.demo」で起動するサーバ・アプリケーションを用意する。スキームの登録方法は、[iOS][Web]ネイティブWebアプリケーション(その4)を参考にして欲しい。
サーバ・アプリケーションの画面にはラベルがある、クライアント・アプリケーションから情報を受け取ると、表示するようにする。
@implementation AppDelegate
...
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
NSLog(@"%s, %@", __func__, url);
if ([[url scheme] compare:@"IPCServer.demo"] == NSOrderedSame) {
UIPasteboard *pastedboard = [UIPasteboard pasteboardWithName:@"demo.IPCClient" create:NO];
ViewController *viewController = (ViewController *)self.window.rootViewController;
[viewController setMessage:pastedboard.string];
NSLog(@"pastedboard: %@", pastedboard.string);
return YES;
}
return NO;
}
...
@end
クライアント・アプリケーションでは、テキストフィールドから取得した文字列をサーバに送信している。
@implementation ViewController
...
- (IBAction)send:(id)sender
{
NSURL *url = [NSURL URLWithString:@"IPCServer.demo://IPCServer.demo?key=value"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
UIPasteboard *pastedboard = [UIPasteboard pasteboardWithName:@"demo.IPCClient" create:YES];
pastedboard.persistent = YES;
[pastedboard setString:self.textField.text];
[[UIApplication sharedApplication] openURL:url];
}
}
...
@end
シミュレータは、一度に一つしか起動できないと思うので、実機で試してほしい。
[iOS]15パズルで、デバッグ用に数字を描画したが、反転しているが気にしないで!と説明したが、文字列描画のみの座標系を変更する方法があるのを知らなかった為だった。全体の座標を変更すると、座標計算が面倒だと思っていた。
以前のコードは以下だった。
- (void)drawContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetRGBFillColor(context, 0.7, 0.7, 0.7, 1.0);
CGContextSetRGBStrokeColor(context, 0.5, 0.5, 0.5, 1.0);
CGContextSetLineWidth(context, 1.0);
CGContextAddRect(context, self.frame);
CGContextStrokePath(context);
CGContextSelectFont(context, "Helvetica", 12.0, kCGEncodingMacRoman);
char s[32];
sprintf(s, "%d", self.index);
CGContextShowTextAtPoint(context, self.frame.origin.x + 5.0, self.frame.origin.y + 5.0, s, strlen(s));
UIGraphicsPopContext();
}
これに、CGContextSetTextMatrix()を加えれば良かった
- (void)drawContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetRGBFillColor(context, 0.7, 0.7, 0.7, 1.0);
CGContextSetRGBStrokeColor(context, 0.5, 0.5, 0.5, 1.0);
CGContextSetLineWidth(context, 1.0);
CGContextAddRect(context, self.frame);
CGContextStrokePath(context);
CGContextSelectFont(context, "Helvetica", 12.0, kCGEncodingMacRoman);
char s[32];
sprintf(s, "%d", self.index);
CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1.0, -1.0)); ←ココ
CGContextShowTextAtPoint(context, self.frame.origin.x + 5.0, self.frame.origin.y + 17.0, s, strlen(s));
UIGraphicsPopContext();
}
だたし、座標の向きが変わっているので、描画するY軸の値を変更しないと、上の方に描画されてしまう。