アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

【iOS】MemoryWarningSenderでメモリ警告バグを効率的に見つける

こんにちは。松田です。
iOS用アプリを作成していると、予期しないバグが発生することが多々ありますが、その原因として多いのが「メモリ警告」です。メモリ警告が発生すると、非表示状態のviewが破壊されるため、開発者の意図しない動作を引き起こす事があります。

この状態をテストするには、iOSシミュレータの「メモリ警告をシミュレート」機能を使うことができますが、発生させるタイミングは開発者次第で尚且つショートカットキーも無い(たぶん)ため、なかなか効率的に活用することができません。

これを解決するためのツールがMemoryWarningSenderです。
これを利用すれば、iOSシミュレータの実行中に定期的にメモリ警告を発生させることができるため、想定外のバグの発見につなげることができます。

MemoryWarningSenderは、ブログ「ゆめ技」で、ちきん(@mokemokechicken)さんが作成し公開しています。
http://yumewaza.yumemi.co.jp/2012/03/memorywarningsender_in_ios_simulatorios.html
この記事を読めば使い方はわかりますが、こっちにも簡単に記述しておきます。


インストール



SIMBLのインストール


iOSシミュレータを機能拡張するために、SIMBLが必要になります。
http://www.culater.net/software/SIMBL/SIMBL.php
上記URLからSIMBL-{バージョン番号}.zipをダウンロードし、解凍後にSIMBL-{バージョン番号}.pkgをインストールします。

MemoryWarningSenderのインストール


https://github.com/mokemokechicken/MemoryWarningSender
上記URLでgithub登録されています。
「ZIP」リンクからダウンロードし、解凍するとMemoryWarningSender.pkgが入っているので実行してインストールします。


使い方



iOSシミュレータを起動してメニューの「ハードウェア」を開くと、「メモリ警告を繰り返し送る」が追加されています。


5秒毎、15秒毎、60秒毎の選択肢があるので、どれか一つを選択します。

あとはこの状態でアプリをぐりぐり実行させるだけです。
定期的にメモリ警告が発生するので、想定外のバグが発見できるかもしれません。
是非お試しください!



メモリ警告関連バグの対処法



備忘録も兼ねて・・・

ViewControllerからViewを生成する場合、大まかに以下のような流れになります。


1. ViewControllerの初期化メソッド(initなんちゃらメソッド)の実行
2. ViewControllerのloadViewの実行
3. ViewControllerのviewDidLoadの呼び出し


しかし、メモリ警告でviewが破壊されると、1の初期化メソッドが呼び出されず2と3だけが実行されることになります。そのため、初期化メソッド内でviewの操作を行っているとメモリ警告後におかしな動きになってしまうことになります。

これを踏まえて、

  • ViewControllerの初期化メソッドでviewの操作をしないこと
  • viewの操作をしたい場合はviewDidLoadで行うこと

この2点を守りながらコーディングすればメモリ警告によるバグはだいぶ防げるはずです。

Cordova(PhoneGap)のプラグインの作り方

こんにちは、橋本です。

今日はCordova(PhoneGap)のプラグインの作り方について書いていきたいと思います。

Cordovaは、HTML, CSS, Javascriptを組み合わせて、iOSAndroidのネイティブアプリが作れるというものです。
Cordovaで用意されたJavascriptAPIを用いることで、ネイティブの機能を使用することができるようになっています。

「HTML、CSSJavascriptでネイティブアプリが作れるとは!ネイティブの機能も使えるし!これはお手軽!!素晴らしい!!!」

と、思うかもしれませんが、Cordovaの内部では、ネイティブのWebviewの上でHTMLを動かし、JavascriptAPIを通じて予め用意されたネイティブの機能を使っているだけなので、実際にアプリを作り始めると、痒いところに手が届かない場面がしばしばあります。

たとえば、カメラを起動して写真や動画を撮ることはできるのですが、カメラ自体の機能を拡張したり、見た目を変えたりといったことは出来ません。
また、iTunesライブラリへのアクセスや、Bluetoothを使うといったことも、APIが用意されていないので出来ません。

ただ、Cordovaにはプラグインを組み込む機構が用意されているため、Objective-CJavaを使ってネイティブコードでプラグインを作ることで、機能を追加することができるようになっています。


というわけで、今回はiOSの場合を例にプラグインの作り方をご紹介したいと思います。


プラグインを作成するためには、ネイティブのコードと、それをjavascriptから実行するためのjavascriptコードを記述する必要があります。

1. ネイティブコードを作成する
というわけで、まずCordovaプロジェクトのPluginsフォルダの中に、Objective-Cのclassファイルを作成します。
このクラスは、CDVPluginクラスの子クラスとして作成してください。



新規ファイルを作成したら、まずは「.h」ファイルを開いてください。
完成後のソースはこんな感じです。

PGMyPlugin.h


#import <Cordova/CDV.h>

@interface PGMyPlugin : CDVPlugin

@property (nonatomic, copy) NSString *callbackId;

- (void)hello:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
@end


まず、初期状態ではがインポートされているかと思いますが、これをに修正します。

次に、NSString型のcallbackIdというプロパティを定義します。
これは、javascript側から渡されてくるIDを格納しておくためのプロパティで、javascriptに処理の実行結果を返すときに必要になります。
(プロパティ名はcallbackIdでなくても構いません。)

次に、メソッドを定義しています。
今回は「- (void)hello:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;」というメソッドを定義しました。
argumentsとoptionsはどちらも、javascriptから渡される引数なのですが、javascript側から渡した引数の配列の中で、オブジェクトはoptionsに連想配列として渡され、文字列や数値の場合には配列に値が入ってきます。

では、次に「.m」ファイルを開いてメソッドを実装していきたいと思います。
完成後のソースはこんな感じです。

PGMyPlugin.m


#import "PGMyPlugin.h"

@implementation PGMyPlugin
@synthesize callbackId;

