beryulog

iOSアプリ/サーバーサイド開発者のメモと雑記

ミニ四駆用ラップタイマーアプリを作りたい Part.1

ここ半年ほど、ミニ四駆専用のブログを開設するレベルミニ四駆にどっぷりハマっています。
楽しいと思っているポイントはいくつかあるんですが、一番楽しいのは何と言っても各コースに合わせてセッティングを試行錯誤し、より速いタイムを叩き出すことだと思います。
そこでiOS向けの丁度良いラップタイマーアプリを探してるんですが、これがなかなか目的に合うものが見つかりません。。

f:id:beryu:20161031182943j:plain

そこで、この自分の欲しいラップタイマーアプリを作ってみることにしました。
はてなブログにたまたま

今週のお題「2017年にやりたいこと」

というお題が挙がっていたので、これに乗っかって計画段階から記事を書いてみます。

既存のラップタイマーアプリで満たせない要件

実は、ミニ四駆用のラップタイマーアプリは既に存在します(例:M4 LAPTIMER)。
しかし、それらのアプリでは私の要件が満たせなかったのです。

私の自宅にはコースを置けるほどの空間や環境が無いので、ミニ四駆コースの設置してあるミニ四駆ステーションに走らせに行くことがほとんどです。
特にタミヤ プラモデルファクトリー 新橋店にはよく行くんですが、ここにはラップタイマーが設置されておらず、かつ来客数もかなり多いのでたった1つのコースで大勢の方が同時にミニ四駆を走らせています。

ここで何が困るかというと、自分のマシンを走らせている間に他のマシンがゴールラインを通過してしまうので、ラップタイムが計測出来ないのです。
これはアプリだからNGというわけではなく、市販のラップタイマーを使っても同様の問題が起きます。

これから作るアプリの要件

以上を踏まえて、作りたいアプリの要件を整理すると下記の2点になりそうです。

  1. ラップタイムを計測する
  2. 私のマシンを他人のマシンと区別する

作っていくなかで様々な追加要件が出てくるとは思いますが、まずは上記2点を要件とします。

要件1.「ラップタイムを計測する」の実現方法

iPhoneのカメラを使って、コースを通過するマシンを検知して時間を計測する方法になりそうです。
具体的には、「コースだけの画像」と「コースにマシンが通過しているときの画像」の差分を取って、差分が閾値以上になったら「マシンが通過した」と判定すれば良さそうです。
ただ、多くのミニ四駆コースは室内なので、蛍光灯が少し古かったりするとカメラに映る画像の明るさが連続的に変化する傾向がありそうなので、ここの調整工程はかなりTry&Errorを繰り返す事になりそう。。

要件2.「私のマシンを他人のマシンと区別する」の実現方法

各自が作るミニ四駆をそれぞれ区別する方法としてまず思いつくのは「色」だと思います。
実際、私がコースを走らせている自分のマシンを区別しようとしても早すぎて輪郭が分からないので、区別するときは色を頼りにすることが多い気がします。
ただ、シールや複雑な塗装で1台のマシンに様々な色が配色されている場合もあるので、平均色(※)を求める方がいいのかなと(今は)思っています。
(※)平均色…複数の色を平均した「色の傾向」を単一の色として扱ったもの

ただ、走っているマシンの平均色を取る、なんて思い切ったことがスマホの性能で出来るのかは未知数です。
(もし無理だったら別の区別方法を考えなきゃな…)

直近やること

まずは、要件1「ラップタイムを計測する」を実現するためにカメラの画像を解析してミニ四駆ゴールラインを超えたことを検知する実装方法を調査するところから始めます。
恐らく背景差分法というアルゴリズムを使って実装することになりそうですが、何も考えずに実装すると性能的にかなり無理が出そうなので、iOSアプリで効率よく実装する方法を調べないといけません。。

不透明度が100%未満のView同士の合成について混乱したので少し深く考えてみた

つい最近、alphaが1未満(=100%未満)のView同士を重ねて描画する実装が必要になったんですが、ふと悩んでしまいました。

alpha=0.1 のViewを2つ重ねたら alpha=0.2 になるのか?

結論、なりませんでした。
しかし、恥ずかしながら何故そうならないのかピンときませんでした。

実際にやってみた

このアプリは、画面の上部に alpha=0.8 のViewを1枚、画面の下部に alpha=0.4 のViewを2枚配置しただけのアプリです。
構造は下記の図のようになっています。 f:id:beryu:20160714005706p:plain

見て分かるように、 alpha=0.4 のViewを2枚重ねただけでは alpha=0.8 のViewと同じ色にはなっていませんでした。

では、合成した色はどうやって計算されているのか

f:id:beryu:20160714012241p:plain ※不気味な図になったことをお詫びします

