My365の作り方 〜iOS編〜

My365は、まずiPhoneアプリとしてリリースすることにしました。
iPhoneアプリをリリースしたのはこれが初めてで、勉強しながらの開発となりました。(iPhoneアプリ開発自体では、ヒマボタンという素晴らしいアプリの開発経験があるのですが、このアプリは残念ながら僕のiPodにしか入っていません。)
(よかったらこちらもどうぞ → iOS編, サーバ編, サーバサイドプログラム編 )

勉強に使った書籍



「よくわかるiPhoneアプリ開発の教科書」は、Objective-Cの言語体系についてはほとんど触れられていませんが、Xcodeの使い方であるとか、iOSのフレームワークの使い方であるとかが、とても丁寧に説明されていて分かりやすい本です。
4〜5時間くらいで読み終わります。この本だけでとりあえずアプリが作れる風になれることは間違いないです。

「Objective‐C 2.0徹底解説」は、iOSだけでなくMac OSも含めたObjective-C開発全般を対象として、Objective-Cの体系をとても詳しく書いてあります。「よくわかるiPhoneアプリ開発の教科書」では言語体系の部分が不足なので、こちらの本で補います。補うというと失礼なくらい詳しく書いてあります。
注意すべきは、Mac OSも対象にしているので、全てのプログラムがiPhoneでも動くとは限らないということです。ガベージコレクションはiPhoneにはありませんし、一部フレームワークもMac OS限定の場合があります。

プログラムの動かし方

My365は、ブラウザアプリとほとんど同じような感じで、サーバとHTTPで通信しています。iPhoneのビューが開くタイミングなどで、サーバにHTTPリクエストを送ると、JSON形式でデータが返ってくるので、それを展開して画面を表示していきます。

ASIHTTPRequest

HTTPで通信をするときに、同期通信と非同期通信の2種が選択できます。同期通信はプログラムが1本道になるので非常に楽なのですが、通信中に画面の操作ができなくなってしまうのがストレスになります。一方の非同期通信は、通信中にも操作ができるので通信を途中でキャンセルできますし、通信中に他の画面を見たりすることができます。その分プログラムは複雑になりがちです。
ユーザビリティを考えて、同期でなければならない一部以外は全て非同期の通信で実装しました。実装の面倒くささを緩和するために、ASIHTTPRequestというライブラリを利用しました。

- (ASIHTTPRequest *)getAPI:(NSString *)api delegate:(id)delegate finished:(SEL)finished failed:(SEL)failed;

更にそれをラップするメソッドを作って、通信が成功した時と、失敗した時で処理を分けています。

json-framework

レスポンスはJSONでただのNSStringの状態なので、NSDictionaryなどに展開して使います。この時には、json-frameworkを利用しています。

NSString *response = [request responseString];
NSDictionary *dict = [response JSONValue];

ご覧のとおり、使うのは非常に簡単です。

レスポンスの内容

iPhoneアプリはユーザーがアップデートをしてくれなければクライアント側が変えられないというのが、最も頭を悩ませるところです。サーバ側をアップデートしたらクライアント側が動かなくなったということにならないように、できるだけ拡張性の高い形式で返したい。ということで、結局次のような形になりました。

{
  "meta":{
    "api":"popular"
  },
  "error":{
    "code":200,
    "message":"OK"
  },
  "response":[
    {
      "id":123456,
      "text":"sunny day",
      "created":"2011-11-10 00:00:00",
      "photo":"http://my365.in/image/thumbnail/123_12345"
    },
    ...(略)...
  ],
  "notification":null
}

開発当初は、配列が返ったり連想配列が返ったりバラバラだったのですが、最終的には全て連想配列で返すようにしました。配列で返してしまうと追加で情報が必要になった時に追加できないためです。連想配列であれば、新しいキーを追加しても、古いバージョンではそのキーが無視されるだけです。

リクエストの内容

順番が前後しますが、リクエストの内容には必ずふたつの情報を含めることにしました。

バージョン

1つ目はアプリのバージョンです。アップデートの際に大幅な変更が不可欠な場合が出るかもしれません。アプリがどのバージョンであるかが分かれば、それに応じて旧バージョンのレスポンスを変えずに、新バージョンのための新しいレスポンスを返すことができます。

