2015年3月26日木曜日

処理を一定時間止める

同期処理で、一定時間処理を待たせたり、数秒間何かを表示してから消すとかそういう時に使える。

以下2秒間処理停止させる書き方をいくつか。

NSThreadを使う方法

‪Objective-C‬

[NSThread sleepForTimeInterval:2.0f];

Swift3

NSThread.sleepForTimeInterval(2.0)

Swift4

Thread.sleep(forTimeInterval: 2.0)

sleep関数を使う方法

sleep(2);
でも同じことができる。こっちはC言語の関数なんだろう。
上の2つは結果は一緒みたいで、画面更新まで止まるようだ。直前に書き換えた画面は停止が明けないと更新されない。
処理的にはどっちでもいいんだろうけど、NSThread使ったほうがiOS的には推奨だったりするのかな?

NSRunLoopを使う方法

‪Objective-C

‬[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0f]];

Swift3

NSRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 2.0))

Swift4

RunLoop.current.run(until: Date.init(timeIntervalSinceNow: 2.0))

NSRunLoopってのは要するにアプリのバックで回り続けてる入出力とかの監視をしてるプロセスのようだ。よくわかってないけど。
こっちは画面更新は止まらないので、直前に書き換えたものをちゃんと表示してくれる。
逆に、書いた部分のコードの進行以外は実行されちゃうようなので、注意。

直前の画面書き換えを反映させてから処理を停止させる方法

画面更新はOSが定期的(数十分の1秒ごととか)にUIViewのdrawRectメソッドを実行することで行われるので、画面書き換え(ラベルのテキスト変更とかも)処理直後に[NSThread sleepForTimeInterval:2.0f];なんて呼ぶと更新される間もなく停止されちゃうのだね。

だから、
_label.text = @"hogehoge";
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05f]];
で画面更新のタイミング分コードの進行を待たせた後に
[NSThread sleepForTimeInterval:2.0f];
で処理を停止させてやればいいのだね。
めんどっちーけど。

なお、
[NSThread sleepForTimeInterval:2.0f];
を使った停止中でも、画面をいじるとその操作がバッファされてるようで、停止明けに実行されちゃったりするので、ボタンとかをイネーブルにするなど、別途対策を取られたし。

2015年3月20日金曜日

効果音を鳴らす

正解したら「ピンポ〜ン」、不正解なら「ブッ!」といった簡単なSEを鳴らしたい場合。
21世紀にもなったのに、やり方がちょっととっても面倒なのでまとめる。
なお、Swift 2.0もわかった点だけ後述。

鳴らすためのサウンドファイルはどっかから用意すること。GarageBandあたりで作ってもよし。

ファイルをXcodeにドラッグしてプロジェクトに登録するまでは画像なんかと一緒。
しかし次が重要。
ドラッグして追加した場合、Build PhasesのCopy Bundle Resourcesで、「+」ボタンを押してサウンドファイルを指定してやらないと、コード中でpathが掴めなくてエラーになってしまう。
ただし、ファイルメニューの「Add Files to "プロジェクト名"」で追加した場合は自動的にCopy Bundle Resourcesに追加されるので、不要のようだ。こっちの方が楽だな。

サウンドファイルを選んでAddを押す。

これでちゃんとサウンドファイルが認識されるようになる。

必要なFrameworkはAVFoundation.frameworkなので、インポートしとくこと。


ヘッダーファイルのinterface部でAVAudioPlayerDelegateの設定が必要。
@interface GameViewController : UIViewController <AVAudioPlayerDelegate>

実装ファイルの方でも工夫が必要。
サウンド用のインスタンス変数を用意。(後述)
NSMutableArray *soundArray; //SEのインスタンスを一時的にしまっておく
AVAudioPlayer *soundPlayer; //SE再生用