- (void)hello:(NSMutableArray *)arguments withDict:(NSMutableDictionary *)options
{
    self.callbackId = [arguments pop];
    
    NSString *name = [arguments objectAtIndex:0];
    CDVPluginResult *result;
    NSString *ret;
    
    if (![name isEqual:@""]) {
        NSString *str = [NSString stringWithFormat:@"Hello %@", [arguments objectAtIndex:0]];
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:str];
        ret = [result toSuccessCallbackString:self.callbackId];
    } else {
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"名前をください!"];
        ret = [result toErrorCallbackString:self.callbackId];
    }
    
    [self writeJavascript:ret];
}
@end


まず、optionsの一つ目の値としてコールバック用のIDが格納されているので、それをcallbackIdプロパティに格納しています。

次にjavascriptから渡される引数をname変数に格納し、その有無で処理を切り分けています。
CDVPluginResultクラスは、処理結果を作成するためのクラスで、処理結果とステーテスコードを渡して作成します。
また、CDVPluginResultのtoSuccessCallbackStringメソッドや、toErrorCallbackStringメソッドを使うことで、javascriptで定義したコールバック関数を叩くための文字列を作成することができます。
これらのメソッドには、javascriptから渡されたコールバックIDを指定する必要があります。

最後に、writeJavascriptメソッドで、作成した結果の文字列をJavascriptとして実行して完了です。

プラグインの作成が完了したら、このプラグインを実際に使用可能にするために、「Cordova.plist」に登録する必要があります。
具体的には、Cordova.plistのPluginsというDictionaryの中にキーと値を記述します。
キーには、javascript側から呼ぶための名前を記述し、値にはプラグインのクラス名を記述します。
今回は、キーに「jp.co.asial.hello」と記述し、値に「PGMyPlugin」と記述しました。



ネイティブ側はこれで完了です。

2. Javascriptを作成する

次に、プラグインを呼ぶためのjavascriptコードを書いていきます。
完成後のソースはこんな感じです。


<!DOCTYPE HTML>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" charset="utf-8" src="cordova-1.5.0.js"></script>
</head>
<body>
    <input id="input" type="text" />
    <button id="btn">表示</button>
    <script>
        document.addEventListener('deviceready', function(){
            document.querySelector('#btn').addEventListener('click', function(){
                var str = document.querySelector('#input').value;     
                Cordova.exec(function(ret){alert(ret);}, function(error){alert(error);}, 'jp.co.asial.hello', 'hello', [str]);
            });
        });
    </script>
</body>
</html>


javascript側からネイティブのプラグインを呼ぶためには、Cordova.exec()メソッドを使用します。
メソッドの引数は、1つ目から順に、成功時のコールバック関数、失敗時のコールバック関数、Cordova.plistの「キー」に記述した値、呼びたいプラグインのメソッド名、引数の配列となります。

では、実行してみます。



このように、プラグインが実行されていることが確認できるかと思います。

今回は、この簡単なサンプルだけだと物足りないかと思い、iTunesMusicLibraryから音楽を取得するプラグインを作成してみましたので、こっちもよろしければ見てみてください。

PGiTunesMusicLibrary.h


#import <Cordova/CDV.h>
#import <MediaPlayer/MediaPlayer.h>
#import <AVFoundation/AVFoundation.h>

@interface PGiTunesMusicLibrary : CDVPlugin <MPMediaPickerControllerDelegate>

@property (nonatomic, copy) NSString *callbackId;

- (void)showMusicPicker:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
@end


PGiTunesMusicLibrary.m


#import "PGiTunesMusicLibrary.h"


@implementation PGiTunesMusicLibrary
@synthesize callbackId;

- (void)showMusicPicker:(NSMutableArray *)arguments withDict:(NSMutableDictionary *)options
{
    self.callbackId = [arguments pop];
    
    // メディアアイテムピッカーの表示
    MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAnyAudio];
    
    
    [picker setDelegate:self];
    [picker setAllowsPickingMultipleItems:YES];
    
    picker.prompt = NSLocalizedString(@"Add songs to play", @"Prompt in media item picker");
    
    [[super viewController] presentModalViewController:picker animated:YES];
}

- (void) mediaPicker: (MPMediaPickerController *) mediaPicker didPickMediaItems: (MPMediaItemCollection *) collection
{
    // @todo: くるくるを表示する
    
    // 音楽ファイルを取り出す
    // AVURLAssetを作成
    MPMediaItem *item = [collection.items lastObject];
    NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url
                                               options:nil];
    
    // AVAssetExportSessionを生成
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:urlAsset
                                                                           presetName:AVAssetExportPresetAppleM4A];
    
    exportSession.outputFileType = [[exportSession supportedFileTypes] objectAtIndex:0];
    
    // artworkとmusicファイルを保存するためのURLを作成
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *baseFilePath = [docDir stringByAppendingPathComponent:[item valueForProperty:MPMediaItemPropertyTitle]];
    NSString *artworkPath = [baseFilePath stringByAppendingPathExtension:@"png"];
    NSString *musicFilePath = [baseFilePath stringByAppendingPathExtension:@"m4a"];
    
    // ArtworkのExport。
    UIImage *artwork = [[item valueForProperty:MPMediaItemPropertyArtwork] imageWithSize:CGSizeMake(100, 100)];
    NSData *artworkData = UIImagePNGRepresentation(artwork);
    
    bool artworkCreated = [artworkData writeToFile:artworkPath
                                        atomically:YES];
    
    // MusicファイルのExport。Exportは非同期に行われるので、完了時にJavascriptに値を返すようにする
    exportSession.outputURL = [NSURL fileURLWithPath:musicFilePath];
    // blocks用にselfを格納
    __block PGiTunesMusicLibrary *blockSelf = self;
    
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        NSMutableDictionary *dic;
        CDVPluginResult *result;   
        
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                   musicFilePath, @"musicUrl",
                   [item valueForProperty:MPMediaItemPropertyTitle], @"title", 
                   nil];
            
            // artworkがある場合のみ
            if (artworkCreated) {
                [dic setObject:artworkPath forKey:@"artworkUrl"];
            }
            
            // javascriptの成功時のコールバックに値を渡す
            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dic];
            [blockSelf performSelectorOnMainThread:@selector(writeJavascript:)
                                        withObject:[result toSuccessCallbackString:blockSelf.callbackId]
                                     waitUntilDone:YES];
        } else {
            dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                   @"Music File Export Error.", @"message",
                   nil];
            
            // javascriptの失敗時のコールバックに値を渡す
            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dic];
            [blockSelf performSelectorOnMainThread:@selector(writeJavascript:)
                                        withObject:[result toErrorCallbackString:blockSelf.callbackId]
                                     waitUntilDone:YES];
        }
        
        [[blockSelf viewController] performSelectorOnMainThread:@selector(dismissModalViewControllerAnimated:)
                                                     withObject:[NSNumber numberWithBool:YES]
                                                  waitUntilDone:YES];
    }];
    
}

