Cocoa練習帳 -21ページ目

[iOS]Storyboardのinitial view controller

StoryboardのトップのViewControlerはどうやって指定するのか?

InterfaceBuilderにチェック項目がありました!


IB


関連情報

【Cocoa練習帳】
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]独自のコンテナViewController

iOSのCocoa Touchフレームワークには、横並びの画面はTab Bar Controller、階層方向はNavigaton Controllerが用意されているので、画面管理はこれを使えばいいのだが、標準のUIと異なる画面遷移が必要になった際に困ってしまう。




そうなったら、自分でビューの登録削除を行って独自の画面遷移を実装する事になるのだが、せっかくのビューコントローラが有効に活用できなくて残念と感じた事はないだろうか?




iOS 5から、Tab Bar Controllerの様なコンテナViewControllerを独自に実装できる機能がUIViewControllerに追加されたので、それを使って横並びの画面管理を実装してみた。




iOS 5から追加されたコンテナ機能の為のメソッドは、以下の4つ。





  • addChildViewController:

  • removeFromParentViewController

  • willMoveToParentViewController:

  • didMoveToParentViewController:




作成する画面は、灰色のTOPのビューコントローラの配下に赤と青の子ビューコントローラがあって、子っビューコントローラの画面上のボタンを押下すると、画面が切り替わるというものだ。




階層




子ビューコントローラは、それぞれ、独立したStoryboardで作成した。




UIStoryboard    *oneStoryboard = [UIStoryboard storyboardWithName:@"OneStoryboard" bundle:nil];
UIStoryboard    *twoStoryboard = [UIStoryboard storyboardWithName:@"TwoStoryboard" bundle:nil];
OneViewController   *oneViewController = [oneStoryboard instantiateInitialViewController];
TwoViewController   *twoViewController = [twoStoryboard instantiateInitialViewController];



コンテナビューコントローラに子ビューコントローラを登録。




[self addChildViewController:oneViewController];
[self addChildViewController:twoViewController];
oneViewController.cvcViewController = self;
twoViewController.cvcViewController = self;



子ビュコントローラがコンテナビューコントローラ配下になった際の処理は、didMoveToParentViewController:に記述するのだが、コンテナビューコントローラの画面表示が完了した後でないと、addChildViewController:時に自動で呼ばれないようなので、呼ぶ必要がある場合は、自分で呼ぶ事になるようだ。




[oneViewController didMoveToParentViewController:self];
[twoViewController didMoveToParentViewController:self];



ビューコントローラの階層構造を作っても、Viewとは無関係の話。なので、最初の画面を表示するコードを記述しないといけない。




self.selectedViewController = [self.childViewControllers objectAtIndex:0];
[self.view addSubview:self.selectedViewController.view];



画面を切り替えるのは、メソッドtoggleVCで行っている。UIViewControllerには、画面切り替え用のメソッド– transitionFromViewController:toViewController:duration:options:animations:completion:を用意しているが、用意されている画面遷移のアニメーションでないのを望む場合は、自分でなんとかしないといけないようだ。それが、else分の青(two)から赤(one)に切り替わるコードとなる。




- (void)toggleVC
{
    UIViewController    *oneViewController = [self.childViewControllers objectAtIndex:0];
    UIViewController    *twoViewController = [self.childViewControllers objectAtIndex:1];
    if (self.selectedViewController == oneViewController) {
        [self transitionFromViewController:oneViewController
                          toViewController:twoViewController
                                  duration:1.0
                                   options:UIViewAnimationOptionTransitionFlipFromLeft
                                animations:NULL
                                completion:NULL];
        self.selectedViewController = twoViewController;
    }
    else {
        [UIView animateWithDuration:1.0
                         animations:^{
                             CGPoint fromPt = twoViewController.view.layer.position;
                             CGPoint toPt = CGPointMake(fromPt.x, (fromPt.y * -1.0));
                             twoViewController.view.layer.position = toPt;
                         }
                         completion:^(BOOL finished) {
                             /* 元の位置の戻す */
                             CGPoint fromPt = twoViewController.view.layer.position;
                             CGPoint toPt = CGPointMake(fromPt.x, (fromPt.y * -1.0));
                             twoViewController.view.layer.position = toPt;
                             
                             /* 画面遷移(アニメーションなし) */
                             [self transitionFromViewController:twoViewController
                                               toViewController:oneViewController
                                                       duration:0.0
                                                        options:0
                                                     animations:NULL
                                                     completion:NULL];
                         }];
        self.selectedViewController = oneViewController;
    }
}



ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/ContainerVC - GitHub