NSString *version =  [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];

言語

もう1つは、アプリを使用している言語です。上のレスポンスの内容を見ると、エラーが発生した時にはエラーメッセージを返すようになっています。My365は世界中で広く使われるようにしたかったので、徹底的に多言語化しようと思っていました。そういうわけで、エラーメッセージが「誰に対しても英語」というのは避けたいところでした。
エラーコードだけ取得してアプリ側でメッセージを生成することもできますが、機能追加で新しいエラーが生じる可能性が生まれた時に対応できません。そこで、アプリで使用している言語をサーバに送信して、その言語に応じてサーバ側からエラーメッセージを返すことにしました。
言語の取得方法は、いまいちスマートじゃないですが、localizable.stringsの中にLanguageという項を作って、英語には”en”を、日本語には”ja”を指定しています。あとはこれをNSLocalizedStringで取得すれば使用している言語が分かります。

NSString *language = NSLocalizedString(@"Language", nil);

写真フィルタ

写真フィルタのあたりはかなり時間をかけた部分です。各フィルタが写真に対してどのような処理を施すかという情報は、初期状態ではアプリ内に持っておらず、サーバ側で管理しています。これによって、リリース後にフィルタの微調整が可能だったり、フィルタの追加が可能になっています。
こういう機構にしたために、処理の内容を絞り込まざるを得なくなりましたが、以下のような機能の組み合わせでフィルタを実現しています。

RGBカラーフィルタ

ピクセルごとに色を変換していくフィルタです。今現在は、これがフィルタの機能の9割を担っています。
R’ = (RR曲線)R + (GR曲線)G + (BR曲線)B
G’ = (RG曲線)R + (GG曲線)G + (BG曲線)B
B’ = (RB曲線)R + (GB曲線)G + (BB曲線)B
色の入力を横軸、出力を縦軸にとって考えいます。色の値は8bitで0〜255です。色を変化させない場合は、(0,0)から(255,255)までの直線です。色を明るくする場合はこの直線を上に凸になるように歪ませます。色を暗くする場合は逆です。曲線によって、コントラストを上げたり下げたりもできます。
こういう曲線を9種類、3×3行列のような形で作っておけば、非常に多様なカラーフィルタが表現できます。

ぼかしフィルタ

ぼかしは、あるピクセルの付近の色の平均をとるだけです。どのくらいの範囲の平均をとるかだけを指定できるようにしています。
ぼかしをかけるのは、iPhoneのパワーではそれなりに時間がかかります。ピクセルごとに周囲の平均をとるということで想像できると思いますが。

for (int j = 0; j < height; j++) {
  for (int i = 0; i < width; i++) {
    for (int n = -2; n < 2; n++) {
      for (int m = -2; m < 2; m++) {
        // ピクセルのRGBそれぞれの足し算
      }
    }
  }
}

My365の写真は600×600のサイズなので、周囲5×5の画素で平均を取っていくとすると、足し算処理は27,000,000回必要です。実際にやってみると、5秒以上かかりました。
600×600×5×5×3 = 27,000,000回
そこで、
高速ぼかしフィルタ - iPhoneアプリレビュー
を参考に計算回数の少ない方法を採用しました。こちらで紹介されているのは、簡単にいえば、「ある画素でぼかしが計算できていれば、その隣はそれを元に計算できますよ」という方法です。これによってぼかしが速くなり、iPod touchでも1秒程度で計算できるようになりました。(でもまだ遅い・・・)

テクスチャ

これは単に上から画像を重ねるだけです。フレームやキラキラした効果を重ねたい場合に使うつもりで実装しましたが、今のフィルタでは使われていません。でも、フィルタを追加できるようになっているので、これから活用されるはずです。

マスク

これらの3種類は写真全体にのっぺりと効果を与えるので、選択的に効果を与えられるようにもしています。たとえば周辺のみに値をもたせたマスクと、ぼかしを組み合わせることで、「周辺だけぼかす」などができるようになっています。

ブレンド

更にテクスチャなどもただ上から重ねるだけではなく、加算、乗算、スクリーン、焼きこみなど、色々なブレンドができるようにしています。

