イッツァハローワールド

恥さらしていこうかなとか。

Node.js(restify) + MongoDB(mongoose) でAPIサーバつくってHerokuでデプロイするまで

先日ようやくリリースしたアプリ「お階段登り」では、ただ階段を上るだけのクソアプリと思いきや、他のユーザーの位置まで来たらすれ違ってみたり、一番上にいる人の段数をとってきたり、影でコソコソやっております。(クソなのには変わりませんが。)
https://itunes.apple.com/jp/app/o-jie-duan-dengri-zuo-rino/id956171342?mt=8&uo=4&at=10l8JW&ct=hatenablog


コソコソやるためにサーバーを自作しました。

サーバー側でやってること例

  • ユーザー登録
  • 自分のスコアを更新する
  • 1位のスコアを取得する
  • 自分が1位かどうか返す
  • 自分のスコアに一番近いユーザの情報を返す(→ユーザのスコアまで到達したらすれ違い実行)

アプリからはHTTPリクエスト投げ、JSON形式でレスを取得してレスによって描画を変えたりします。
こんなAPIサーバー的なものをNode.jsをつかって実装しました。

備忘録でサーバー作成からHerokuへのデプロイまでやり方を残します。

環境

  • Mac OS 10.10.2
  • Node.js v0.10.25
  • MongoDB 2.6.7

事前に勉強したこと

本件の記事と直接関係ないので、別記事にします。
↓こちら

Node.js + MongoDBでAPIサーバつくるまでに勉強したこと - イッツァハローワールド

なんでわざわざRestifyつかったか。

ググったら「REST Webサービスの構築に特化したフレームワーク」って出てきたのでおおそうかつかってみよ!って感じで使いました。
今回つくるような簡単なもんならExpressで十分に思えます。(作った後でわかりましたw)

あと、英語のドキュメントで四苦八苦してみたかったっていうのもちょこっとあります。

今回作るものの仕様

  • ユーザーごとに以下のデータを持つ
    • ユーザーID(id)
    • スコア(score)
  • サーバーはAPICRUDな動きをする
  • レスポンスはJSONで返す

あとは適当に。

まずはRestifyをつかって簡単なサンプルコードをつくる

Node.js入れるところとかは割愛します。

restifyをインストール

$ npm install restify

サンプルコード実装

以下は、http://0.0.0.0:8080/hello_worldにアクセスするとmessage : Hello World!って内容のJSONを返す例。

// sample.js
var restify = require('restify');

// サーバー生成
var server = restify.createServer();

// http://0.0.0.0:8080/hello_worldにGETリクエストしたときの処理
function helloWorld(req, res, next){
	// レスポンス
	res.send({message: 'Hello World!'});
}

// パスと関数の紐付け
// ↓はhttp://0.0.0.0:8080/hello_worldにGETリクエストしたらhelloWorld関数を実行
server.get('/hello_world', helloWorld);

server.listen((8080), function() {
	console.log('%s listening at %s', server.name, server.url);
});

実行

コマンド叩いて実行。

$ node sample.js

http://0.0.0.0:8080/hello_worldを開いてみる

f:id:hanamiju:20150305020831p:plain

できた。こんなかんじで、APIごとに関数と、関数とパスとの紐付けを定義すればわりと簡単に作れます。
以下はmongooseを落として、MongoDBをつかってCRUDします。

実際のAPIサーバ実装やってみる

まずMongoDB, mongoose入れる

$ npm install mongodb
$ npm install mongoose

コード

// sample.js
var restify = require('restify');
var server = restify.createServer();	// サーバー生成
server.use(restify.bodyParser());	// POSTデータをパースするひとを登録
server.use(restify.queryParser());	// クエリをパースするひとを登録

var mongoose = require('mongoose');
var uristring = 'mongodb://localhost/';

var db = mongoose.connect(uristring);
var Schema = mongoose.Schema;

// スキーマの定義
var userSchema = new Schema({
	score: Number,
});

// モデルを生成
mongoose.model('user', userSchema);
var User = mongoose.model('user');