Viewのalpha(=不透明度)に1未満を設定した時、そのViewは 1-alpha 分だけ背景の色を透過するということになります。
alphaが1未満のViewが2枚重なっていると、上記の図のように背景が透ける量が乗算する形で減っていきます。
これが、単純に合成したViewのalpha値を足し算したalpha値を持つViewと同じ色にならない理由でした。

では、合成して同じ色になるalphaはどうやって求めるのか

f:id:beryu:20160714013221p:plain
再現したいalpha値と、合成する片方のViewのalpha値が決まれば、もう片方のViewのalpha値は次の式で求める事ができます。
例えば、再現したいalpha値=0.8 、合成する片方のViewのalpha値=0.4 だった場合の計算式は下記の通りです。
ソースコードではなく、数学の式です)

0.8 = 0.4 + (1 - 0.4) * x
...
x = 0.66666....

実際にやってみると、 alpha=0.4 のViewに alpha=0.67 のViewを合成すれば alpha=0.8 のViewとだいたい同じ色に見えるようになりました。

知ってる人からすると常識的な内容だとは思うのですが、恥ずかしながら少し悩んでしまったので、記事にしてみました。
どなたかの助けになれば幸いです。

GIFアニメはPhotoshopで書き出すと綺麗で軽い

先日、CustomizableActionSheetというライブラリのスクリーンショットPNG画像からGIFアニメに差し替えました。
GIFアニメ書き出しに使うツールとして「ffmpeg」と「Photoshop」を試したんですが、Photoshopの方が圧倒的に綺麗・かつ軽く仕上がったのでメモっておきます。

元動画はQuickTimeの画面録画機能でiPhone Simulatorのウィンドウを録画したmovファイルです。

Photoshop CC 2015

Photoshop CC 2015でmovファイルを読み込んだ後、[ファイル] -> [書き出し] -> [WEB 用に保存] -> [GIF 128 ディザ]で書きだしたもの。
色の再現度は申し分なく、動きも滑らかです。
ファイル容量は918KB。

f:id:beryu:20160503234033g:plain

ffmpeg

コマンドラインffmpeg -i {movファイルのパス} -vf scale=372:664 -r 60 {書き出し先のパス} を実行して書きだしたもの。 なんだかザラザラしていて、再生速度も何故か遅い。。
右端に変な黒い帯入っちゃってるし。
ファイル容量は5.7MB。

f:id:beryu:20160503234859g:plain

結論

Photoshopで書きだしたものはGIFのカラールックアップテーブルが最適化されている感じで、iPhone Simulatorの色が忠実に再現されているように見えました。
一方のffmpegは、決め打ちのカラールックアップテーブルから最も近い色を選んで書き出してる感じなのかな?

ffmpegにもうちょっと綺麗に書き出せるオプションが有るのかもしれませんが、しばらくはGIFアニメをPhotoshop CC 2015で書き出すことになりそうです。

Realm0.87.0で増えたRLMResultsクラスの使い方

Realmのバージョンアップ頻度高いなー。。

ソース:Realm Cocoa 0.87.0 available now

RLMArray has been split into two classes: RLMArray and RLMResults.

これまであったRLMArrayクラスが、RLMArray RLMResultsの2クラスに分割されたらしいです。 じゃあいったいどう使い分ければ良いのかというと…

RLMArray is now used only for to-many properties on RLMObject classes, while RLMResults is used for all of the querying and sorting methods.

RLMArrayクラスはRLMObject(=Modelクラス)にしか使わず、その他のクエリ結果やソート用途には全てRLMResultsに使う、という翻訳で良いのかな。

Realm0.86.0でRLMObjectのサブクラスのサブクラスも扱えるようになった(※罠あり)

ソース:Realm Cocoa 0.86.0 available now

You can now inherit from RLMObject subclasses.

とのことで、今までRLMObjectクラスを継承したModelしか扱えなかったのがやっと改善されたみたいです。 ただ、罠もあって…

but you cannot use an object of a subclass in a relationship where the parent is expected.

スーパークラス型のプロパティにはサブクラスのインスタンスを設定できない仕様なので、結局各プロパティに保存できるのは単一のクラス型のインスタンスのみ。

個人的には、これだとせっかくRLMObjectのサブクラスのサブクラスを扱えるようになっても出来ることはそんなに拡がらない気がしたので残念です…。

Kitematicの起動トラブル時にMac OSをアップデートしたら直った

起きた問題

f:id:beryu:20140927185026p:plain

Mac OS X 10.9.4を使っていたんですが、Kitematicのバージョン0.1から0.23に上げた時、上記スクショの"Adding custom host adapter to the Kitematic VM"または""Start the Kitematic VM"の箇所で必ず失敗するようになってしまいました。

対処法

ダメ元でMac OS X 10.9.5にアップデートしたら治りました。 なんだったんだろう…。

FXFormsの背景色や書体をカスタマイズする