- (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker
{
    [[super viewController] dismissModalViewControllerAnimated:YES];
}

@end


index.html


<!DOCTYPE HTML>

<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script src="http://code.jquery.com/jquery-latest.js"></script>
        <script src="cordova-1.5.0.js" type="text/javascript"></script>
        <style type="text/css">
            #musicList {
                list-style: none;
                padding: 0;
                margin: 5px;
            }

            #musicList li .artwork {
                height: 50px;
                width: 50px;
                border: solid 1px #bbb;
                -webkit-box-shadow: 1px 1px 3px #000;
            }

            #musicList li div.artwork {
                background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#fff));
                display: table-cell;
                vertical-align: middle;
                text-align: center;
                font-size: 0.8em;
            }
        </style>
    </head>
    <body>
        <audio id="player" controls></audio>
        <button id="btn">音楽を選択</button>
        <ul id="musicList"></ul>
        <script type="text/javascript">
            document.addEventListener('deviceready', function(){
                var iTunesMusicLibrary = {
                    showMusicPicker: function(types, success, fail) {
                        return Cordova.exec(success, fail, "jp.co.asial.iTunesMusicLibrary", "showMusicPicker", types);
                    }
                };

                var playMusic = function(musicUrl){
                    var $_player = $('#player');
                    $_player
                    .attr({
                        'src': musicUrl    
                    });
                    $_player.get(0).load();
                    $_player.get(0).play();
                };

                var addMusicItem = function(data){
                    var $_list = $('#musicList');

                    // artworkの有無を確認
                    var $_appendedImg;
                    if (data.hasOwnProperty('artworkUrl')) {
                        $_appendedImg = $('<img>')
                        .attr({
                            'src': data.artworkUrl,
                            'class': 'artwork'
                        })
                        .bind('touchend', function(){
                            playMusic(data.musicUrl);      
                        });
                    } else {
                        $_appendedImg = $('<div>')
                        .attr({
                            'class': 'artwork'
                        })
                        .text('No image')
                        .bind('touchend', function(){
                            playMusic(data.musicUrl);      
                        });
                    }

                    $_list.append(
                        $('<li>')
                        .append($_appendedImg)
                        .append(
                            $('<a>')
                            .attr({
                                'href': 'javascript:(function(){return undefined;}())',
                                })
                            .text(data.title)
                            .click(function(){playMusic(data.musicUrl)})
                        )
                    )
                }

                var MusicManager = function(){
                    if (!MusicManager.instance) {
                        var musicList = localStorage.getItem(MusicManager.key);
                        musicList = musicList ? eval('(' + musicList  + ')') : [];

                        this.setMusic = function(data, callback) {
                            data.id = new Date().getTime();
                            musicList.push(data);

                            localStorage.setItem(MusicManager.key, JSON.stringify(musicList));

                            // callbackがある場合は実行
                            if (callback  & & typeof callback === 'function') {
                                callback(data);
                            }
                        };

                        this.getMusic = function(musicId) {
                            if (musicId) {
                                var ret;
                                musicList.forEach(function(item, index, arr){
                                        if (!ret  & & item.id == musicId) {
                                        ret = item;
                                        }     
                                        });

                            } else {
                                ret = musicList;
                            }

                            return ret;
                        }

                        MusicManager.instance = this;
                    }

                    return MusicManager.instance;
                };

                MusicManager.getInstance = function(){
                    if (!this.instance) {
                        this.instance = new MusicManager();
                    }

                    return this.instance;
                }

                MusicManager.key = "MUSIC_LIST";

                $('#btn').click(function(){
                    iTunesMusicLibrary.showMusicPicker([], function(data){
                        // urlにファイルのURL、titleにタイトルが入っているので、localStorageに保存
                        MusicManager.getInstance().setMusic(data, addMusicItem);
                    }, function(error){
                        alert(error.message);
                    });
                });


                // 初期化処理
                MusicManager.getInstance().getMusic().forEach(function(item, index, arr){
                    addMusicItem(item);     
                });
            });
        </script>
    </body>
</html>


ではでは。

【ぐぬぬ】iPhoneアプリのこんなとき、どうするの??

こんにちは、iPhone大好き橋本です。

今日はiPhoneアプリの作成に関する「こんなときどうするの??」という疑問にお答えするべく、小技をいくつか紹介したいと思います。


1. 文字サイズに合わせてUILabelのサイズを変えたい。

UILabelを作成するときに、Labelに表示する文字によってUILabelのサイズを変えたいということ無いですか??ただ、UILabelを作成するときはframeのサイズを指定しなきゃいけませんよね。

こんなとき、どうするの??

そんなときには、UILabelにこんなカテゴリを書いちゃいましょ。



@interface UILabel (resize)
- (void) resizeWithPoint: (CGPoint)point;
- (id) initWithText:(NSString *)text andFont:(UIFont *)font;
- (id) initWithText:(NSString *)text andFont:(UIFont *)font andPoint:(CGPoint)point;
- (CGRect)resizedFrameWithPoint: (CGPoint)point;
@end

@implementation UILabel (resize)
- (void)resizeWithPoint:(CGPoint)point {
    self.frame = [self resizedFrameWithPoint:point];
}

