新アプリ IconTimer をリリースしました。決めた時間をカウントダウンしてアラームを鳴らすだけのシンプルなタイマーアプリです。見た目にも音にもこだわりました。
特徴的なのは時間の選び方。
タップしたボタンの数字を足した合計が計測時間(分)になります。
以下は紹介動画。
時間・アイコン・色を自由にカスタマイズ。
以下は開発メモをつらつらと書きます。
ユーザインターフェイスめも
既存のタイマーアプリを使っていて毎回不満に感じていたのは、時間を選ぶ時に頭の中で「ひっかかり」があるところ。うまく説明できないのだけれど、あえて言えば時間を選ぶ時に注意を払わせるのが苦痛だった(どんだけものぐさと言われそうですが。。)
アプリを使うのがちっとも楽しくない。
例えば電卓式の時間を直接入力するタイプの場合、目的の数字を押してスタートするだけ....なんだけど打ち間違えたらクリアしないといけない。だから無意識のうちに間違えないように注意を払う。それがひっかかる。一方あらかじめ時間がリストで選べるタイプの場合、たくさんの選択肢から目的の時間を注意を払って探さなければならない。これもひっかかる。
そんな気を遣わずにポン・ポン・ポンと選ぶだけで時間が選べる UIはできないのか?それが開発の出発点だった。注意を払わ無くて良い UIって、きっと失敗しても直すのが苦にならないものだろう。試行錯誤がしやすいUI はどんなものか。
最初イメージとしてあったのは、少ないボタンを配置してそこから時間を選んでスタートするデザイン。どうせよく使う時間は決まっているだろうから、あらかじめ好きな時間を割り当てておけばボタンは少なくていいだろう。数が少ないのでひと目で把握できて判断もしやすそうだ。ただ問題なのはボタンが少ないと時間のパターンが物足りないこと。さらに使う前に時間を決めて設定するのはいちいち面倒。もしパターンを多くしようとボタンを増やせば選択方式と同じ罠に陥る。
そんなことを思いつつプロトタイプをひたすら弄ってた。ふとボタンを排他的に扱うではなく「足し算」にしたらどうかと思いつく。タップした時にONだった他のボタンをOFFにするのではなくそのまま残す。その残ったONボタンの数字の合計がタイマーの時間とする。逆にもう一度押すとOFFになり、その分タイマーの時間が減る。つまりタップのON・OFFがちょうど足し算・引き算の意味を持つことになる。これなら間違っても何度でもやり直せる。そして何よりもこの方法だとボタンの数以上の時間パターンを作り出すことができるのに気がついた。
でも、タップ数は他の方式に比べると増えてしまう。これって最初の問題に逆戻りしないだろうか?ところがプロトタイプで試してみると意外にもこれが楽しい。妙な話だけど、3分のタイマーを作るのに1分のボタンをわざわざ3回押したくなる。むしろタップが増えて非効率になってるし。。でもついつい目的の時間を作るのに今あるボタンだけで何を足し引きすれば良いのかと考えてしまう。そうか、タップのON・OFF行為がちょっとしたパズルを解く感覚なんだと気がついた。さらにアイコンを表示して音を鳴らすとこの傾向が顕著になった。意味もなくついついボタンを弄ってしまう。これで行くことに決めた。
開発情報
ツール:Xcode7 / swift 2.0期間:1ヶ月
要員:1人
アイディアを元にプロトタイプから作り始めた。それを iPhoneに入れて暇があれば弄って改良を加える漸進的なアプローチ。プログラムもデザインも一人でやるのでお気楽。
アイコン
アイコンはいつも使っている icons8 のカラーアイコンを使用。自分の開発ではもはや定番。デザインが良くて種類も豊富だし、SVGもサポートしているなど使い勝手がいい。100pxまでのPNGはフリー。$149 でPNGのすべてのサイズと1年間の更新権。$249ならSVG / PDF / EPS / AI 形式のほかフォント/SVGの生成、カラーリング等々さまざまなオプションが付く。
効果音
ライブラリ
Crashlytics, AdMob のみ。その他 UI / 機能に関しては自前。UIは色々なパターンを試作して捨てたものも多数。その分作るのは時間がかかったが楽しい作業でもあった。
多言語対応(アプリ)
開発当初から意識していた。アプリの性格上、文字での説明は要らないと割りきって文字の埋め込みをやめて、その分 UI を誰が見ても理解できて迷わないものにすることを心がけた。これによって翻訳の手間がほぼゼロとなった。一人開発でこれは嬉しい。対応言語はXcodeで選択可能なものをかたっぱしからローカライズした。なお唯一文字があるクレジットは英語表記のみとしてどの言語も統一した。
(1) 無駄無く簡潔で短い最低限の文面を日本語で作成
(2) それをGoogle翻訳の助けを借りて英語へ翻訳
(3) Google Sheetを使って英語から他の言語へ一気に翻訳
多言語展開の元を英語にしたのは日本語起点よりも精度が良さそうだから。
Google Sheet(Google Driveで提供されるオンラインのスプレッドシートアプリ)にはセルの文字列を Google翻訳サービスへ渡す関数が用意されていて、これを使うと大量の文字列を一気に翻訳することができる。こんな感じ。
多言語対応(AppStore)
多言語といえばアプリとは別に AppStore向けの説明がある。こればっかりは翻訳をやらないわけには行かないので各言語用に文面を作った。アプローチは、(1) 無駄無く簡潔で短い最低限の文面を日本語で作成
(2) それをGoogle翻訳の助けを借りて英語へ翻訳
(3) Google Sheetを使って英語から他の言語へ一気に翻訳
多言語展開の元を英語にしたのは日本語起点よりも精度が良さそうだから。
Google Sheet(Google Driveで提供されるオンラインのスプレッドシートアプリ)にはセルの文字列を Google翻訳サービスへ渡す関数が用意されていて、これを使うと大量の文字列を一気に翻訳することができる。こんな感じ。
あらかじめ言語コード(jaとかen)の列を用意しておき、それを関数へ渡して翻訳をかける。
日本語を含む23言語の翻訳がこの方法であっという間にできた。Googleすごい。
Apple審査
12/04 Ver1.0を申請
12/12 リジェクト:1.0 メタデータでの却下
AppStore用のアプリ名に長い説明を入れたのがまずかったらしい。説明はやめて IconTimer だけにして再申請。バイナリは更新せず。
12/18 リジェクト:1.0 バイナリでの却下
設定画面に分を増減させる+−のボタンがあるのだが、それを押している時にクラッシュするのでダメというもの。自分の手持ちの iPhoneやiPadやシミュレータを総動員して試すも再現しない。再現しない上に、メイン機能ならともかく設定の一部の機能のクラッシュだけで落とすな=3、となんだか頭に来て「再現しない」「もっと詳しい情報を出して」的なコメントを送る。
が、その後繰り返しボタンを押し続けるとクラッシュが再現。簡単には起きないが、確かに使っていると誰でもいつかはクラッシュに遭遇しそう。クラッシュログはこんな感じ。
が、その後繰り返しボタンを押し続けるとクラッシュが再現。簡単には起きないが、確かに使っていると誰でもいつかはクラッシュに遭遇しそう。クラッシュログはこんな感じ。
Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Subtype: KERN_INVALID_ADDRESS at 0x00000000155d3f40 Triggered by Thread: 0
Oh...わからん。
その後、Xcodeに繋いだiPhoneで問題を再現させるも AppDelegateで停止して上記例外が出てるだけ。調査を重ねるもさっぱりわからない。仕方なくボタンを連打しつつ(それが唯一の条件なので)、一つ一つコードから行を取り除いてクラッシュに関係のある箇所を絞り込んでいった。泣きが入りそうになったころ、ボタンを押した時に鳴らす音の処理で落ちていることが判明。音を鳴らすために Soundというクラスを作り、AVAudioPlayerのインスタンスをクラス変数で管理していたのだが、どうもそこがクラッシュの原因ぽい。こんな感じ。
class Sound: NSObject { static var player:AVAudioPlayer? static func play(...) { if let sound = NSDataAsset(name: name) { do { : try player = AVAudioPlayer(data: sound.data, fileTypeHint: fileTypeHint)
※元ネタはネット上のブログから拾ってきた。いま手元に情報が無いので略
ボタンが続けて押された場合、前の発音が終わらない内にこの play()メソッドが呼び出され、新しい AVAudioPlayerのインスタンスが作られる。この時、直前の発音で使っていた AVAudioPlayer は音が鳴っている最中に参照が切れて廃棄対象になってしまうケースがあるようだ。そして音が鳴り終わった時にこの廃棄対象のインスタンスで後処理を行おうとしてクラッシュ(たぶん)。そこで次の音を鳴らす時には直前の発音を明示的に停止する処理を入れた。
if文いらないかも。ともかくこれでクラッシュがピタッと無くなった。速攻でバイナリをアップロードして再申請。頭に来て送ったコメントを思い出すとなんだかバツが悪かったので、クラッシュの指摘の(最大限の)お礼と、ついでに急いで審査お願いと付け足しておく。その後なぜか expedited review はできないよ、という連絡メールが来る(expedited reviewは依頼していない)。
12/22 審査通過
日本時間で 0時ごろレビュー開始、6時すぎに通過の連絡。これは嬉しかった。
(おわり)
ボタンが続けて押された場合、前の発音が終わらない内にこの play()メソッドが呼び出され、新しい AVAudioPlayerのインスタンスが作られる。この時、直前の発音で使っていた AVAudioPlayer は音が鳴っている最中に参照が切れて廃棄対象になってしまうケースがあるようだ。そして音が鳴り終わった時にこの廃棄対象のインスタンスで後処理を行おうとしてクラッシュ(たぶん)。そこで次の音を鳴らす時には直前の発音を明示的に停止する処理を入れた。
class Sound: NSObject { static var player:AVAudioPlayer? static func play(...) { if let _ = player { player?.stop() } if let sound = NSDataAsset(name: name) { do { : try player = AVAudioPlayer(data: sound.data, fileTypeHint: fileTypeHint)
if文いらないかも。ともかくこれでクラッシュがピタッと無くなった。速攻でバイナリをアップロードして再申請。頭に来て送ったコメントを思い出すとなんだかバツが悪かったので、クラッシュの指摘の(最大限の)お礼と、ついでに急いで審査お願いと付け足しておく。その後なぜか expedited review はできないよ、という連絡メールが来る(expedited reviewは依頼していない)。
12/22 審査通過
日本時間で 0時ごろレビュー開始、6時すぎに通過の連絡。これは嬉しかった。
(おわり)