AVAudioPlayerってので再生するのだが、ローカル変数では、ARCで動かすせいで、メソッドを抜けるとすぐに変数が解放されてしまうため、音を鳴らすところまでいってくれないのだ。
ということでサウンドのインスタンスはインスタンス変数にしまって保持するのだが、一つのインスタンス変数だけでは、今度は前の音が終了するまで次の音が鳴らなくなってしまう。ギギギ…(-_-;)
そこで、NSMutableArrayに同時に鳴らす数だけのインスタンス変数をしまって保持するようにする。
そして音が鳴り終わった時に呼ばれるありがたいdelegateメソッドによって、不要なインスタンス変数をNSMutableArrayから消すのだ。

//サウンドの再生
- (void)playSoundWithResource:(NSString *)resource ofType:(NSString *)type{
    NSString *path = [[NSBundle mainBundle]pathForResource:resource ofType:type];
    
    soundPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL fileURLWithPath:path] error:nil];
    soundPlayer.delegate = self;
    soundPlayer.volume = 0.4;
    soundPlayer.numberOfLoops = 0; //連続再生回数。0で1回、1で2回…、-1で無限ループ
    [soundPlayer prepareToPlay]; //あらかじめメモリに展開するなど、すぐ再生できるよう準備
    [soundPlayer play];
    [soundArray insertObject:soundPlayer atIndex:0]; //ARCでメソッド終了後に消されないようにインスタンス変数の配列に格納
}

//鳴り終わったら呼ばれるdelegateメソッド
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    //鳴り終わったSEのインスタンスを消す
    [soundArray removeObject:player];
}

上記のplaySoundWithResource:〜メソッドは自作なので、実際に音を鳴らす際は、どっかから以下のように呼び出す。
[self playSoundWithResource:@"correct" ofType:@"m4a"];

いちいちめんどくせえだろ? 8ビットパソコンでBEEPとだけ書けば良かったものが、この手間の数。世の中間違っとるよね。Swiftではどうなのか知らんけど。


BGMを鳴らす場合

ゲームなんかでBGMを鳴らしっぱなしにする際は、
soundPlayer.numberOfLoops = -1;
にしてやりゃいい。

BGMをGarageBandで作ると無音部分が追加される

共有でiTunesに書き出して、そのファイルを使うことになるわけだけど、共有メニューからiTunesやディスクに書き出すと、曲の最後に1秒程度の無音部分が勝手に追加されてしまう。
おかげで、BGM2巡目に入る前に音が途切れることとなり甚だよろしくない。
音の切り貼りができるソフト(PRO版のQuickTime7 Playerとか)を使って残響音のところを消してしまえば問題はないが、めんどくさいし、QTPlayerだと波形が見られないからちょうどいいところで削除できなかったり、AIFFかWaveなどでしか保存できないみたい。

40秒という制限はあるが、GarageBandの共有から着信音として書き出すと、連続再生に向いた、無音部分のないものが書き出せる。

なお、Xcodeにとっかえひっかえ曲のファイルを入れて試していたら、新しく追加した無音部分なしファイルが認識されず、差し替えたはずの無音部分付きファイルが再生されることがあって、やっぱりダメなのかと迷ったことがあった。
プロダクトのクリーン、Xcode再起動で直ったが、こういうことはXcodeではよくあるね。

Swift 2.0の場合

・AVFoundation.frameworkのインポートは必要
import AVFoundation

・ヘッダー部分でのAVAudioPlayerDelegateの記述は必要
class ViewController: UIViewController, AVAudioPlayerDelegate {

・インスタンス変数を定義しておくのも必要
    var soundPlayer:AVAudioPlayer? //SE再生用

・Swift 2.0の仕様変更でエラー処理の仕方が変わったため、AVAudioPlayer使用時に
do 〜 catch内にtry文を書く必要がある。
よく知らんのだけど、try後に書いたメソッドでエラーが発生したら、catch後の処理を実行するのだね。
    func playSoundWithResource(resource:String, type:String) {
        let path = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(resource, ofType: type)!)
        do {
            soundPlayer = try AVAudioPlayer(contentsOfURL: path)
            soundPlayer!.delegate = self
            soundPlayer!.volume = 0.4
            soundPlayer!.numberOfLoops = 0 //0で1回再生
            soundPlayer!.prepareToPlay()
            soundPlayer!.play()
        } catch {
            print("sound再生でエラー発生")
        } 
    }