- (id)initWithText:(NSString *)text andFont:(UIFont *)font {
    self = [self initWithText:text
                      andFont:font
                     andPoint:CGPointMake(0, 0)];
    
    return self;
}

- (id)initWithText:(NSString *)text andFont:(UIFont *)font andPoint:(CGPoint)point {
    self = [super initWithFrame:CGRectZero];
	
	if (self) {
		self.numberOfLines = 0;
		self.font = font;
		self.text = text;
        
        self.frame = [self resizedFrameWithPoint:point];
	}
    
    return self;
}

- (CGRect)resizedFrameWithPoint:(CGPoint)point
{
    CGSize size = [self.text sizeWithFont:self.font
                        constrainedToSize:CGSizeMake([[UIScreen mainScreen] bounds].size.width, NSIntegerMax) 
                            lineBreakMode:UILineBreakModeTailTruncation];
    
    CGRect resizedFrame = CGRectMake(point.x, point.y, size.width, size.height);
    
    return resizedFrame;
}

@end


NSStringクラスの sizeWithFont:forWidth:lineBreakMode:メソッドを使用することで、UILabelの表示に必要なCGSizeを得ることができます。

CGSizeが取得できれば、あとは、UILabelのframeに取得したCGSizeを反映させれば、表示する文字に応じたサイズのUILabelを作成することができます。

CGSizeを得るためには、fontとtextが必要なため、今回はfontとtextを引数とするメソッドを書いてみました。オブジェクトを使い回すときのために、resizeWithPoint:も実装しておくといいと思います。

べんりー!


2. UIAlertViewにUITextFieldを表示したい。

UIAlertViewにUITextFieldを表示したいっていう場面があると思うんです。
ログインをユーザに求めるAlertを出したいときとか。

ただ、デフォルトのUIAlertViewだと、テキストを表示することしか出来ませんよね。

こんなとき、どうするの??

そんなときは、こういう感じで強引に実装しちゃいましょ。




UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"ログイン"
                                                message:@" \n\n"
                                               delegate:self
                                      cancelButtonTitle:@"Cancel"
                                      otherButtonTitles:@"Login", nil] autorelease];

// ユーザ名
UITextField *userNameField = [[[UITextField alloc] initWithFrame:CGRectMake(12, 45, 260, 24)] autorelease];
userNameField.placeholder = @"ユーザ名";
userNameField.font = [UIFont systemFontOfSize:12];
userNameField.borderStyle = UITextBorderStyleRoundedRect;
userNameField.backgroundColor = [UIColor clearColor];
userNameField.tag = 1;

// キーボードのフォーカスを当てる
[userNameField becomeFirstResponder];

// パスワード
UITextField *passwordField = [[[UITextField alloc] initWithFrame:CGRectMake(12, 72, 260, 24)] autorelease];
passwordField.placeholder = @"パスワード";
passwordField.font = [UIFont systemFontOfSize:12];
passwordField.borderStyle = UITextBorderStyleRoundedRect;
passwordField.backgroundColor =  [UIColor clearColor];
passwordField.tag = 2;
passwordField.secureTextEntry = YES;

[alert addSubview:userNameField];
[alert addSubview:passwordField];
[alert show];



UIAlertViewのメッセージに改行を2つ入れて二行分のスペースを確保します。そのスペースにうまいことUITextFieldを入れてやります。
これだけでUITextFieldをもつUIAlertViewを表示出来ちゃうんですね。

べんりー!


3. UITextViewで表示したURLのリンク先をSafariじゃなくてUIWebviewで表示したい。

UITextViewのdataDetectorTypesプロパティにUIDataDetectorTypeLinkを設定すると、UITextViewに表示した文字列に含まれるリンクを拾うことができるようになるのはご存知のことかと思いますが、これだけだとリンクをクリックしたときにSafariで表示されちゃうんですね。

いちいちSafariが起動するのはイヤな感じですよね。

iPhone3Gだとマルチタスク対応してないので、Safariから戻ってきたときに、アプリを起動しなおすことになりますよね。

なんてだるい。
これはナンセンス。

UIWebViewを使ってアプリ内で表示できたら最高ですね。
いや、むしろ、ユーザにUIWebViewで開くか、Safariで開くか選択させてあげたいですよね。

UIActionSheetとか使って。

ただ、UITextView側ではこれを制御することが出来ないんです。

こんなとき、どうするの??

リンクが押されたときには、UIApplicationのopenUrl:メソッドが呼ばれるんです。
なので、このメソッドをオーバーライドしたサブクラスを作っちゃえばいいんですね。こんな感じで。



@interface MyUIApplication : UIApplication {
    NSURL *openUrl_;
}

@property (nonatomic, retain) NSURL *openUrl;
@end

#define ACTIONSHEET_OPEN_URL 1

@implementation MyUIApplication
@synthesize openUrl = openUrl_;

- (BOOL)openURL:(NSURL *)url
{
    if (!url) {
        return NO;
    }
    
    self.openUrl = url;
    
    UIActionSheet *sheet = [[[UIActionSheet alloc] init] autorelease];
    sheet.delegate = self;
    [sheet addButtonWithTitle:@"このアプリで開く"];
    [sheet addButtonWithTitle:@"Safariで開く"]; 
    [sheet addButtonWithTitle:@"キャンセル"];
    sheet.cancelButtonIndex = 2;
    sheet.tag = ACTIONSHEET_OPEN_URL;
    
    hogeAppDelegate *appDelegate = (hogeAppDelegate *)[self delegate];
    
    [sheet showInView:appDelegate.window];
    
    return YES;
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (actionSheet.tag == ACTIONSHEET_OPEN_URL) {
        switch (buttonIndex) {
            case 0:
            {
                // WebView表示用のController。各々自作してくださいね!デフォじゃないよ!
                WebViewController *webViewController = [[[WebViewController alloc] initWithUrlStr:[self.openUrl absoluteString]] autorelease];
                
                 // 今回はUITabBarとUINavigationを組み合わせたアプリのnavigationに突っ込んでる例です。ここは各自カスタムしてね!
                hogeAppDelegate *appDelegate = (hogeAppDelegate *)[self delegate];
                UINavigationController *nav = (UINavigationController *)[appDelegate.tabBarController selectedViewController];
                
                [nav pushViewController:webViewController animated:YES];

                break;                
            }
                
            case 1:
            {
                [super openURL:self.openUrl];
                break;                   
            }
                
            case 2:
            {
                break;
            }

            default:
                break;
        }
    }
    
    self.openUrl = nil;
}