関連情報
UIViewControllerのコンテナ機能



【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

【夜間/初級脱出】iOS開発セミナー:15パズル ゲームを作ろう

9月18日に、iOS上でゲームプログラミングにチャレンジしてみたい初級者を対象にセミナーを開催いたします。


【夜間/初級脱出】iOS開発セミナー:15パズル ゲームを作ろう




関連情報

【夜間/初級脱出】iOS開発セミナー:15パズル ゲームを作ろう




【Cocoa練習帳】

http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]入力制限(2)

前回紹介した方法だと上手くいかない事が分かったので、紹介する。


フォーマッタを通した後だと、装飾的な文字が追加されるし、元の文字列と、置換される文字列を個別に評価するのは面倒なので、くっつけた後、評価するようにした。以下は、n桁の数値のみの場合。




- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSMutableString *text = [textField.text mutableCopy];
    [text replaceCharactersInRange:range withString:string];
    
    /* クリア(空文字) */
    if (0 == text.length) {
        return YES;
    }
    
    /* n桁 */
    if (n < text.length) {
        return NO;
    }
    
    /* 数字 */
    NSCharacterSet  *digitCharSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];
    NSScanner   *scanner = [NSScanner localizedScannerWithString:text];
    [scanner setCharactersToBeSkipped:nil];
    [scanner scanCharactersFromSet:digitCharSet intoString:NULL];
    if (![scanner isAtEnd]) {
        return NO;
    }
    
    return YES;
}



これを金額に変換するメソッドは、以下のとおり。




- (NSString *)stringFromDecimalNumber:(NSDecimalNumber *)decimalNumber
{
    NSNumberFormatter   *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    [numberFormatter setCurrencyCode:@"JPY"];
    NSString    *s = [numberFormatter stringFromNumber:decimalNumber];
    return s;
}



【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]入力制限

たとえば、テキスト・フィールドの場合。
OSXでは、InterfaceBuilderでテキストフィールドのフォーマッタを設定して、ユーザが入力できる値の形式を指定できる。


iOSでもNSFormatterは存在しているが、ビューの仕組みが違う為か、InterfaceBuilderで入力できる値の形式を指定できない。


そこで、NSFormatterを利用するが、別の方法で入力値の制限方法を試してみることにする。




どので入力値の制限を行うかは、テキストフィールドの場合、UITextFieldのデリゲートUITextFieldDelegateで用意されているメソッドによる事になる。





  • – textFieldShouldBeginEditing:

    編集開始の直前に呼ばれる。NOを返すと編集は開始しない。

  • – textFieldDidBeginEditing:

    編集開始の直後に呼ばれる。

  • – textFieldShouldEndEditing:

    編集終了の直前に呼ばれる。NOを返すと編集は終了しない。

  • – textFieldDidEndEditing:

    編集終了の直後に呼ばれる。

  • – textField:shouldChangeCharactersInRange:replacementString:

    文字列が変更される直前に呼ばれる。変更範囲とそこに入る文字列は引数のrangeとstringで渡される。NOを返すと変更は反映されない。

  • – textFieldShouldClear:

    クリアボタン選択時に呼ばれる。YESを返すとクリアされる。

  • – textFieldShouldReturn:

    リターンキー選択時に呼ばれる。




入力制限は、入力中に行われるのが親切だと思うので、– textField:shouldChangeCharactersInRange:replacementString:で対応することにする。