// Create
function createUser(req, res, next){
	var user = new User();	// コレクションを作成
	user.score = 0;
	// つくったコレクションを保存
	user.save(function(err, data){
		res.send({function:'Create', status: 'OK', id: data._id});
	});
}

// Read
function getUsers(req, res, next){
	User.find(function(arr, data){
	// すべてのコレクションの情報を返す
		res.send(data);
	});
}

// Delete
function deleteUser(req, res, next){
	// 入力されたIDのコレクションを削除する
	User.remove({ _id: req.params.id }, function(err){
		res.send({function:'Delete', status: 'OK'})
	});
	return next();
}

function updateScore(req, res, next){
	// IDを検索
	User.findById(req.params.id, function (err, data) {
		// エラー処理
		if (err) {
			res.send({function:'Update', status:'NG', reason: 'User not found.'});
			return next();
		};
		// 入力パラメータ異常
		if (req.params.id == '' || req.params.id === undefined ) {
			res.send({function:'Update', status:'NG', reason: 'User not found.'});
			return next();
		};
		if (req.params.score == '' || req.params.score == 0) {
			res.send({function:'Update', status:'NG', reason: 'Score is invalid.'});
			return next();
		};
		// 入力されたIDのスコアを入力されたものに変更する
		User.update({ _id: req.params.id }, { $set: {score: req.params.score} },
		{ upsert: false, multi: true }, function(err) {
			if (err) {
				res.send({function:'Update', status:'NG', reason: 'Update faild'});
				return next();
			};
			res.send({function:'Update', status:'OK'});
		});
	});
}

server.get('/get_users', getUsers);
server.post('/delete_user', deleteUser);
server.put('/update_score',updateScore);
server.post('/create_user',createUser);

server.listen((8080), function() {
	console.log('%s listening at %s', server.name, server.url);
});

動作確認

実際に各API呼んだらどんなレスポンスを返すか見てみます。
動作を見るためにChromeのエクステンションのAdvanced REST Clientをつかいます。

create(create_user)

まずはコレクションを生成してみます。
Advanced REST Clientを起動し、Reqestタブを開きます。
POSTを選択し、パスを入力します。
f:id:hanamiju:20150305221011p:plain

入力したら「Send」ボタンを押下するとレスポンスが帰ってきます。
f:id:hanamiju:20150305221058p:plain

これでひとつコレクションができました。

get(get_users)

つぎに生成したコレクションを取得します。
こんどはGETを選択します。
f:id:hanamiju:20150305221224p:plain

「Send」ボタン押下で、先ほど作ったコレクションが取得できます。
f:id:hanamiju:20150305221253p:plain

update(update_score)

コレクションのscoreを更新します。
こんどはPUTを選択し、更新するid, 更新後のscoreを入力します。
ためしに1234を入れてみます。
f:id:hanamiju:20150305221640p:plain

「Send」ボタン押下で、更新完了が帰ってきます。
f:id:hanamiju:20150305221724p:plain

再度GETしてスコアを見てみます。
f:id:hanamiju:20150305221758p:plain
更新されました。

入力エラー用のコードも書いてみたので動作を確認してみます。
ためしに、idを存在しない値に書き換えて「Send」ボタンを押下すると
f:id:hanamiju:20150305221852p:plain
エラーが帰ってきました。

delete(delete_user)

つくったコレクションの削除を行います。
POSTを選択して、削除したいコレクションのidを入力します。
f:id:hanamiju:20150305222018p:plain

「Send」ボタン押下でコレクションが削除されます。
f:id:hanamiju:20150305222121p:plain

GETして確かめてみます。
f:id:hanamiju:20150305222233p:plain
DBが空になってました。

やった!

Herokuへデプロイしてみる

ここまでで実装はほぼ終わり。Herokuへデプロイしてみます。

Heroku toolbeld(Heroku推奨)のインストール


Herokuにログイン

$ heroku login

Herokuへ登録した時のメールアドレスとパスワードを聞かれるので入力します。

package.json作成

適当に入力します。