tryのところは以下でもいいみたい。
try soundPlayer = AVAudioPlayer(contentsOfURL: path)

まあ後はテキトーに調べて書こう。

2015年3月19日木曜日

オブジェクトのタッチ位置検出とドラッグ

単純に位置を得る場合
タッチイベントの検出は、以下のメソッドで。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    CGPoint dustPoint = [[touches anyObject]locationInView:self.view];
}

[touches anyObject]
はNSSetからUITouchのインスタンスを取り出している。よくわからんけど、そうなるの。
んで、
[[touches anyObject]locationInView:self.view];
によって、自分のviewの座標の位置に変換している。これもよくわからんけど、そうなるからいいの。
よって、dustPointを見ればタッチした位置がわかる。

touchesBegan:〜はタッチした瞬間の位置なので、
指を離した時、
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
タッチしたまま指を動かした時、
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
はそれぞれ別メソッドで。


オブジェクトをドラッグしたりする場合
複数のカスタムクラスによるUIオブジェクトの、タッチしたものだけドラッグしたりする場合は、delegate使う。
カスタムクラス側でdelegateの記述を書く。
例)
Dust.h
@protocol DustDelegate
- (void)dustTouchMoved:(Dust *)dust;
@end

@interface Dust : UIImageView
@property (weak) id delegate; //独自delegate用
@property (assign) CGPoint touchPoint; //位置
@end

Dust.m
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //タッチされた位置をpropertyに入れてdelegateされる
    _dustPoint = [[touches anyObject]locationInView:nil];

    if ([self.delegate respondsToSelector:@selector(dustTouchMoved:)]) {
        [self.delegate dustTouchMoved:self];
    }
}
プロパティのtouchPointにタッチ位置を入れておいてやれば、delegateで渡された先でも位置がわかる。
locationInView:nil
がnilになってることに注意。self.viewもしくはself(viewを持たない部品の場合)のままでは自分のUIの左上からの座標になってしまう。

ただし、nilを指定したまま画面の縦横を変えると(landscape、portrate)座標が左下からになってしまうという情報も? 自分で試してみたところならなかったみたいだけど、念のため注意。

参考サイト:Cocoaの日々

んで、delegate先に以下のメソッドを書いておけば、指の位置にズルズルとUIオブジェクトがくっついてくるぞ。
(カスタムクラスdustをallocした際に、dust.delegate = self; とやっとくのは当然)
- (void)dustTouchMoved:(Dust *)dust
{
    //property dustPointにタッチされた位置が入ってる
    dust.center = dust.touchPoint;
}

2015年3月17日火曜日

Objective-Cの多次元配列

Objective-Cでの2次元配列の使い方

配列に設定
2次元配列の場合
NSArray *dustName = 
          [NSArray arrayWithObjects:@[@"A", @"B", @"C"],
                                    @[@"D", @"E", @"F"], nil];
これで3×2の2次元配列ができる。[]がそれぞれ1次元。中のA, B, Cなどがそれぞれ2次元。
中身へのアクセスは、
NSLog(@"%@",dustName[0][1]); //結果は「B」
などとなる。
古い書き方で
NSLog(@"%@",[[dustName objectAtIndex:0] objectAtIndex:1]);
とも書けるが益はない。



3次元配列の場合
NSArray *dustName = 
      [NSArray arrayWithObjects:@[@[@"A", @"B"], @[@"C", @"D"]],
                                @[@[@"E", @"F"], @[@"G", @"H"]],nil];
これで2×2×2の3次元配列ができる。
[]がそれぞれ1次元。[]がそれぞれ2次元。中のA, Bなどがそれぞれ3次元。
中身へのアクセスは、
NSLog(@"%@",dustName[0][1][0]); //結果は「C」
NSLog(@"%@",dustName[0][1][1]); //結果は「D」
古い書き方では
NSLog(@"%@",[[[dustName objectAtIndex:0] objectAtIndex:1] objectAtIndex:1]);