NSFormatterの使い方を試してみる。




小終点以下1~2桁までを表示する。


NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[numberFormatter setMinimumFractionDigits:1];
[numberFormatter setMaximumFractionDigits:2];
NSNumber    *n = [NSNumber numberWithDouble:123456.123456];
NSString    *s = [numberFormatter stringFromNumber:n];
NSLog(@"%@", s);



お金として扱い、頭に¥をつける。


NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setCurrencyCode:@"JPY"];
NSNumber *n = [NSNumber numberWithInt:123456.123456];
NSString *s = [numberFormatter stringFromNumber:n];
NSLog(@"%@", s);



今度は、末尾に円をつける。


NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setPositiveFormat:@"#,##0円"];
NSNumber *n = [NSNumber numberWithInt:123456.123456];
NSString *s = [numberFormatter stringFromNumber:n];
NSLog(@"%@", s);



三桁毎にカンマ。


NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[numberFormatter setGroupingSeparator:@","];
[numberFormatter setGroupingSize:3];
NSNumber *n = [NSNumber numberWithInt:123456.123456];
NSString *s = [numberFormatter stringFromNumber:n];
NSLog(@"%@", s);



今の時刻を24時間表記で分まで表示。


NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setTimeStyle:NSDateFormatterShortStyle];
[dateFormatter setDateStyle:NSDateFormatterNoStyle];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"]];
NSString *s = [dateFormatter stringFromDate:[NSDate date]];
NSLog(@"%@", s);



頭に年月日を加える。


NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm"];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"]];
NSString *s = [dateFormatter stringFromDate:[NSDate date]];
NSLog(@"%@", s);



10桁の数字のみを受け付ける例を作ってみる。




- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
    NSInteger i = [string integerValue];
    NSNumber *n = [[NSNumber alloc] initWithInteger:i];
    NSString *s = [numberFormatter stringFromNumber:n];
    if ([string compare:s] == NSOrderedSame) {
        return YES;
    }    
    return NO;
}



もっと、いい方法がありそうだが。。。




【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]ユニークID (2)

前回、UUIDをユニークIDとして利用する方法を紹介したが、UUIDは取得する度に異なる値となる為、そのアプリケーションにとて、インストールされたiOS機器でユニークで固定なIDとして利用できない。


そこで、UUIDをキーチェインに登録して、そのアプリケーションにとってユニークで固定なIDにするコードを紹介する。まずは、Secuirty.frameworkをプロジェクトに追加する。





#import <Security/Security.h>
 
@interface ViewController ()
- (NSString *)createUUID;
- (NSString *)loadKeychainServices;
- (void)removeKeychainService;
@end
 
@implementation ViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    NSString    *uuidString = [self loadKeychainServices];
    NSLog(@"UUID:%@", uuidString);
    uuidString = [self loadKeychainServices];
    NSLog(@"UUID:%@", uuidString);
    [self removeKeychainService];
    uuidString = [self loadKeychainServices];
    NSLog(@"UUID:%@", uuidString);
}
 
- (NSString *)createUUID
{
    CFUUIDRef   uuid = CFUUIDCreate(NULL);
    NSString    *uuidString = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid);
    CFRelease(uuid);
    return uuidString;
}
 