@end


あとはこのクラスをデフォルトのUIApplicationと置き換えるだけ!main.mを書き換えてください。



int main(int argc, char *argv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // UIApplicationMain関数の第三引数を書き換える。
    int retVal = UIApplicationMain(argc, argv, @"MyUIApplication", nil);
    [pool release];
    return retVal;
}


なんてユーザフレンドリー!

べんりー!


4. UIViewを角丸にしたい。
したいですよねー、角丸。やっぱりどうしてもこういう欲求出てきますよねー。

こんなとき、どうするの??

簡単です。

UIViewのlayerプロパティをいじりましょう。layerをいじりたいときはQuartsCore/QuartsCore.hをimportするのをお忘れなく。



UIView *hoge = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)] autorelease];
hoge.layer.cornerRadius = 8.0f;


はい。これだけです。

べんりー!


5. UIViewを角丸にしたついでに影とかつけたい。

影もつけたいですよね。そうくると思ってました。わかります。

じゃあ、こんなとき、どうするの??

これもlayerいじると簡単に実装出来ちゃうんですね。



UIView *hoge = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)] autorelease];
hoge.layer.cornerRadius = 8.0f
// 次の二行を追加するだけです。
hoge.layer.shadowOpacity = 0.8;
hoge.layer.shadowOffset = CGSizeMake(2, 2);


shadowOpacityは影の濃さ、shadowOffsetは影の場所です。あとは、shadowColorで色を変えたりすることもできますよ。

べんりー!

今回は5つほど小技を紹介させていただきました。どれも実際にアプリを作っていると出てくる疑問点、欲求かと思います。

今回紹介させていただいた小技が、少しでも読んでいただいた方のお役にたてると幸いです。

ではでは。

『iPad電子書籍アプリ開発ガイドブック』の執筆の一部を担当しました

今回は、弊社の新井が執筆の一部を担当した書籍「iPad 電子書籍アプリ 開発ガイドブック」の紹介をさせていただきます。




アシアルでは、iPhone/iPad/Androidアプリの開発にも精力的に取り組んでおり、以下にて事例の公開も行っています。

アシアルサイト:iPhone/Androidアプリ構築

また、日経BP社が提供する、日本最大級のIT情報サイト「ITpro」にも、iPhone/Androidにおいて、その世界観や、アプリ登録方法、デバッグからリリースまでの手順、基本的な開発方法などについて連載記事の執筆を行っています。

オール・イン・ワンiPhone開発
Androidで広がる,携帯アプリ開発の世界
開発者から見たiPhoneとAndroid

更に、日経ソフトウェア様・ITpro様主催で、弊社開発陣が講師を務める「iPhone/iPadAndroid向け業務アプリケーション開発 実践セミナー」を開催いたしました。2回シリーズで、「Android」「iPhone/iPad」のそれぞれの開発について丸1日かけて解説しております。こちらのセミナーについては、述べ600名を超える方にご来場いただき、数多くのご好評のお言葉をいただきました。




今回、一部執筆を担当した書籍では、「App Storeで販売できる雑誌・書籍アプリのプログラミングから、ePub形式によるコーディングまでiPad向け電子書籍を徹底解説!」と謳っているように、iPad電子書籍リーダーとしての役割に着目し、

iPadのリーダー「CloudReaders」の作者の中島聡氏による、iPadのプログラミング経験談
iPadアプリを作成するための環境作り
・実際のプログラミング解説(※ この部分を担当しています)
電子書籍の仕様「ePub」の概要と、その表示仕方・作成仕方についての説明
・ニューヨークでの電子書籍事情や、紙の書籍から電子書籍への発展・広がり

など、プログラミングの解説だけでなく、幅広い視点から電子書籍についての記載がなされています。


今回、新井が担当したのが、「iPadの雑誌アプリを作る」という章で、Objective-Cを記述して、実際にiPad用の雑誌アプリを作るノウハウについて執筆しています。
単純にスキャンした紙雑誌のページを閲覧するだけでなく、電子書籍ならではのアプリとして、画像スクロール機能やアニメーション機能、ビデオ再生、グーグルマップの表示といったさまざまな要素を盛りこんだアプリの作成を行います。


こちらの本に関して、筆者は以下のようにコメントしております。

iPadアプリを初めて作成する読者を想定して、先に基本的な機能の実装方法についての紹介をした後で、それらを組み合わせてサンプルアプリを作る、という構成にしました。また、電子書籍のサンプルアプリと言ってもただのPDFビュアーになってしまわないように、自前で作れる電子ペーパーのようなものを想定して作成するようにしました。
既にiPhoneアプリを作った経験のある方には物足りないかもしれませんが、iPadを通して何か物作りをしてみたい、と思った方にキッカケとなってもらえれば嬉しいと思っております。


宜しければ、一読いただき、本人も非常に喜びますので、その感想を届けていただけたらと思います。

>>BOXNC
 
第3章 iPadの雑誌アプリを作る


3-1 電子雑誌の各機能の紹介
 
以下、電子雑誌に基本となる各機能について、ソースコードと共に、その実装方法について記述します。
 
・プロジェクトの作成
・ページの色やフォント・余白の設定
・画像の描画
・テキストの表示
・動画の再生
・地図(グーグルマップ)の表示
・各種アニメーションの設定
・画像のスクロール
・ポップアップ表示
・ページ送り
・画面の分割
 