$ npm init

今回作成したのはこんな感じです。

{
  "name": "sample",
  "version": "0.0.1",
  "description": "sample api server",
  "main": "sample.js",
  "dependencies": {
    "mongodb": "~1.4.33",
    "mongoose": "~3.8.24",
    "restify": "~2.8.5"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "hanamiju",
  "license": "MIT"
}

Heroku用のProcfileを作成

web: node sample

こやつにHerokuから呼ぶjsファイルを記載します。拡張子はいらないです。

Herokuのプロジェクト作成

プロジェクトを生成する。今回はapiserverkunnという名前で登録しました。

$ heroku create apiserverkunn

MongoLabをいれる

先ほどまではローカル環境にMongoDBを構築してましたが、デプロイにあたりMongoLabを入れます。
MongoLabはHerokuのアドオンのひとつで、HerokuでMongoDBを496MBまで無料で使えます。最高。
というわけでアドオンを探します。Mongoとか検索ワードで入れればでてきます。
f:id:hanamiju:20150305212133p:plain

こいつのfreeプランをapiserverkunnに適用させます。
上記のようにブラウザからでもいいし、

$ heroku addons:add mongolab

でもオーライ。

sample.jsファイルを修正する。

sample.jsはこのままだとローカル環境でしか動かないので、ファイルを一部修正します。
まずはMongoDBとの接続時のパス

var uristring = 'mongodb://localhost/';  // 変更前
var uristring = process.env.MONGOLAB_URI || 'mongodb://localhost/'; // 変更後

これで、process.env.MONGOLAB_URIに接続し、存在しない場合はmongodb://localhost/に接続される。
つまり、ローカルと本番でコードを改変せずに済むということ。

つぎにサーバー処理開始時

server.listen((8080), function() {    // 変更前
server.listen((process.env.PORT || 8080), function() {    // 変更後

簡単に実行環境毎の設定でアプリケーションを実行することができます。

Herokuへデプロイ

$ git init
$ git add .
$ git commit -m "hoge"
$ git push heroku master

結果を見てみる

あらかじめcreateしといてから、getしてレスポンスを見てみます。
f:id:hanamiju:20150305223334p:plain

ちゃんと表示されました。ばんざい!

まとめ

  • Node.js (restify) つかえばRESTなAPIサーバーが簡単に作れる。
  • Herokuがあれば簡単にデプロイできる
  • MongoLabつかえば、タダで大容量のDB使える。

→ 簡単なCRUDサーバーならタダで簡単に作れる。

(備忘録)AppStoreにリジェクトされた後に再審査出したら言い訳聞いてくれないばかりか審査までまた一週間食うよ

タイトルの通りのことを2度やって無駄に申請長引かせた私はバカ野郎この野郎。

顛末フロー

  1. アプリ申請
  2. 一週間後リジェクト通知がくる。原因はPLA 3.3.12関連
  3. 「広告ちゃんと使っているで!」って言えば申請通る可能性高いからResolution Center に直訴
  4. itunes connectでアプリを再申請
  5. それからお返事がなく、1週間後に上記と同じ理由でリジェクト
  6. 以降ループ(2回もwww)

いけなかったこと。

上記順番4で再申請すると、また列の最後尾まで並んでしまって申請待ち再スタートのになってしまうようです。
せっかくResolution Center で質問や意見しても返事が来ませんでした。のう。
久々に申請するとリジェクトされた時のこととか覚えてないよう(泣)

正解フロー

  1. アプリ申請
  2. 一週間後リジェクト通知がくる。原因はPLA 3.3.12関連
  3. 「広告ちゃんと使っているで!」って言えば申請通る可能性高いからResolution Center に直訴
  4. itunes connectでアプリを再申請ここはじっと返事を待機
  5. リリース通知くる
  6. うれしい。(*^-^*)

注意事項

ビルドし直さないといけない時は、あたりまえに上記の手順やっても時間の無駄なのでお気をつけて。

勉強メモ 2/19

今日触れた内容をダイジェストで備忘録。

指定したセレクタを認識しなくする

メソッド

-(void)doesNotRecognizeSelector:(SEL)aSelector:

// hogeを認識しないようにする
- (void)hoge {
    [self doesNotRecognizeSelector:_cmd]; //_cmdはカレントセレクタ(つまりhoge)
}

NSObjectのメソッドなんでみんなつかえる。

コードを一度だけ呼ぶ

構文

static dispatch_once_t token;
dispatch_once(&token, ^{
    // 1度だけ実行するコード
});

シングルトンオブジェクト作るときに簡単にできそう。



宇宙のスケールでものを話す宇宙人スタンプ作った

懲りずにまたLineスタンプ作った(申請中)

コンセプト

「宇宙のスケールで、ものを話す宇宙人」

わりと路線は前回と似たかんじ。↓前回
スタンプリベンジしたの巻 - イッツァハローワールド

調子がいい時、調子に乗った時に使いたいです。

こんなかんじ

f:id:hanamiju:20150217022047p:plain

頑張ったところ

前回のスタンプでは似た構図の使い回しでリジェクト食らったので多様な構図になるように努力しようとしました。

でもやっぱり、楽したい。

今回はパーツをバラして一から描くものだけを最小限に抑えました。
それでもリジェクトされたらリベンジしてもっといいのを描けばいい話なのです。

おやすみなさい。

外部フォントをiOSアプリで使用する

ゲーム作る時にシステムフォントだけでは物足りない場合が往々にしてあるとおもいます。
そんなとき外部フォントが使いたいのでやり方を備忘録。

フォントを用意する

今回使用したのはこれ。(閲覧注意)
暗黒工房 日本語フリーホラーフォント怨霊


フォントをインストールします。
f:id:hanamiju:20150214204755p:plain

フォントをプロジェクトに登録します。

f:id:hanamiju:20150214204917p:plain

ターゲットにチェック入れてないとフォントが適用されないっていうオチが待ってますので注意。

info.plistに設定を追加する

f:id:hanamiju:20150214205357p:plain

Fonts provided by applicationの項目にフォント名を入力します。

適用する

InterfaceBuilderのフォント選ぶでもいいし、コードで入れるでも良いです。

    // 怨霊フォントのサイズ17をlabelに設定
    _label.font = [UIFont fontWithName:@"onryou" size:17];

表示してみました

f:id:hanamiju:20150214205938p:plain

超怖い。

UIViewを回転させる

地味に傾けるやり方とかわかんないなーと思って調べたので備忘録

コード

    // myViewを180度回転させる
    myView.transform = CGAffineTransformRotate(myView.transform, M_PI);

サンプル:前の記事のダイアログを回転させました。

手作りモーダルダイアログ(もどき)を作る - イッツァハローワールド

まず、対象のビュー(ContentsView)をOutletにする。
f:id:hanamiju:20150211194014p:plain


つづいてコードを少しいじる。

// CustomDialogViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    _mTitleLabel.text = _title;
    _mContentsLabel.text = _contents;
    // ContentsViewを180度回転させる
    self.mContentsView.transform = CGAffineTransformRotate(self.mContentsView.transform, M_PI);
}

結果

f:id:hanamiju:20150211194154p:plain

ひっくり返ってます。

スタンプリベンジしたの巻

処女作のスタンプ一晩で作ってレビュアーから「ほとんどダメ」っていう痛烈なリジェクト食らって意気消沈したのが前回のお話。

Lineスタンプ早くもリジェクト食らったの巻 - イッツァハローワールド

2ヶ月の時を経て気分転換に製作再開しました。
以下の修正を加えて再申請しました。

  • 手でボディランゲージを試みる
  • 無駄な遊びを入れる

加えて、せっかく再申請するんだから前回よりも面白くしたいなーとワードも再考しました。
そんだら前回よりも卑屈なワードが並んだ印象に。
スタンプは性格を表すな...

こんなかんじ

f:id:hanamiju:20150213025123p:plain

表情を変えないのがコンセプトだからなー
同じこと言われなければいいなー