- (NSString *)loadKeychainServices
{
    NSString    *savedUUID = nil;
    
    NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
    [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrGeneric];
    [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrAccount];
    [query setObject:[[NSBundle mainBundle] bundleIdentifier] forKey:(__bridge id)kSecAttrService];
    [query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    [query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
    [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];
    
    CFDictionaryRef attributesDictRef = nil;
    OSStatus    result = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&attributesDictRef);
    NSDictionary    *attributes = (__bridge_transfer NSDictionary *)attributesDictRef;
    if (result == noErr) {
        query = [NSMutableDictionary dictionaryWithDictionary:attributes];
        [query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        [query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
        CFDataRef   dataRef;
        result = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&dataRef);
        NSData  *data = (__bridge_transfer NSData *)dataRef;
        if (result == noErr) {
            savedUUID = [[NSString alloc] initWithBytes:[data bytes]
                                                 length:[data length]
                                               encoding:NSUTF8StringEncoding];
        }
    }
    if (! savedUUID) {
        savedUUID = [self createUUID];
        query = [[NSMutableDictionary alloc] init];
        [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrGeneric];
        [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrAccount];
        [query setObject:[[NSBundle mainBundle] bundleIdentifier] forKey:(__bridge id)kSecAttrService];
        [query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrLabel];
        [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrDescription];
        [query setObject:(id)[savedUUID dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
        result = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
        if (result != noErr) {
            savedUUID = nil;
        }
    }
    return savedUUID;
}
 
- (void)removeKeychainService
{
    NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
    [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrGeneric];
    [query setObject:(id)@"UUID" forKey:(__bridge id)kSecAttrAccount];
    [query setObject:[[NSBundle mainBundle] bundleIdentifier] forKey:(__bridge id)kSecAttrService];
    [query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    
    OSStatus result = SecItemDelete((__bridge CFDictionaryRef)query);
    if (result != noErr) {
    }
}
 
@end



キーチェインに登録できる情報の種類は決められているので、パスワードとしてUUIDを登録した。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/UniqueID - GitHub


関連情報
CFUUID Reference



【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]ユニークID

外部サービスと通信する際、アプリケーションがインストールされているiOS機器を区別できるユニークな識別子が必要になる事があると思うが、iOS 5より以前では、以下のコードでiOS機器毎にユニークなIDを取得できる。




NSString    *uniqueID = [[UIDevice currentDevice] uniqueIdentifier];
NSLog(@"UDID: %@", uniqueID);



iOS5からは、この方法は推奨されなくなった。そこで、多くの場合、以下の様なコードでユニークなIDを取得している。




- (NSString *)createUUID
{
    CFUUIDRef   uuid = CFUUIDCreate(NULL);
    NSString    *uuidString = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid);
    CFRelease(uuid);
    return uuidString;
}



ただし、このメソッドは呼ばれる度にユニークなIDを返す。その為、そのiOS機器にインストールされたアプリケーションという単位でユニークな識別子が欲しい場合は、この値をキーチェインに保存して、キーチェインに存在していたらそれを理由するという方法が、よく行われていると思う。




関連情報
CFUUID Reference



【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]Quartzで文字列描画

[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軸の値を変更しないと、上の方に描画されてしまう。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/FifteenPuzzle - GitHub


関連情報
iOS SDK Hacks ―プロが教えるiPhoneアプリ開発テクニック

参考にさせていただきました。



【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]アプリケーション間通信

独自スキームとクリップボードを使ったアプリケーション間通信を試してみた。




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



シミュレータは、一度に一つしか起動できないと思うので、実機で試してほしい。




ソースコード
GitHubからどうぞ。

https://github.com/murakami/workbook/tree/master/ios/IPCServer - GitHub
https://github.com/murakami/workbook/tree/master/ios/IPCClient - GitHub


関連情報
Text, Web, and Editing Programming Guide for iOS



【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]ビューの背景画像

UIViewのサブクラスを用意して、それで背景を描画する方法があると思うが、簡単な方法を。




ビューの背景色となるUIColorはRGBのようなものだけでなく、画像パターンも指定できる。




なので、以下のような画像を用意して、




bg




ビューの背景色として指定すれば、




- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIImage *backgroundImage = [UIImage imageNamed:@"background.png"];
    self.view.backgroundColor = [UIColor colorWithPatternImage:backgroundImage];
}



背景画像として表示される。




run




ただし、この方法だとメモリ使用量が増えるようで、メモリ使用量を抑えたい場合は、この方法は難しいようだ。




【Cocoa練習帳】
http://www.bitz.co.jp/weblog/

http://ameblo.jp/bitz/(ミラー・サイト)