TOPIC | ULColor
TOPIC | iPadで使用できるフォント
TOPIC | カスタムフォントを使う
TOPIC | 文字に色をつける
TOPIC | ボタンに画像を設定する
TOPIC | ボタンの種類を変更する
TOPIC | URLを自動的に検出する
TOPIC | ローカルのhtmlを読み込む
TOPIC | 音楽を再生する
TOPIC | 現在位置を表示する
TOPIC | アノテーションを表示する
TOPIC | タップを検知してアニメーションを開始する
TOPIC | UIImageViewを使ったフレームワークアニメーション
TOPIC | ページコントロールを使用する
TOPIC | スワイプを検知してページ送りをする
 
3-2 電子雑誌のサンプルアプリを実装する
 
3-1に紹介されたさまざまな機能を利用して、動画の再生、地図の表示を含む、実際に電子雑誌のサンプルアプリケーションを1つ作っていきます。
 

「PHP×iPhone」PHPテクニカルセミナー(無料)第5弾の募集を開始しました!!

皆さま。こんにちは。
和田でございます。

今年の6月から全5回にわけて開催してきました「開発者向け、一歩先を行くためのテクニカルセミナー(無料)」も、次でいよいよ最終回となります。
ここまでこれたのも、たくさんの皆さまが参加して下さったおかげです。
この場を借りてお礼申し上げます。

さて、今回のセミナーは、皆さまお待ちかねの「PHP×iPhone」です。
講師は第1線で活躍している弊社エバンジェリストの亀本が担当いたします。
亀本は、iPhoneアプリケーションの開発はもとより、大手情報総合サイトや人材系の公開セミナーでもiPhoneの講演をしている実力者です。
しかも毎回満席になるほどの人気ぶりですので、どうか皆さま、この機会にふるってご応募ください!!!

お申し込みは、以下URLよりお願い致します。
https://www.asial.co.jp/seminar/




【開催概要】

日程: 10月21日(木) 10:00 ~ 12:00(※ 開始30分前より入室可能)
会場: 山王健保会館2F 多目的ホール
交通: 地下鉄銀座線・南北線溜池山王駅 7出口 徒歩3分
    地下鉄千代田線:赤坂駅 1出口 徒歩5分
    地下鉄銀座線・丸ノ内線赤坂見附駅出口 下車徒歩7分
主催: アシアル株式会社
受講対象者: Webシステムにおいて1~3年の開発経験のある方
定員: 100名(※ 定員となり次第、受付を終了させていただきます 。)
参加費: 無料(事前登録制)


【第5回 PHP×iPhoneiPhoneアプリケーション開発のノウハウ

2007年に登場したiPhoneは、今やアプリの登録数が10万本に達し、ダウンロード件数は30億本を超えるという、大変大きな市場となってきています。
そして、多くの企業がiPhoneアプリケーションや対応サービスをリリースする中、Webデベロッパに対しても、クライアントアプリとしてのiPhoneアプリを構築する力が求められ始めています。

そこで本セミナーでは、iPhoneアプリ開発に必要とされる端末やOSなどの様々な予備知識から、Objective-Cの基本文法の解説、また、iPhone独自のインターフェースを用いたプログラミングの手法などを、PHP開発者向けにエッセンスを抽出してお届けします。

講師は、現場で活躍する弊社エバンジェリストの亀本が担当いたします。

講演者:アシアル株式会社 亀本 大地(カメモト ダイチ)
アシアル株式会社のエバンジェリスト
Webシステムの構築と、iPhoneアプリケーション開発を行っている。システム構築においては、主に企業の業務システムやモバイルSNSの開発などに従事。
iPhoneアプリケーションの実績としては、ITproアプリケーションをはじめ、Webと連携するアプリケーションなどを多く手掛けている。




【第3回 「PHP×Flex(前編)」セミナーを受講された皆さまの声】
------------------------------------------------------
Flexでの開発の流れを知れたのが良かった
・業務用に使えそう
Flexを使ってみようとおもった
Flexは想像していたよりははじめ易いと思った
・自分で開発を行ううえでのイメージが出来た
・「何で?!」と思うような点のTipsも含まれていたので実践に役立ちそう
Flexを使った動的コンテンツの作成について詳しく知れた
・内容が濃くて良かった。スピード感も良い
・プログラム作成者の観点で資料がまとまっていた
PHPActionScriptの書き方の比較がありイメージしやすかった
Flex、ActionScriptnitについて詳しく知りたくなりました
・基本的な部分を丁寧に説明してくれていたため、とても分かりやすかった
------------------------------------------------------

IBを使わずに作るiPhoneアプリ作成入門:第2回

こんにちは、亀本です。目下の悩みはブログのタイトル欄の短さです。サブタイトルつけると長すぎてダメぽ!

そんなことはさておき、前回はそこそこ反響をいただいたようで、ちょいちょいとiPhoneアプリ開発の連載を続けていこうかと思います。
最初は続きを書くか迷っていて、忙しさもあってちょっと間が空いてしまいましたが、これからはできるだけちょこちょこと書いていけるようにしていきます。

第1回はこちら:http://blog.asial.co.jp/502

第2回に今さら言うのもアレですが、全体の流れとしては、まずはいろんなView / Controllerの使い方を説明し、個別の機能を理解するとともに、iPhoneアプリケーションで採用されているMVCフレームワークの構造を紹介していきます。
それが終わった後で、何かそこそこ楽しめそうなアプリ作成を順を追って説明できればいいかな、なんて思っています。

時にはIB(Interface Builder)との比較も用いながら、IBのどの役割をどのメソッドが担当しているのか、などを解説できたら、IB無しでも構造的にきれいなMVCプログラミングができるんじゃないか思って、その辺を目標にしています。
# ただし、そのあたりの説明は自分の経験則になってくるので、正確性は保障できないのですが。。。間違いがあったらぜひ突っ込みをお願いします><


今回の目標

今回は、前回適当に貼り付けたテーブルをカスタマイズする方法を通じて、Controllerの基本的な使い方を説明していきたいと思います。
まだiPhoneMVCフレームワークがどんな構造か、までは説明が到達しません。
デキるプログラマの人には冗長な解説かもしれませんが、ゆっくりゆっくり。