このように多次元配列はできるけど、定義も使用もややこしいので、あまりデータ量が多くなるなら、カスタムクラスを使うなど別の方法を考えたほうがいいかも。

Delegateの仕組み

「委譲」などと訳されるObjective-Cのdelegate。(辞書で引くと「派遣」と出た)
正式な仕組みや意味的なものはともかく、何かのイベントが起こった時にそのメソッドが実行されるので、イベントハンドラーのようなものと考えてればいい。
書き方はわかったけど、根本的仕組みがよくわからんのでまとめ。

Objective-Cの場合

基本的使い方

ヘッダファイル.h のinterfaceのところで、○○Delegateと宣言してやらないといけないことが多い。
これは後で知ったんだけど、delegateメソッドを宣言してるプロトコルってやつなんだね。詳しくはプロトコルのところで。
例)
@interface DetailTableViewController : UITableViewController <MFMailComposeViewControllerDelegate, ADBannerViewDelegate, UITextFieldDelegate, UITextViewDelegate, UIActionSheetDelegate, CLLocationManagerDelegate, MKMapViewDelegate>

実装ファイル.m の中では、あらかじめ関連するクラスのdelegateプロパティにselfを指定する。(delegateメソッドの場所がその実装ファイル内にあるということ)
例)
    //returnキーでキーボードを隠すためのdelegate
    _titleTextField.delegate = self;
    _detailTextView.delegate = self;
    _mapView.delegate = self;
    
    //iAdのdelegate
    _iAdView.delegate = self;

同じ実装ファイル内にイベント発生時に呼ばれるdelegateメソッドを記述する。
メソッド名はそれぞれ決まっているので、どのイベントで何が呼ばれるかはあらかじめ調べておかないといけない。
例)
//textFieldでreturnが押されたらキーボードを隠すdelegateメソッド
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return YES;
}

//textView編集終了時、キーボードを隠し、元データを更新するdelegateメソッド
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
    _incidentData.text = _detailTextView.text; //元データ更新
    //ファイル保存
    [appDelegate writeFile];

    [textView endEditing:YES];
    return YES;
}

TableView関係のdelegateなどは、ヘッダファイルでの宣言、実装ファイルでのdelegateプロパティにselfの指定などは省略できるようだ。たぶんどこか見えないところでされてるんだとは思うが。
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.objects.count;
}

カスタムクラスでの独自Delegateの実装方法

DustというImageViewの独自クラスをタッチしたら、メインのGameViewControllerでラベルにメッセージを表示するコード。

Dustのヘッダファイルの記述

protocolの宣言と、delegateメソッド名の設定、delegateプロパティの設定をする。
(protocolを外部ファイルにしてある場合は、それを#importする)
//独自delegateの設定
@class Dust;
@protocol DustDelegate
- (void)dustTouched:(Dust *)dust;
@end

@interface Dust : UIImageView
@property (weak) id delegate; //独自delegate用
@end

Dustの実装ファイルのdelegateメソッドを発生させるメソッドに、GameViewController内にdelegateメソッドが実装されていたらそれを呼ぶコードを書く。
respondsToSelector:メソッドは、:@selector()で指定したメソッドが存在するかの判定。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //delegate先(GameViewController)にdustTouched:メソッドが実装されていれば、
    //自分(Dust)を引数にそいつを呼ぶ
    if ([self.delegate respondsToSelector:@selector(dustTouched:)]) {
        [self.delegate dustTouched:self];
    }
}

GameViewControllerのヘッダファイルでは特にdelegateに関する宣言なし。

実装ファイル内でDustのインスタンスを作る際にdelegateにself(GameViewController)を指定する。
    //Dustクラスを作る
    Dust *dust = [[Dust alloc]initWithImage:image];
    dust.delegate = self; //touch時に呼ばれるdelegateメソッドの場所の指示