フィルタ情報の同期

フィルタ情報は起動時と「フィルタを獲得しました」というプッシュ通知をトリガーにしてサーバと同期するようになっています。フィルタ情報自体も連想配列になっていて、これはダウンロードしたらiPhone内に保存するようにしていますので電波のないところでも写真を加工できます。(もっとも電波がないと加工した写真をアップロードできませんが・・・)テクスチャやマスクの画像も同様にフィルタを同期する際にダウンロードして保存しています。

アニメーション

iOSのフレームワークは簡単にアニメーションが作れるようになっています。ちょっとしたアニメを入れるだけで気持ち良くなるので使わない手はないです。個人的に好きなのが、フィルタを選択する部分が下からニョキッと出てくる動きです。filterViewの中に要素を配置しておいて、画面のすぐ真下にはみ出すように移動させて、アニメで画面上に移動させるだけです。

CGRect rect = filterView.frame;
rect.origin.y = self.view.frame.size.height;
filterView.frame = rect;
rect.origin.y = self.view.frame.size.height - rect.size.height;
[UIView animateWithDuration:0.4f animations:^(void) {
    filterView.frame = rect;
}];

読み込み中にピンが右下でくるくると回りますが、あれはanimateWithDurationではなく、UIImageViewを使っています。こちらはパラパラ漫画です。デザイナーの@t_ishiyamaが凝ったことしていて、animateWithDurationでは表現できない微妙な表現をしています。

NSArray *imgArray = [[NSArray alloc] initWithObjects:
                     [UIImage imageNamed:@"loading_0.png"],
                     [UIImage imageNamed:@"loading_1.png"],
             ...(略)...
                     [UIImage imageNamed:@"loading_23.png"], nil];
UIImageView *animationView = [[UIImageView alloc] initWithFrame:CGRectMake(250,360,60,60)];
animationView.animationImages = imgArray;
animationView.animationDuration = 0.6f;
[animationView startAnimating];
[imgArray release];

このようにアニメを作るにはこの2つの方法がありますが、アニメを作る手間と、データ容量と、その効果を比較してお得な方を使いたいものです。

おわりに

ちょっと凝ったことしてる部分を紹介しようと思ってソースコード見ながら書き始めましたが、全然大したことしてませんでした。最近やっとスラスラとObjective-Cが書けるようになってきたくらいのレベルで、勉強とか言語的な試行錯誤に使っていた時間の方が長い印象です・・・。

コメント

  1. Dalt より:

    共有ありがとうございます!
    以下3点質問があります。
    iPhoneアプリは何本かリリースしてるので
    ばりばり技術の話をしていただいてかまいません。
    面倒かもしれませんが、お答えいただけると幸いです。
    1:
    横画面で撮影した画像を600×600で保存する際にどのような
    画像処理を行っていますか?
    2:
    フィードで写真をクリックすると下に伸びたり縮んだりすると思うのですが
    これはどのようなアルゴリズムですか?
    UIImageにtouchDelegate的なのを付けて処理を行っているのでしょうか?
    3:初期起動時の動画は何を使って再生していますか?
    一般的に用いられてるやつだと画面をタッチした際に
    左上に青いボタンでDoneとか出てきたと思うのですが
    出てきた記憶がないので、どうやっているのかなと。
    以上、よろしくお願いします。

  2. katty0324 より:

    1. UIImagePickerController で取得した画像から、カメラビューの窓に当たる部分を切り出しているだけです。
    カメラビューの窓の座標はデバイスの方向に依存するので、それだけ考慮しています。
    2. UITableViewのinsertRowsAtIndexPaths:withRowAnimation: だけです。
    時間ある時にもっと綺麗なアニメーションを入れたいと思っています・・・。
    3. MPMoviePlayerController のcontrolStyleをMPMovieControlStyleNoneにして、viewを必要なところに貼りつけているだけです。
    (http://i.studio23c.com/?cat=48 などを参考にしたような記憶があります。)

  3. […] 投稿ナビゲーション ← 前へ […]

  4. […] → iOS編, サーバ編 […]

タイトルとURLをコピーしました