テーブルをカスタマイズする方針としては、前回使用したUITableViewControllerを継承したクラスを作成し、テーブルの性質や動作を定義するための各メソッドをオーバーライドしていきます。
iPhoneでの開発は、基本的にはこの方針のようにフレームワークの用意したControllerをオーバーライドする形で進みます。


新しいコントローラの生成

まずは新しいコントローラクラスを記述するファイルを生成します。
プロジェクトへのファイルの追加は、Xcode左側の「グループとファイル」のツリーで行うのが楽です。(メニューからもできますが。)
各種クラス実装は、基本的にsamplePrjプロジェクトのClassesグループ内に追加していきます。
ここでは、ClassesグループをCtrl+クリックして「追加」→「新規ファイル」と選択します。



すると、新規ファイルのテンプレートを選択する画面が開きます。
最初にプロジェクトを作成するときと同様、Xcodeでは各クラスを継承した新たなクラスを作る際のテンプレートも用意してくれています。
用意されているテンプレートは5種類ですが、言ってみればこの5種類がiPhone開発時のほとんどを占めることになる、ということです。
(なお、どれもただの「テンプレート」生成なので、後々自分が作りたいのに適切なものが無い場合には、適当なテンプレートを生成後、適宜書き換えましょう。)



ここでは、「UITableViewController subclass」を選択し、SimpleTableViewController.mという名前を付けて生成しましょう。
すると、Classesグループ内に新たにSimpleTableViewController.hとSimpleTableViewController.mという名前のついた2つのファイルが生成されます。

Objective-Cでは、クラスの定義はインターフェース定義(@interface)と実装定義(@implementation)の2つを別々に記述します。
主に.hファイル(ヘッダファイル)でクラスのインターフェースを定義し、.mファイルでその実装を記述するのが流儀です。

ここで、SimpleTableViewController.hを見てください。おそらく、次のような内容のファイルができていると思います。
この中身をざっと説明しましょう。



@interface SimpleTableViewController : UITableViewController {

}

@end


上のコードはただ単にUITableViewControllerクラスを継承し、SimpleTableViewControllerというサブクラスを定義しています。ただそれだけで他に何もしていません。

インターフェースの定義式としては


@interface SubClass : SuperClass <interface1,interface2, ...> {
  // ここでメンバ変数定義
}
// ここでシグネチャ定義

@end

という形になります。(メンバ変数とシグネチャはおいおい説明します。)
クラスの定義は、このように@interface ~ @endまでの間に色々な物を記述する形で行います。
クラス定義時には、:「コロン」で区切って親クラスを記述し、<>で囲った中に組み込むインターフェースを列挙します。必要ない場合には、それぞれ省略できます。


さて、新しいコントローラが生成できたところで、現在使っているUITableViewControllerから、SimpleTableViewControllerを使うように切り替えましょう。

sampleProjAppDelegate.mを、以下のように修正します。



#import "samplePrjAppDelegate.h"
#import "SimpleTableViewController.h"
 
@implementation samplePrjAppDelegate
 
@synthesize window;
 
- (void)applicationDidFinishLaunching:(UIApplication *)application {
	SimpleTableViewController *simpleTableViewController = [[SimpleTableViewController alloc] initWithStyle:UITableViewStylePlain];
	[window addSubview:simpleTableViewController.view];
 
    // Override point for customization after application launch
    [window makeKeyAndVisible];
}
.
.
.


まず2行目で、SimpleTableViewController.hをimportで読み込んでいます。
importとは、PHPで言うところのrequire_onceに当たるもの(仕組み自体は違いますが)で、まだ読みこまれていなければそのファイルを読み込む、というものです。
ユーザ定義のクラスを利用するには、このように#importを使ってヘッダファイルを読み込む必要があります。

実装コード側では、UITableViewControllerを指定していた部分が、SimpleTableViewControllerに置き換えてあります。

これでビルドを実行すると、SimpleTableViewControllerが利用されるようになります。
見た目的には変わりませんが。。(^^;

セル内容の表示

それでは、新しいテーブル内にデータを流し込んでみましょう。

まず、SimpleTableViewControllerにデータの格納用メンバ変数を定義します。
メンバ変数の定義はインターフェース部で行います。SimpleTableViewController.hを次のように編集します。



@interface SimpleTableViewController : UITableViewController {
	NSArray *myDataSource;
}
@property (nonatomic, retain) NSArray *myDataSource;
 
@end


myDataSourceというメンバ変数を定義しています。この変数に、テーブルに表示するデータを定義します。
まず、@interfaceのクラス定義の{}ブロック内に記述してあるのが、メンバ変数の定義です。定義は、型の指定と変数名の指定で行われます。
ここでは、NSArrayオブジェクト型(のポインタ)を格納するmyDataSorceという変数を定義しています。
(※なお、NSArray型とは、Objective-Cフレームワークに組み込みの、配列オブジェクトです。)

また、ブロック外に新たに@propertyという指定が追加されていますが、これは指定したメンバ変数のsetterとgetterのシグネチャを同時に定義する糖衣構文です。
hogeというメンバ変数に対して、setHogeというsetterとhoge:というgetterのシグネチャが定義されます。
ここでは、myDataSourceが指定されているので、setMyDataSourceとdataSourceというメソッドのシグネチャが定義されたことになります。
(※あくまでも、シグネチャの定義だけであって、実装は別に.mファイルの実装部に記述しなければならない点に注意してください。)

@propertyの()内の指定は、自動生成されるsetter/getterの性質を定義するものです。ここではおまじない程度に思っておいてください(名前からなんとなく想像できるかもしれませんが)。
オブジェクト型を指定したメンバ変数定義の場合は、ほとんどの場合このおまじないの通りで大丈夫です。


次にSimpleTableViewController.mに実装を行い、myDataSourceの内容を読み込んでテーブルにデータを表示するようにしましょう。
修正すべき点は、以下の3つです。

1. synthesizeの追加

まず、@implementationの下に@synthesizeという指定を追加します。
(なお、@implementationは実装部の開始を示すもので、@interfaceと同様@implementation ~ @endの間に実装コードを記述します。)



@implementation samplePrjAppDelegate
@synthesize myDataSource;


@synthesizeは、指定したメンバ変数に対して、自動的に単純なgetterとsetterの実装を定義してくれます。
言ってみれば@propertyの実装版で、多くの場合対で使用されます。
この@synthesizeと@propertyを使うと、面倒なgetter/setterの定義が非常に楽に済みます。

2. テーブル内行数のカウント

テーブル内の行数設定は、tableView: numberOfRowsInSection: というメソッドで行われます。



- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [myDataSource count];
}