実装ファイル内にdelegateメソッドを書く。以上。
//Dustがタッチされたら呼ばれるdelegate
- (void)dustTouched:(Dust *)dust
{
    _dustNameLabel.text = @"タッチされた";
}


仕組みの解説


Dust.hのprotocolの設定で、メソッドの引数に自分自身を指定していることと、プロパティで汎用のid型を使い、あらゆるクラスから呼ばれてもいいようにしているのが肝。

GameViewController.mで
dust.delegate = self;
としているので、dustのdelegateプロパティにはGameViewController自身が入る。(②)

GameViewController内でDustインスタンスが作られ、画面でタッチされると、Dust内のtouchesBegan:〜メソッドが動く。(③)
if ([self.delegate respondsToSelector:@selector(dustTouched:)]) {〜}
によるチェックで、delegateプロパティに入っているのはGameViewController自身なので、当然dustTouched:メソッドは存在することになる。(④)

そしてDust.mのtouchesBegan:〜メソッド中の
[self.delegate dustTouched:self];
において、self(Dust)のdelegateプロパティに入っているクラス(GameViewController)内のdustTouched:メソッドを実行させている。(⑤)
引数にself(Dust)を渡してあるので、dustTouched:メソッドの中でもDustのいろいろなプロパティを使用することができる。(ここでは記述してないが)

つまり、GameViewControllerを親、Dustを子とした場合、親を指定して子を呼び、子の中で親にdelegateメソッドがあるかどうかをチェックし、あれば親の中のそのdelegateメソッドを実行しているわけだ。

dust.delegate = self;
というコードが単に「delegateメソッドの場所の指示」というわけではなく、dustのプロパティdelegateにGameViewController自身(正確にはそのポインタ)を入れているわけだ。
いろいろややこしいが。

他のイベント時に別のdelegateメソッドを発生させたかったら、Dustクラス中の記述(ヘッダ、実行ファイルとも)と、GameViewController中の対応するdelegateメソッドを記述すればいいわけだ。


Swiftの場合

protocolを宣言し、delegate変数にselfを入れてメソッドを呼び出すとかはObjective-Cと一緒。
以下はSpriteNode Coinをタッチしたら親ノードに書かれたdelegateメソッドを呼び出す例。

//delegate用プロトコルの宣言
protocol CoinDelegate: class {
    //delegateメソッドの定義
    func touchCoinBegan(coin: Coin)
    func touchCoinEnded(coin: Coin)
}

class Coin: SKSpriteNode {
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        delegate?.touchCoinBegan(self)
    }
    
    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
        delegate?.touchCoinEnded(self)
    }
}

なお、コードの中で
delegate?.touchCoinBegan(self)
は、Objective-Cの
if ([self.delegate respondsToSelector:@selector(dustTouched:)]) {
        [self.delegate dustTouched:self];
    }
にあたる部分。
やってることはちょっと違うんだけどね。
SwiftにもrespondsToSelectorメソッドはあるんだけど、まだ俺が書き方がわかんないし、オプショナル「?」で、変数delegateがnilじゃないか調べ、nilじゃなければtouchCoinBeganメソッドを実行させる方が簡単でSwiftらしい書き方のようだ。
プロトコル実装してればdelegate先にそのメソッドがないことはあり得ないしね。


んで、delegateを呼ぶ親ノード側。
ちゃんと最初にCoinDelegateプロトコルを使う旨最初に宣言。
Objective-Cの時は、なんか書かなくても動いちゃったけど、Swiftだと書かないとダメみたい。
class GameScene: SKScene, CoinDelegate { //自作delegateのプロトコルを宣言
    var 開始時刻:CFTimeInterval = 0
    var 射出力:CGFloat = 0
    var coin:Coin = Coin()
    