iOSネイティブアプリ内で入力フォームを作るためにFXFormsを使ったんですが、そのカスタマイズに思いの外手間取ったので取り急ぎ採った方法をメモ。

FXFormControllerをオーバーライド

FXFormsは入力欄をUITableCellで表現しているので、tableのdelegate先になっているFXFormControllerのセル生成メソッドをオーバーライドしました。 多分正攻法ではないので、正しい方法をご存知の方がいたら教えて下さい…m(_ _)m

@interface MYFormController : FXFormController

@end

@implementation MYFormController

/**
 * FXFormTextFieldCellの代わりにMYFormTextFieldCellを使うように初期化処理変更
 */
- (instancetype)init
{
    if ((self = [super init]))
    {
        self.cellClassesForFieldTypes = [@{FXFormFieldTypeDefault: [FXFormBaseCell class],
                                       FXFormFieldTypeText: [MYFormTextFieldCell class],
                                       FXFormFieldTypeLongText: [FXFormTextViewCell class],
                                       FXFormFieldTypeURL: [MYFormTextFieldCell class],
                                       FXFormFieldTypeEmail: [MYFormTextFieldCell class],
                                       FXFormFieldTypePassword: [MYFormTextFieldCell class],
                                       FXFormFieldTypeNumber: [MYFormTextFieldCell class],
                                       FXFormFieldTypeFloat: [MYFormTextFieldCell class],
                                       FXFormFieldTypeInteger: [MYFormTextFieldCell class],
                                       FXFormFieldTypeBoolean: [FXFormSwitchCell class],
                                       FXFormFieldTypeDate: [FXFormDatePickerCell class],
                                       FXFormFieldTypeTime: [FXFormDatePickerCell class],
                                       FXFormFieldTypeDateTime: [FXFormDatePickerCell class],
                                       FXFormFieldTypeImage: [FXFormImagePickerCell class]} mutableCopy];
    }
    return self;
}

/**
 * セルの見た目調節
 */
- (UITableViewCell *)tableView:(__unused UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
    
    // 背景色変更
    [cell setBackgroundColor:[UIColor blackColor]];
    
    return cell;
}

@end

FXFormTextFieldCellをオーバーライド

入力欄にフォーカスした時、キーボードの上に閉じるボタンが欲しかったので、入力欄を管理しているFXFormTextFieldCellをオーバーライドしました。 また、標準だとUIReturnKeyNextとUIReturnKeyDoneしか対応していなかったので、UIReturnKeySendにも対応できるように拡張しました。

@interface MYFormTextFieldCell : FXFormTextFieldCell

@end

@implementation MYFormTextFieldCell

- (void)setUp
{
    [super setUp];

    // キーボードに閉じるボタンを追加
    UIView *keyboardActionView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, screenSize.width, 40)];
    [keyboardActionView setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:1]];
    UIButton *btnClose = [UIButton buttonWithType:UIButtonTypeSystem];
    [btnClose setFrame:CGRectMake(10, 10, 100, 20)];
    [btnClose setTitle:@"閉じる" forState:UIControlStateNormal];
    [btnClose setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [btnClose addTarget:self
                 action:@selector(btnCloseClickHandler:)
       forControlEvents:UIControlEventTouchUpInside];
    [keyboardActionView addSubview:btnClose];
    [self.textField setInputAccessoryView:keyboardActionView];
}

/**
 * キーボードのキャンセルボタンのイベントハンドラ
 */
- (void)btnCloseClickHandler:(UIButton *)sender
{
    // キーボードを隠す
    [self.textField resignFirstResponder];
}

/**
 * キーボードのエンターキーのイベントハンドラ
 */
- (BOOL)textFieldShouldReturn:(__unused UITextField *)textField
{
    if (self.textField.returnKeyType == UIReturnKeyNext)
    {
        // Nextボタン→次の入力項目へフォーカス移動
        [[self nextCell] becomeFirstResponder];
    } else if (self.textField.returnKeyType == UIReturnKeySend) {
        // Sendボタン→フォーカスを外す & 今表示中のViewControllerの送信メソッドを呼ぶ
        [self.textField resignFirstResponder];
        AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
        UINavigationController *navigationController = (UINavigationController *)delegate.window.rootViewController;
        id<MYFormSendProtocol> vc = (id<MYFormSendProtocol>)[navigationController topViewController];
        [vc submitForm];
    } else {
        // それ以外→フォーカスを外す
        [self.textField resignFirstResponder];
    }
    return NO;
}

- (void)update
{
    [super update];

    [self.textField setTextColor:[UIColor whiteColor]];
    self.textField.attributedPlaceholder =
        [[NSAttributedString alloc] initWithString:[self.field.placeholder fieldDescription]
                                        attributes:@{ NSForegroundColorAttributeName:[UIColor whiteColor] }];
}

@end

これでなんとかやりたい事が実現できました。 ゴリ押し感がやばい。。