メソッドの定義について、ざっくりとイメージだけつかめるような説明を書いておくと



という感じです。
修飾子はクラスメソッドなら+、インスタンスメソッドなら-になります。
引数セパレータは、関数名の一部だと思っても構わないようなレベルのもので、メソッド呼び出し時に第2引数を指定する際には、第1引数に続けてこのセパレータもきちんと記述しなければなりません。
文章としてメソッド名を記述する場合は、このセパレータも含めて記述したりもします。

上記の tableView:numberOfRowsInSection:というメソッドは、UITableViewControllerで定義されているメソッドで、戻り値が1つのテーブルで扱われるセルの数になります。
myDataSource変数はNSArrayオブジェクト型なので、NSArrayオブジェクトがもともと持っているcountメソッドを呼び出すことで、その配列要素数を取得できます。

これによって、myDataSourceに格納したデータの数だけテーブルのセルが用意されるようになります。

3. 各セルへのデータの追加

各セルの中身を実際に作成・処理するのは、tableView:cellForRowAtIndexPath:メソッドです。



- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }
    
    // Set up the cell...
	cell.text = [myDataSource objectAtIndex:indexPath.row];
 
    return cell;
}


このメソッドは、生成されたTableViewのセル数分だけループ実行され、第2引数のindexPathに現在のループのセル番号(0スタート)が渡されます。

上記のコード中で、自動生成されたコードから変更されているのは下から3行目の


	cell.text = [myDataSource objectAtIndex:indexPath.row];


という行だけです。Objective-Cのメソッドは割と英文として読みやすいものが多いので、何となく動作の想像がつくのではないかと思います。
objectAtIndex:もNSArrayの組み込みメソッドで、指定したindexの配列要素を返します。また、indexPath.rowは現在のセル番号です。
ここでの処理は、myDataSource配列の中から指定したオブジェクトを取り出して、cellのtext要素の中に渡す、というものです。

その前の部分の処理は、細かくは説明しませんが、
① セルのオブジェクトに付与する固有名としてCellIdentifierという静的な変数を作成
② CellIdentifierをキーにして、既にCellオブジェクトを生成済みならそれを使いまわす。
③ もし生成されていなかったら、新たにCellIdentifierをキーとしてCellオブジェクトを生成
という流れです。

これをやっている理由は、Cellオブジェクトの生成コストの問題があるからです。
データが1つや2つの生成なら問題ないのですが、テーブル内のセル数がどんどん増えて行った場合、毎回生成していたのではオブジェクトの生成だけでかなりのパワーを要しますし、メモリの消費も多くなります。
iPhoneはそれほど強力なマシンではないので、こうして無駄なメモリ消費を抑えるような工夫をしています。
この自動生成コードだと、Cellという名前の1つのインスタンスをひたすら使いまわしています。

ともかく、これでセル内にテキストを渡す仕組みは実装できました。

データの流し込んでみる

それでは、sampleProjAppDelegate.mを以下のように編集してビルドしてみます。



- (void)applicationDidFinishLaunching:(UIApplication *)application {
	NSArray *data = [[NSArray alloc] initWithObjects:
					 @"hoge",
					 @"foo",
					 @"bar",
					 nil
					 ];
	
	SimpleTableViewController *simpleTableViewController = [[SimpleTableViewController alloc] initWithStyle:UITableViewStylePlain];
	[simpleTableViewController setMyDataSource: data];
 
	[window addSubview:simpleTableViewController.view];
 
    // Override point for customization after application launch
    [window makeKeyAndVisible];
}


変更点は、dataというNSArray型の変数が追加されたのと、それをsimpleTableViewControllerのsetterに渡している点のみです。

なお、補足しておくと、NSArrayの生成のところで使っているinitWithObjects:は前回説明したイニシャライザです。このメソッドは、引数にカンマ区切りで続けて並べたオブジェクトをすべて配列の要素として格納していきます。
配列の終端を指定するには、nil(空のポインタ)を指定します。
なお、直感的にわかると思いますが@"hoge"などという指定は文字列オブジェクトであるNSStringの生成、初期化の糖衣構文です。
ここでは、hoge,foo,barという3つの文字列オブジェクトをdata配列に追加している、ということになります。

実際にビルドしてみると、以下の画像のような結果が表れると思います。



まとめ

これでひとまず、テーブルへのデータの流し込みの基本的なフローを見てきました。
今回やった事は
・新しいContorllerクラスの作成方法と定義、および使用方法
  →@interfaceと@implementation
・メンバ変数とgetter、setter
  →@propertyと@synthesize
・TableViewオブジェクトの操作のキソのキソ

やたらと長ーーーーーーーーい説明になってしまいましたが、判りにくいところとかおかしかったところはぜひぜひ突っ込みお願いします><

次回

次回は、ちょっとばかり理屈じみた話になります。
何でかというと、実は、1回2回と見てきたプログラムは、適切なメモリ管理の意識されていないプログラムになっています(※説明のため、そうなってます)。
そこで、次回はここから色々な知識を身につけていく前に、メモリの確保と開放をきちんと意識したプログラミングのための考え方と手法を説明していきます。
また、そろそろ「自分でメソッド調べてモリモリ書いてみるかな!」というノリの人が出始めるかなーなんて思うので、余裕があったらAPIリファレンスとかXcodeの補完機能の使い方等を説明しようかと思います。