    override func didMoveToView(view: SKView) {
        coin = Coin(imageNamed: "10yen")
        coin.userInteractionEnabled = true 
        coin.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMaxY(self.frame))
        coin.setScale(0.5)
        coin.physicsBody = SKPhysicsBody(circleOfRadius: coin.size.width / 2)
        coin.delegate = self //delegate先をselfに
        self.addChild(coin)
    }

    //こっからがプロトコルに定義されたdelegateメソッド
    func touchCoinBegan(coin: Coin) {
        開始時刻 = CFAbsoluteTimeGetCurrent() //現在の時刻を得る(2001年1月1日からの経過秒数)
        射出力 = 0
    }
    
    func touchCoinEnded(coin: Coin) {
        let 経過時間:CGFloat = CGFloat(CFAbsoluteTimeGetCurrent() - 開始時刻)
        coin.physicsBody?.applyForce(CGVectorMake(経過時間, 0))
    }
}

2015年3月12日木曜日

UIViewのAnimationについて

UIViewは簡単にアニメーションの設定ができる

座標を移動させる場合はコードの中で終点の座標を指定してやるだけで、現在位置から終点までの移動をアニメーションしてくれる。

UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut;
        [UIView animateWithDuration:2.0 //アニメーション時間
                              delay:0.0 //アニメーション開始までの待ち時間
                            options:options //オプション
                         animations:^{ //この中でアニメーションの内容を設定する
                             CGPoint terminatePoint = CGPointMake(dust.dustTerminateX, CGRectGetMaxY(_houseImageView.frame));
                             human.center = terminatePoint; //終点座標指定
                             dust.center = terminatePoint; //終点座標指定
                         } completion:^(BOOL finished) { //アニメーション終了後の処理
                             isDustMoving = NO;
                         }];

指定できるoptionsの一部。
単独のbitになっているものがあるので、OR(|)を使って複数指定することができる。
他にもいろいろある。
  • UIViewAnimationOptionCurveLinear
    • 同じ速度でアニメーションする
  • UIViewAnimationOptionCurveEaseIn
    • 移動がゆっくり始まる
  • UIViewAnimationOptionCurveEaseOut
    • 移動がゆっくり終わる
  • UIViewAnimationOptionCurveEaseInOut
    • 移動がゆっくり始まり、ゆっくり終わる
  • UIViewAnimationOptionAutoreverse
    • 終点まで行ったら始点まで戻る
  • UIViewAnimationOptionRepeat 
    • 同じアニメーションを繰り返す
  • UIViewAnimationOptionBeginFromCurrentState
    • すでに動いているアニメーションに現在の設定をつなげてアニメーションを開始。よくわかんないけど、単独のアニメなら無視される。

2015年3月10日火曜日

XcodeでAppleWatch用プロジェクトを作る


普通にプロジェクト作った後に、File/New/Target.../Apple Watchを選び、WatchKit Appを選んでNextすると関連Frameworkがインポートされる。

「アプリ名 WatchKit App」フォルダ中のInterface.storyboardがAppleWatch用のStoryboard。
38mmと42mmとAnyのScreenSizeが選択できる。38と42mmでは解像度が違うので、Anyで吸収できない場合はそれぞれレイアウトの修正が必要なようだ。
Constraintsの設定がないようなので、AutoLayoutは使えない模様。
Static Interface、Dynamic InterfaceというStoryboardは何をしてるのかは不明。最初から出てくるくらいなので、よく使われるのかな?
AppleWatch側のコードは「アプリ名 WatchKit Extension」フォルダ中のInterfaceController.h、同.mに書く。
動作確認はiPhone5以降のシミュレータで。(対象外の機種は選択肢に出てこないし、iPad実機も使えない)
AppleWatchは基本的にはiPhoneとペアで使うので、iPhoneの画面とAppleWatchの画面の両方が表示される。
うまく表示されない場合は、シミュレータメニューのHardware/External DisplaysでAppleWatchのサイズを変更してみるといいかも。

しかし、最高200万円するってのはすげえな。いくらゴールド素材を使ったからって、原価は50万もいかんだろう。高級感を出して利益率を上げる作戦と思う。
一生ものの時計と違って数年で陳腐化するIT商品に、そこまで金を出す人がどれだけいるかという気がするが。