iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
今回は、Arduinoだ。
まずは、Arduinoを入手しないと話にならない。私はスイッチサイエンスの製品をAmazonで購入した。
ArduinoとMacとは、USBケーブルで接続となるとなるがA-Bタイプを購入する事。A-miniBは別物だ!私はそれで失敗した。
製品が入手できたら以下のサイトの説明通りにすればいい。
ただ、私の場合だけの可能性があるが、入手したArduino UNO R3は、初期状態でExamples/1.Basics/Blinkがアップロードされている様で、この手順で試しても最初は変化が分からなかった。
Notificationには、LocalとPushの二種類があって、前者は発生源が手元のアプリケーション、後者は発生源が外部という事のようだ。
Local Notificationの登録は、通知する日時を指定する方法と、直ぐに通知する方法の二通りがある。
通知する日時を指定する手順は以下のとおり。
- (IBAction)scheduleNotification:(id)sender
{
DBGMSG(@"%s", __func__);
NSDate *today = [NSDate date];
UILocalNotification *localNotif = [[UILocalNotification alloc] init];
if (localNotif == nil)
return;
localNotif.fireDate = [today dateByAddingTimeInterval:10];
localNotif.timeZone = [NSTimeZone defaultTimeZone];
localNotif.alertBody = [NSString stringWithString:NSLocalizedString(@"Local Notify", nil)];
localNotif.alertAction = NSLocalizedString(@"local notify", nil);
localNotif.soundName = UILocalNotificationDefaultSoundName;
localNotif.applicationIconBadgeNumber = localNotif.applicationIconBadgeNumber + 1;
NSDictionary *infoDict = [NSDictionary dictionaryWithObject:@"local notify" forKey:@"Key"];
localNotif.userInfo = infoDict;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
}
UILocalNotificationのプロパティfireDateに、通知する日時を設定する。上記の例は、現在の時刻の10秒後だ。
UILocalNotificationのインスタンスをUIApplicationのscheduleLocalNotification:メソッドに設定すると、指定された日時に通知される。
直ぐに通知させたい場合は、presentLocalNotificationNow:メソッドを呼ぶ。
- (void)applicationDidEnterBackground:(UIApplication *)application
{
DBGMSG(@"Application entered background state.");
NSAssert(self.bgTask == UIBackgroundTaskInvalid, nil);
self.bgTask = [application beginBackgroundTaskWithExpirationHandler: ^{
dispatch_async(dispatch_get_main_queue(), ^{
[application endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
});
}];
dispatch_async(dispatch_get_main_queue(), ^{
while ([application backgroundTimeRemaining] > 1.0) {
UILocalNotification *localNotif = [[UILocalNotification alloc] init];
if (localNotif) {
localNotif.alertBody = [NSString stringWithString:NSLocalizedString(@"background", nil)];
localNotif.alertAction = NSLocalizedString(@"action", nil);
localNotif.soundName = UILocalNotificationDefaultSoundName;
localNotif.applicationIconBadgeNumber = localNotif.applicationIconBadgeNumber + 1;
[application presentLocalNotificationNow:localNotif];
break;
}
}
[application endBackgroundTask:self.bgTask];
self.bgTask = UIBackgroundTaskInvalid;
});
}
通知を受けてアプリケーションを呼び出す場合は、アプリケーション側で準備が必要となる。
アプリケーションの起動されていない場合は、アプリケーションのデリゲートのdidFinishLaunchingWithOptions:メソッドで通知からの呼び出しかどうかの判断を行う。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UILocalNotification *localNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (localNotif) {
NSString *value = [localNotif.userInfo objectForKey:@"Key"];
DBGMSG(@"%s, Notify: %@", __func__, value);
application.applicationIconBadgeNumber = localNotif.applicationIconBadgeNumber - 1;
}
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
起動済みの場合は、didReceiveLocalNotification:メソッドが呼ばれる。
- (void)application:(UIApplication *)app didReceiveLocalNotification:(UILocalNotification *)notif
{
NSString *value = [notif.userInfo objectForKey:@"Key"];
NSLog(@"%s, Notify: %@", __func__, value);
app.applicationIconBadgeNumber = notif.applicationIconBadgeNumber - 1;
}
今回は、USBパラレル変換モジュールをMacに接続してみる、ストーリーボードとセグウェ、スペルチェッカー、Local Notificationと通知センター、アクションシートのバグの発表があった。
AppleStore銀座で開催されたAruduinoハンズオンセミナー初級編に参加してきた。
今回は、その復習の結果だが、素人なので間違いがある可能性がある。その場合は指摘してくれると嬉しい!
オームの法則
I=(1/R)*V[A]
I:電流[A]
V:電圧[V]
1/R:比例定数。
R:電気抵抗[Ω]
発光ダイオード(LED)
Arduinoからの電源電圧は5V、LEDの順方向降下電圧は3.5V(予想)、セミナーで使用した抵抗は1,600Ω(だと思う)
電流 = 電圧 ÷ 抵抗 = (5V - 3.5V) ÷ 1.6kΩ ≒ 1mA
つまり、1mAの電流を流れる事を期待したということか。
プルアップ抵抗/プルダウン抵抗
回路にOn/Offするスイッチを入れた場合、Off時は断線した状態。つまり、電圧不定の状態になる。プルアップの場合、例えば、5Vの電源と間に10kΩの抵抗を入れてつなぐと、0.5mA(=5V÷10kΩ)と低い電流で安定することになる。そして、スイッチをOnにすると抵抗値が低い側に電流が流れるので、望みどおりとなる。
プルダウンの場合、スイッチがOffの際、マイコンの0Vの端子と接続させて安定させ、Onの際に5Vの端子と接続させるということか。まだ、説明していて自身がない。
今回は、基礎的な話なので、今後は、Arduinoとの関係について調べて説明していきたい。
先日のCocoa勉強会の復習だ。
Empty Applicationを生成する。
Storyboardファイルを新規作成する。
Main Storyboardに、さっき作成したStoryboardファイルを設定する。
ViewControllerを追加する。追加した物かどうか、分かりやすくする為、背景を黄色に設定。
アプリケーション・デリゲートでwindowプロパティの生成をコメントアウトする。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
/*
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
*/
return YES;
}
実行する。
今回はここまで。
ソフトウェア開発者、それもアプリケーション開発者ならAndroid開発を学ばない理由はないだろう。
特に、複数のプラットフォームに接する事によって、設計思想的に得る物は多いはずだ。
開発環境の設定については、多くの情報が公開されているので、ここで説明する必要はないだろう。著者は普通にEclipse上で開発を行っている。
ただ、申し訳ないが、AndroidプロジェクトのどのパートをGitHubで公開するといいのか理解していない為、今回はソースコードの公開なしで許して欲しい。
と、ここまで書いて、開発環境の更新に手間取り、これ以上、進めなくなってしまった。
申し訳ないが、今回はここまでとさせて欲しい。
トラブルの原因が分かった。debug.keystoreが期限切れだった。
~/.android/debug.keystoreを削除するなり、ファイル名を変更するなりして、再度、RUNを実行すればOK!
ということで、予定通り入門編をすすめる。
今回は、どこでもある、"hello, world"
新規Androidプロジェクトを作成する。
Project name: HelloWorld
Build Target: Android 2.3.3
Application name: HelloWorld
Package name: demo.hello
Create Activity: HelloWorldActivity
Min SDK Version: 10
プロジェクトが作成された。
Android 4.0 (Ice Cream Sandwich)のSDKが出ているが、ベーシックに2.3.3とした。
実行。
前回作成したストーリーボードに追加したViewControllerを削除して、NavigationControllerを追加し、カスタムクラスでRootViewControllerとSubViewControllerを追加する。
RootViewControllerにはボタンあって、ボタンとSubViewControllerをPushのSegueでつなぐ。
シミュレータで動作確認。
以前、Documentクラスを紹介したが、ストーリーボードを使用する場合、各ビュー・コントローラが個別に存在してる感じなので、シングルトンでアクセスできるのは便利だと思う。状態がDocumentクラスに保持されていれば、このコードだけでそれなりの実装が出来ると思う。
もし、画面が遷移したタイミングを知りたいのなら、元のビュー・コントローラでUIViewControllerの- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)senderメソッドをオーバーライドする。
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
DBGMSG(@"%s", __func__);
DBGMSG(@"segue identifier:%@", [segue identifier]);
}
Segueに"segue01"というidをつけたので、それがデバッグ出力されるはずだ。
ここで画面遷移となった原因に対応した状態をDocumentに保存する等すれば、遷移理由に対応した処理が出来るはずだ。
複数の表示項目(テキストのラベルや画像etc)がある場合、そのどれが有効で、どれが無効なのかを選択するUIはどう実現できるのだろうか?
iPhoneらしいのは、別の設定画面があって、そこで項目をスイッチでOn/Offする。
画面遷移が煩わしくないか。
表示画面そのもので、On/Offを選択する。
良さそうだ。だが、チェック的なモノがあるのも煩わしくないか。
今回は、項目の表示状態からOn/Offが選択できるUIを実験してみた。
項目が選択されたら表示を変更したいのなら、ユーザー操作、つまり、イベントを取得できるようにしないといけない。
今回は、画像とラベルの選択を実践してみたが、こららのビューを管理するビュー・コントローラでイベントを取得する方法と、個々のビューのサブクラスを作成して、そのサブクラスでイベントを取得する方法に二通りが考えられる。
ビュー・コントローラで対応する方法だと、新たにサブクラスを作成する手間が減るが、ユーザーの操作に対して、どのビューが対象なのかを判断しないといけないが、サブクラスを作成する方法は、その判断が不要となる。
そこで、今回の実験では、サブクラスを作成する方法を選択した。
プロジェクトにQuartzCore.frameworkを追加する。
画面のビュー・コントローラに画像とラベルを追加する(下図の赤丸)。
そして、それぞれのクラスをカスタムクラス(MyImageViewとMyLabel)に変更する。
MyImageViewの選択処理のコードは以下のとおり。
- (void)awakeFromNib
{
self.selected = NO;
self.layer.masksToBounds = YES;
self.layer.cornerRadius = 4.0f;
self.layer.borderWidth = 3.0f;
self.layer.borderColor = [[UIColor grayColor] CGColor];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self toggleSwitch];
}
- (void)toggleSwitch
{
if (self.selected) {
self.selected = NO;
self.layer.borderColor = [[UIColor grayColor] CGColor];
}
else {
self.selected = YES;
self.layer.borderColor = [[UIColor blueColor] CGColor];
}
}
MyLabelの選択処理のコードは以下のとおり。
- (void)awakeFromNib
{
self.selected = NO;
self.textColor = [UIColor grayColor];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self toggleSwitch];
}
- (void)toggleSwitch
{
if (self.selected) {
self.selected = NO;
self.textColor = [UIColor grayColor];
}
else {
self.selected = YES;
self.textColor = [UIColor blueColor];
}
}
実行。
_ eien [タッチする位置間違いもあるので、touchsBeganよりかtouchesEndedのほうが私は好きなんですが、どう..]
表示したい画像とマスクの画像を用意する。
それを以下のコードでマスクした画像を取得し、描画する。
- (void)awakeFromNib
{
self.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"pict.png" ofType:nil]];
self.mask = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"mask.png" ofType:nil]];
self.imageMaskedWithImage = [self maskImage:self.image withMask:self.mask];
}
- (void)drawRect:(CGRect)rect
{
[self.imageMaskedWithImage drawAtPoint:CGPointMake(10.0, 10.0)];
}
- (UIImage*)maskImage:(UIImage *)image withMask:(UIImage *)maskImage
{
CGImageRef maskRef = maskImage.CGImage;
CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
CGImageGetHeight(maskRef),
CGImageGetBitsPerComponent(maskRef),
CGImageGetBitsPerPixel(maskRef),
CGImageGetBytesPerRow(maskRef),
CGImageGetDataProvider(maskRef),
NULL,
false);
CGImageRef masked = CGImageCreateWithMask([image CGImage], mask);
CGImageRelease(mask);
return [UIImage imageWithCGImage:masked];
}
実行。
画像のマスクについては、まだまだ、ほんの一部だと思っているので、機会があったら続きを試してみたい。
実は、本題の画像処理のコードより、Storyboardを使った、ターブル一覧の実装に時間がかかってしまったが、いい経験となった。Storyboardについては、機会があれば続きをと考えている。
画像を任意の形に切り抜いて表示する方法の一つにクリッピングがある。
今回は、クリッピングの基本的な内容で、画像の四隅を丸くする例だ。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGSize imageSize = self.image.size;
CGRect imageRect = {10.0, 10.0, imageSize.width, imageSize.height};
float radius = 10.0;
CGFloat minX = CGRectGetMinX(imageRect);
CGFloat midX = CGRectGetMidX(imageRect);
CGFloat maxX = CGRectGetMaxX(imageRect);
CGFloat minY = CGRectGetMinY(imageRect);
CGFloat midY = CGRectGetMidY(imageRect);
CGFloat maxY = CGRectGetMaxY(imageRect);
/* 現状の描画環境を保存 */
CGContextSaveGState(context);
/* 四角形の辺に接する、半径radiusの円弧を四隅に追加 */
CGContextMoveToPoint(context, minX, midY);
CGContextAddArcToPoint(context, minX, minY, midX, minY, radius);
CGContextAddArcToPoint(context, maxX, minY, maxX, midY, radius);
CGContextAddArcToPoint(context, maxX, maxY, midX, maxY, radius);
CGContextAddArcToPoint(context, minX, maxY, minX, midY, radius);
CGContextClosePath(context);
/* 先ほどのパスをクリップ領域として設定 */
CGContextClip(context);
/* 描画 */
[self.image drawAtPoint:CGPointMake(10.0, 10.0)];
/* 描画環境を先ほどの保存時点に戻す */
CGContextRestoreGState(context);
}
実行。テーブルの「ClippingADrawing」を選択して欲しい。
Mac OS Xでは、基本的に描画の座標は左下が原点だ。
iOSでは、UIKitとCoreAnimationでは左上が原点(便宜上、ULO(upper-left-origin)と呼ぶ)、Core Graphicsでは左下が原点(便宜上、LLO(lower-left-origin)と呼ぶ)だそうだ。試してみよう。
まず、Mac OS Xから。NSViewのサブクラスを作成して、以下の描画コードを追加する。
- (void)drawRect:(NSRect)dirtyRect
{
/* LLO(lower-left-origin) */
NSGraphicsContext *nsctx = [NSGraphicsContext currentContext];
CGContextRef context = (CGContextRef)[nsctx graphicsPort];
CGContextSetLineWidth(context, 4.0);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 10.0, 30.0);
CGContextAddLineToPoint(context, 10.0, 10.0);
CGContextDrawPath(context, kCGPathStroke);
CGContextMoveToPoint(context, 10.0, 10.0);
CGContextAddLineToPoint(context, 30.0, 10.0);
CGContextDrawPath(context, kCGPathStroke);
}
実行。確かに左下が原点だ。
次にiPhoneで確認。画像を座標 (10.0, 10.0) に描画する。
- (void)drawRect:(CGRect)rect
{
/* ULO(upper-left-origin) */
[self.upperLeftOriginImage drawAtPoint:CGPointMake(10.0, 10.0)];
}
実行。向きも変えてみる。確かに左上に描画される。
今度はパスを座標 (10.0, 10.0) に描画するコードを追加。
- (void)drawRect:(CGRect)rect
{
/* ULO(upper-left-origin) */
[self.upperLeftOriginImage drawAtPoint:CGPointMake(10.0, 10.0)];
/* LLO(lower-left-origin) */
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 4.0);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 10.0, 30.0);
CGContextAddLineToPoint(context, 10.0, 10.0);
CGContextDrawPath(context, kCGPathStroke);
CGContextMoveToPoint(context, 10.0, 10.0);
CGContextAddLineToPoint(context, 30.0, 10.0);
CGContextDrawPath(context, kCGPathStroke);
}
実行。赤丸の箇所に描画。あれ?左上に描画されている。
何かを勘違いしているのだろうか?
前回の疑問、どのような動作をするのか考えれば、当たり前の結果だと分かった。
アドバイスしていただいたMOSAのMOSAdeBBの方々、cocoa-dev-japanの方々、ありがとうございます!
ただ、今回、これが確認できるサンプルを用意しようと考えたのだが、上手くいかなかったので、少し時間が欲しい。申し訳ない。
今回は、自分のCocoa練習の場を紹介します。
■相互の学習の場。勉強になっています。
Cocoa勉強会
■交流の場。
特定非営利活動法人MOSA
MOSAdeBBという会員向け掲示板があるのですが、これが質問しやすく、直ぐに回答が得られて、助かります。
■Googleグループ
cocoa-dev-japan
状況共有しやすい場として設立されました。
Xcode 4.3 for Lionがリリースされた。
/Developerディレクトリでなく、/Applicationsディレクトリにインストールされるようになったり、バンドル内に各種ツールを置くようになったりと、OS X Mountain Lion時代を感じされる変化が興味深い。
そして、これが一番大きな事だが、バージョンアップに伴う様々が事で、本日は日記に新しい話題を投稿する事が出来なくなってしまった。残念!
デフォルト座標系の話、理由は分かったが、それを実際に試せないか、試行錯誤している。
Apple Developerサイトの文書でも、UIKitでの描画。つまり、UIViewサブクラスのdrawRect:メソッド内で描画する場合、デフォルト座標系は原点は左上で、座業は下と右方向に伸びるULO(upper-left-origin)となって、LLO(lower-left-origin)を選択する場合は独自の変換行列(CTM:Current Transformation Matrix)を適用すると説明されていた。
ということで、現在の描画環境以外にCoreGraphicsで描画すれば、LLOになると考え、ビットマップ・コンテキストに描画した物を表示してみた。
- (void)drawRect:(CGRect)rect
{
DBGMSG(@"%s", __func__);
CGContextRef context = UIGraphicsGetCurrentContext();
/* LLO(lower-left-origin) */
size_t witdh = rect.size.width;
size_t height = rect.size.height;
size_t bytesPerRow = witdh * 4;
bytesPerRow = COMPUTE_BEST_BYTES_PER_ROW(bytesPerRow);
unsigned char *rasterData = calloc(1, bytesPerRow * height);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bitmapContext = CGBitmapContextCreate(rasterData, witdh, height,
8, bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);
CGContextSetRGBStrokeColor(bitmapContext, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(bitmapContext, 4.0);
CGContextBeginPath(bitmapContext);
CGContextMoveToPoint(bitmapContext, 5.0, 25.0);
CGContextAddLineToPoint(bitmapContext, 5.0, 5.0);
CGContextDrawPath(bitmapContext, kCGPathStroke);
CGContextMoveToPoint(bitmapContext, 5.0, 5.0);
CGContextAddLineToPoint(bitmapContext, 25.0, 5.0);
CGContextDrawPath(bitmapContext, kCGPathStroke);
CGImageRef cgimage = CGBitmapContextCreateImage(bitmapContext);
CGContextDrawImage(context, rect, cgimage);
CGContextRelease(bitmapContext);
free(rasterData);
CGColorSpaceRelease(colorSpace);
/* ULO(upper-left-origin) */
[self.upperLeftOriginImage drawAtPoint:CGPointMake(20.0, 20.0)];
CGContextSetLineWidth(context, 4.0);
CGContextBeginPath(context);
CGContextMoveToPoint(context, 20.0, 40.0);
CGContextAddLineToPoint(context, 20.0, 20.0);
CGContextDrawPath(context, kCGPathStroke);
CGContextMoveToPoint(context, 20.0, 20.0);
CGContextAddLineToPoint(context, 40.0, 20.0);
CGContextDrawPath(context, kCGPathStroke);
}
あれ、ULOみたい。
現在の描画環境に表示する際にULOに変換されるのでは?と考え、ビットマップ・コンテキストの内容をファイルに保存してみた。
UIImage *uiimage = [[UIImage alloc] initWithCGImage:cgimage];
NSData *data = UIImagePNGRepresentation(uiimage);
NSString *filePath = [NSString stringWithFormat:@"%@/demo.png" ,
[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]];
NSLog(@"%@", filePath);
[data writeToFile:filePath atomically:YES];
確かに、LLOになっている。
Google AdMobの魅力は、容易に自分のアプリケーションに組み込む事が出来る事だ。
以前は、使用しているオープンソースのフレームワークも一緒に組み込む等、面倒な部分があったが、現在はアーカイブのライブラリとヘッダファイルを追加するだけでOK。組み込むソースコードも、AdMobサイトで丁寧に説明してある。なので、ここで組み込む手順を説明するまでもないので、自分が工夫していることについて説明する。
AdMobは使い始めたら直ぐに広告が表示されるようになる訳ではないのと、小数の同じ端末から何度も広告が表示されると、不正な操作と判断されるということからだと思うが、デバッグ時は明示的にそのことを設定する必要がある。
著者は、Supporting Filesの<アプリ名>-Prefix.pchで、デバッグ出力用のマクロの定義時に、AdMob試験用のマクロを定義している。
#ifdef DEBUG
#define DBGMSG(...) NSLog(__VA_ARGS__)
#define ADMOB_TESTDRIVE
#else /* DEBUG */
#define DBGMSG(...)
#endif /* DEBUG */
ViewControllerにAdMobのプロパティを定義する。
@interface MyViewController : UIViewController
@property (strong, nonatomic) GADBannerView *bannerView;
@end
ViewControllerの-(void)viewDidLoadメソッドでインスタンスを生成する。
- (void)viewDidLoad
{
[super viewDidLoad];
self.bannerView = [[GADBannerView alloc]
initWithFrame:CGRectMake(0.0,
self.view.frame.size.height -
GAD_SIZE_320x50.height,
GAD_SIZE_320x50.width,
GAD_SIZE_320x50.height)];
self.bannerView.adUnitID = @"自分のAdMobパグリッシャーID";
self.bannerView.rootViewController = self;
[self.view addSubview:self.bannerView];
#ifdef ADMOB_TESTDRIVE
GADRequest *request = [GADRequest request];
request.testDevices = [NSArray arrayWithObjects:
GAD_SIMULATOR_ID,
@"デバイスID(1)",
@"デバイスID(2)",
nil];
[bannerView_ loadRequest:request];
#else /* ADMOB_TESTDRIVE */
[self.bannerView loadRequest:[GADRequest request]];
#endif /* ADMOB_TESTDRIVE */
self.bannerView.delegate = self;
}
その際、ADMOB_TESTDRIVEが定義されている。つまりデバッグ時は、デバッグで使用する自分のデバイスIDを上記のコードのように設定する。
広告が表示される場合と、表示されない場合で、画面レイアウトを変更する場合があると思が、以下のそれの例。
- (void)adViewDidReceiveAd:(GADBannerView *)bannerView
{
[UIView beginAnimations:@"BannerSlideOn" context:nil];
bannerView.frame = CGRectMake(0.0,
self.view.frame.size.height - bannerView.frame.size.height,
bannerView.frame.size.width,
bannerView.frame.size.height);
self.コントロール.frame = CGRectMake(self.コントロール.frame.origin.x,
元のY座標の値 - bannerView.frame.size.height,
self.コントロール.frame.size.width,
self.コントロール.frame.size.height);
[UIView commitAnimations];
}
- (void)adView:(GADBannerView *)bannerView
didFailToReceiveAdWithError:(GADRequestError *)error
{
[UIView beginAnimations:@"BannerSlideOff" context:NULL];
self.コントロール.frame = CGRectMake(self.コントロール.frame.origin.x,
元のY座標の値,
self.コントロール.frame.size.width,
self.コントロール.frame.size.height);
[UIView commitAnimations];
}
上記の例では、ボタン等の<コントロール>を広告の表示/非表示に合わせて位置を変更している。全てを計算で求めると、トラブルで順番が狂うとおかしくなるので、ハードコーディングしている。
AdMobが組み込まれていないプロジェクトだと、しょうがないので、今回はソースコードはなしだ。
どの様な物かは知っていたが、ユーザーの立場からアクセスがしづらいのでは?と思えて縁がなかった物の一つに設定バンドル(Settings Bundle)がある。
プロジェクトの新規ファイルとして、ResourceのSettings Bundleを選択する。
Setting.bundleのRoot.plistに項目を設定する。
デフォルトでは、テキストフィールドNameとトグルスイッチ、スライダーの3項目が用意されている。
実装すると、この3項目が表示される。
設定バンドルの値は、以下のコードで取得できる
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"%s", __func__);
NSString *name = [[NSUserDefaults standardUserDefaults] stringForKey:@"name_preference"];
NSLog(@"Name:%@", name);
BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"enabled_preference"];
NSLog(@"Enabled:%d", (int)enabled);
float slider = [[NSUserDefaults standardUserDefaults] floatForKey:@"slider_preference"];
NSLog(@"Slider:%f", slider);
return YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
NSLog(@"%s", __func__);
NSString *name = [[NSUserDefaults standardUserDefaults] stringForKey:@"name_preference"];
NSLog(@"Name:%@", name);
BOOL enabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"enabled_preference"];
NSLog(@"Enabled:%d", (int)enabled);
float slider = [[NSUserDefaults standardUserDefaults] floatForKey:@"slider_preference"];
NSLog(@"Slider:%f", slider);
}
前回の例で、ビットマップ・コンテキストを利用していたが、Retina displayとなった現在では、以下の方法がお勧めだ。
前回では、CoreGraphicsの関数を使ってビットマップ・コンテキストを生成して、そこに描画していた。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
/* LLO(lower-left-origin) */
size_t witdh = rect.size.width;
size_t height = rect.size.height;
size_t bytesPerRow = witdh * 4;
bytesPerRow = COMPUTE_BEST_BYTES_PER_ROW(bytesPerRow);
unsigned char *rasterData = calloc(1, bytesPerRow * height);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bitmapContext = CGBitmapContextCreate(rasterData, witdh, height,
8, bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);
CGContextSetRGBStrokeColor(bitmapContext, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(bitmapContext, 4.0);
CGContextBeginPath(bitmapContext);
CGContextMoveToPoint(bitmapContext, 5.0, 25.0);
CGContextAddLineToPoint(bitmapContext, 5.0, 5.0);
CGContextDrawPath(bitmapContext, kCGPathStroke);
CGContextMoveToPoint(bitmapContext, 5.0, 5.0);
CGContextAddLineToPoint(bitmapContext, 25.0, 5.0);
CGContextDrawPath(bitmapContext, kCGPathStroke);
iOS4からは、以下のように記述できる。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
/* ULO(upper-left-origin) */
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
CGContextRef bitmapContext = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(bitmapContext, 1.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(bitmapContext, 4.0);
CGContextBeginPath(bitmapContext);
CGContextMoveToPoint(bitmapContext, 5.0, 25.0);
CGContextAddLineToPoint(bitmapContext, 5.0, 5.0);
CGContextDrawPath(bitmapContext, kCGPathStroke);
CGContextMoveToPoint(bitmapContext, 5.0, 5.0);
CGContextAddLineToPoint(bitmapContext, 25.0, 5.0);
CGContextDrawPath(bitmapContext, kCGPathStroke);
UIImage* uiimage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[uiimage drawAtPoint:CGPointMake(0.0, 0.0)];
}
この方法だと、- (void)drawRect:(CGRect)rectメソッドの描画環境と座標系が同じとなる。
前回のクリッピングではCoreGraphicsの関数を使ったが、今回はUIKitのUIBezierPathを使ったクリップングに挑戦だ。
前回のCoreGraphicsの関数を使ったコードだ。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGSize imageSize = self.image.size;
CGRect imageRect = {10.0, 10.0, imageSize.width, imageSize.height};
float radius = 10.0;
CGFloat minX = CGRectGetMinX(imageRect);
CGFloat midX = CGRectGetMidX(imageRect);
CGFloat maxX = CGRectGetMaxX(imageRect);
CGFloat minY = CGRectGetMinY(imageRect);
CGFloat midY = CGRectGetMidY(imageRect);
CGFloat maxY = CGRectGetMaxY(imageRect);
/* 現状の描画環境を保存 */
CGContextSaveGState(context);
/* 四角形の辺に接する、半径radiusの円弧を四隅に追加 */
CGContextMoveToPoint(context, minX, midY);
CGContextAddArcToPoint(context, minX, minY, midX, minY, radius);
CGContextAddArcToPoint(context, maxX, minY, maxX, midY, radius);
CGContextAddArcToPoint(context, maxX, maxY, midX, maxY, radius);
CGContextAddArcToPoint(context, minX, maxY, minX, midY, radius);
CGContextClosePath(context);
/* 先ほどのパスをクリップ領域として設定 */
CGContextClip(context);
/* 描画 */
[self.image drawAtPoint:CGPointMake(10.0, 10.0)];
/* 描画環境を先ほどの保存時点に戻す */
CGContextRestoreGState(context);
}
UIBezierPathを使うとこうなる。ちょっと、四隅が丸い四角の例なので、簡単なコードになってしまったが。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGSize imageSize = self.image.size;
CGRect imageRect = {10.0, 10.0, imageSize.width, imageSize.height};
float radius = 10.0;
/* 現状の描画環境を保存 */
CGContextSaveGState(context);
/* 四角形の四隅を半径radiusの円弧に */
UIBezierPath* aPath = [UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:radius];
/* パスをクリップ領域として設定 */
[aPath addClip];
/* 描画 */
[self.image drawAtPoint:CGPointMake(10.0, 10.0)];
/* 描画環境を先ほどの保存時点に戻す */
CGContextRestoreGState(context);
}
この時期になってくると気になるのは、今年のWWDCについてだ。
今年は、どうしましょう。行くべきでしょうね。
Cocoa勉強会でも、WWDCについての情報を発信しています。
WWDCに行くと、どんな方向を目指しているのか感じ取る事が出来る。また、その場で理解できなくても、これは聞いた事があるな?と、調査のヒントを得られる。今回は、WWDCが解決の糸口になった例だ。
OS Xでテキストの内容をスピーチされる事は簡単だ。
Cocoaでは、
NSSpeechSynthesizer *synthesizer = [[NSSpeechSynthesizer alloc] init];
[synthesizer setDelegate:self];
[synthesizer startSpeakingString:@"Hello, world."];
- (void)speehSynthesizer:(NSSpeechSynthesizer *)sender
didFinishSpeaking:(BOOL)finishedpeaking
{
...
}
Core Foundationでは、
SpeechChannel *chan;
err = NewSpeechChannel(NULL, &chan);
CFNumber *callback = CFNumberCreate(NULL, kCFNumberLongType, HighlightSpokenWord);
err = SetSpeechProperty(chan, kSpeechWordCFCallBack, callback);
err = SpeakCFString(chan, CFSTR("Hello, world."), NULL);
void HighlightSpokenWord(SpeechChannel chan,
SRefCo refCon,
CFStringRef aString,
CFRange wordRange)
{
....
}
でも、それにAudio Unitを使った効果を適用させるには、どうすればいいのか?
どうして、これが出来ると思ったのかは今となっては思い出せないが、多分、WWDCでのセッションの内容が頭の片隅にあったのだろう。それで、WWDCのセッションのビデオを見直しのが、WWDC2009のSession 129『Text-to-Speech: Adventures with Alex』だった。
一度、解決の糸口をつかむと、後は芋づる式。ヘッダーファイルのコメントで説明されていた。
SpeechSynthesis.h
/*------------------------------------------*/
/* AudioUnit constants - new in 10.5 */
/*------------------------------------------*/
enum {
kAudioUnitSubType_SpeechSynthesis = 'ttsp', /* kAudioUnitType_Generator */
kAudioUnitProperty_Voice = 3330, /* Get/Set (VoiceSpec) */
kAudioUnitProperty_SpeechChannel = 3331 /* Get (SpeechChannel) */
};
指定した文章をSpeech Synthesisで喋らせ、それをAudio Unitのコンポーネントでディレイさせて再生させる例が以下だ。
AUNode inputNode, effectNode, outputNode;
NewAUGraph(&_auGraph);
AudioComponentDescription cd;
cd.componentType = kAudioUnitType_Generator;
cd.componentSubType = kAudioUnitSubType_SpeechSynthesis;
cd.componentManufacturer = kAudioUnitManufacturer_Apple;
cd.componentFlags = 0;
cd.componentFlagsMask = 0;
AUGraphAddNode(_auGraph, &cd, &inputNode);
cd.componentType = kAudioUnitType_Effect;
cd.componentSubType = kAudioUnitSubType_Delay;
AUGraphAddNode(_auGraph, &cd, &effectNode);
cd.componentType = kAudioUnitType_Output;
cd.componentSubType = kAudioUnitSubType_DefaultOutput;
AUGraphAddNode(_auGraph, &cd, &outputNode);
AUGraphConnectNodeInput(_auGraph, inputNode, 0, effectNode, 0);
AUGraphConnectNodeInput(_auGraph, effectNode, 0, outputNode, 0);
AUGraphOpen(_auGraph);
AUGraphInitialize(_auGraph);
AudioUnit generateAudioUnit;
AUGraphNodeInfo(_auGraph, inputNode, NULL, &generateAudioUnit);
SpeechChannel channel;
UInt32 sz = sizeof(SpeechChannel);
AudioUnitGetProperty(generateAudioUnit, kAudioUnitProperty_SpeechChannel,
kAudioUnitScope_Global, 0, &channel, &sz);
AUGraphStart(_auGraph);
SpeakCFString(channel, CFSTR("Hello, world."), NULL);
WWDCで、最初のiPhoneの開発環境の紹介があった際は、Webアプリケーションのみを認めるという説明だったが不要だった為、その後、ネイティブ・アプリケーションが開発できるSDKがリリースされた。
とは云うものの、Webアプリケーションでも対応できる分野はあると思われるし、WebアプリケーションとネイティブAPIを組み合わせる手法も出来ているので、今回は、ネイティブなWebアプリケーションの初歩の初歩に挑戦してみた。
Single View Applicationのプロジェクトを新規生成する。
ViewをUIWebViewに置換する。
HTMLコンテンツを置くフォルダをグループとして登録し、"index.html"を新規ファイルとして生成する。
ViewControllerに、アプリケーション内部のHTMLコンテンツを表示するコードを追加する。
- (void)viewDidLoad
{
[super viewDidLoad];
UIWebView *webView = (UIWebView *)self.view;
NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:path];
NSURLRequest *req = [NSURLRequest requestWithURL:fileURL];
[webView loadRequest:req];
}
実行。
まだ、これは初歩の初歩で、HTMLコンテンツの置き場所はバンドル・リソースでいいのか?とか、ディレクトリ構造を保持したまま、実行時の環境にコピーするには?とか課題があるが、それは次回で。
DocumentsディレクトリやLibraryディレクトリ配下への配置を考えたが、課題があるということで、バンドル・リソース内に配置する方法を試してみた。
実際にWebアプリケーションを組み込む状況を想定して、前回、用意してHTMLコンテンツが置かれているwebappフォルダ配下に、infoとaboutフォルダを追加して、このディレクトリ構造が保たれたまま、アクセスできる事を確認したいと思う。
infoとaboutフォルダをXcodeのプロジェクトにドラッグ&ドロップする。すると、以下のシートが表示されるが、Distinationのチェックは外し、Foldersは2番目の"Create folder references for any added folders"を選択する。
するとプロジェクトでの表示は、以下となる。
"webapp/index.html"の内容は以下のとおり。
<html>
<head>
<title>My WebApp</title>
</head>
<body>
<p>My WebApp</p>
<a href="info/index.html">info</a><br />
<a href="about/index.html">about</a>
</body>
</html>
"webapp/info/index.html"の内容は以下のとおり。
<html>
<head>
<title>Info</title>
</head>
<body>
<p>Info</p>
</body>
</html>
"webapp/about/index.html"の内容は以下のとおり。
<html>
<head>
<title>About</title>
</head>
<body>
<p>About</p>
</body>
</html>
実行。infoとaboutのリンクが機能するので、バンドル・リソース内でもディレクトリ構造は保持されている事が分かる。
iOSはストレージ容量もメモリも節約を求められる。また、データサイズが小さくなる事によって、操作性の向上も期待できる。
その為にキーとなる事の一つにデータの圧縮と解凍があるが、今回は、Cocoaでzipデータを扱う初歩的に事に挑戦する。
業務上パスワード付きzip圧縮ファイルを受け取る事が多いのだが、何時もこれを解凍するのをコマンドラインでおこなっていた。
$ unzip -P パスワード ファイル.zip
これをAppStore時代では存在が小さくなってしまったが、Dropletsとして実装してみよう。
Mac OS XのCocoa Applicationを新規作成し、TARGETSの設定のDocument TypesにZip文書を追加する。
アプリケーションのデリゲートに、以下のコードを追加する。
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
DBGMSG(@"%s, filename(%@)", __func__, filename);
return NO;
}
これでドラッグ&ドロップされた一つのファイルのパスが取得できるようになった。
次に、ZipArchiveライブラリを以下のサイトから入手する。
入手したZipArchiveライブラリをプロジェクトに組み込み、ZipArchive.hでUIKit.hをインクルードしているのでコメントアウトし、
ZipArchive.mmをARCの対象から外す。
そして、かなり手抜きをしているが、zipファイルが渡されると、末尾の"zip"サフィクスを削るコードを先ほどのメソッドに追加する。
#import "ZipArchive.h"
:
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
ZipArchive *archiver = [[ZipArchive alloc] init];
[archiver UnzipOpenFile:filename];
NSString *path = [filename stringByDeletingPathExtension];
[archiver UnzipFileTo:path overWrite:NO];
[archiver UnzipCloseFile];
return YES;
}
:
前回のZipArchiveライブラリが内部で利用していたminizipライブラリを直接利用する事に挑戦する。
minizipを直接利用する利点は、複数のファイルが固められて圧縮されていた場合、全てを解凍するのではなくて、必要なファイルのみにアクセスする事が可能になる事だ。
以下のMinizipのサイトからminizipライブラリ一式をダウンロードする。
minizip一式には、コマンドラインのプログラム用のソースファイルが含まれているので、ライブラリとして使用する場合に必要となる、以下のファイルのみをプロジェクトに追加する。
crypt.h
ioapi.c
ioapi.h
mztools.c
mztools.h
unzip.c
unzip.h
zip.c
zip.h
プロジェクトに以下のライブラリを追加する。
libz.dylib
文書オープンのダイアログで指定したzipファイルの中身を出力するコードを追加する。
@interface AppDelegate : NSObject
:
- (IBAction)openDocument:(id)sender;
@end
#import "unzip.h"
:
@interface AppDelegate ()
- (void)unzip:(NSString *)path;
@end
@implementation AppDelegate
:
- (IBAction)openDocument:(id)sender
{
DBGMSG(@"%s", __func__);
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel beginSheetModalForWindow:self.window
completionHandler:^(NSInteger returnCode){
NSURL *pathToFile = nil;
NSString *path = nil;
if (returnCode == NSOKButton) {
pathToFile = [[panel URLs] objectAtIndex:0];
path = [pathToFile path];
DBGMSG(@"%@", pathToFile);
DBGMSG(@"%@", path);
dispatch_async(dispatch_get_main_queue(), ^{
[self unzip:path];
});
}
}];
}
- (void)unzip:(NSString *)path
{
DBGMSG(@"%s, %@", __func__, path);
int error = UNZ_OK;
unzFile file = NULL;
file = unzOpen([path UTF8String]);
unzGoToFirstFile(file);
while (error == UNZ_OK) {
unz_file_info fileInfo;
char filename[PATH_MAX];
unzGetCurrentFileInfo(file, &fileInfo, filename, PATH_MAX, NULL, 0, NULL, 0);
DBGMSG(@"%s", filename);
error = unzGoToNextFile(file);
}
unzClose(file);
}
@end
以下のディレクトリ構造のフォルダをzipで圧縮する。
junk
| file01.txt
| file02.txt
+- folder01
| file11.txt
| file12.txt
+- folder02
file21.txt
file22.txt
これを先ほどのコードでディレクトリ構造をデバッグ出力した結果の抜粋が以下。
junk/
junk/file01.txt
junk/file02.txt
junk/folder01/
junk/folder01/file11.txt
junk/folder01/file12.txt
junk/folder02/
junk/folder02/file21.txt
junk/folder02/file22.txt
_ m_yukio [環境構築失敗の原因が分かった。 後で試す。]