初心者が4ヶ月半でアプリを申請するまでにやったこと
こんにちは、@keimaejima です。
ここ4ヶ月半ほど、Port Loungeという動画共有アプリをつくってきまして、先日やっとアップルストアへの申請にこぎつけました。
portlounge.jp
会社の仲間と3人で、インフラ&バックエンド、デザイナー、iOS&webアプリという担当でやってきました。
仕事をしながらの開発だったため、割と効率性を意識してやってきました。今回の記事では、アプリ開発初心者だった僕がやってきたことを参考にした書籍やwebページと一緒に時系列で簡単にまとめてみました。
これからアプリを作ろうと思っている人の参考になれば幸いです。
前提
まず前提として、僕はiOSアプリの開発は初体験でしたが、プログラミグ自体には経験がありました。学生時代に2年ほど簡単な受託開発(PHP)をしていたのと、会社に入ってからは3ヶ月間の開発研修(Java)を受講しています。
ですので、プログラミング自体が初心者の方はドットインストールでRubyなどの言語に簡単に触れてみて、プログラミングとは何かについて雰囲気を掴んでおいた方が良いと思います。
あとは、何らかの形で通信を行うアプリを作りたい人は、この本を読んでおくと良いと思います。クライアントとサーバーの関係はwebもアプリも原理的には同じなので、参考になると思います。
www.amazon.co.jp
導入期
アプリを開発しようと決めてから、まずはこのサイトに沿って簡単なToDoアプリをつくってみました。アプリ開発の全体像がなんとなく掴めます。また、実機で動く簡単なアプリが実際に作れるのでなんだか「これはいけるのでは」と前向きな気持ちになれます。
easyramble.com
次にこの本を読みました。Swiftの基本的な文法から、メジャーなライブラリーの使い方まで、初心者だけではなく本格的にアプリ開発をはじめてからも使える知識が詰まっています。
www.amazon.co.jp
この本は実際にサンプルアプリを開発しながら学習をすすめていく形式で書かれています。僕は、この本を一通りすすめた後も、Swiftの文法についてまとめてある章(4章)を繰り返しリファレンスとして使っていました。
ただ、注意して欲しいのが、Swiftは2015年9月にメジャーバージョンアップが行われましたが、この本はSwift2.0には対応していないということです。
そのため、下記のような情報を参考にしつつ、文法を読み替えていくことが必要です。
qiita.com
開発期
さて、いよいよ開発に入っていきます。
開発中はとにかく、書く→つまずく→調べるの繰り返しですので、いかにこのサイクルを早く回せるか、そしてつまずきの時間を短くできるかがポイントになってきます。
ここでは、僕は以下のようなことを気にしていました。
(1)一定時間考えてわからないことは人に聞く
これは結構重要だと思います。プログラミングをしていてわからないことがあるとつい自分の力だけで解決しようとしがちですが、数学や物理と一緒で一人で考えてずっと答えが出ないことでも、一瞬で解決できるということがよくありました。
僕の場合は長くても2時間自分でチャレンジして解決できないときは人に聞いていました。
職場の知人に聞いたり、下記のサイトにはすごくお世話になりました。
teratail.com
stackoverflow.com
teratailは日本語のQ&Aサイトとしては一番良いと思います。しかし、やはり英語版のStack overflowが質・量ともに最高です。
コードを貼って、簡潔に説明をすれば意味をくみとってもらえるので、そんなに英語力も必要ないと思います。
(2)情報取得の幅を広げてみる
また、この時期にはSlideshareで必要な情報を調べて、そのSlideをつくった人に連絡をとってみるみたいなこともやっていました。
www.slideshare.net
先にも書きましたが、詳しい人に聞くと一発で解決できることが沢山ありました。
(3)ライブラリについてはこまめに調べる
これは、いわゆる「車輪の再発明」をしないために気をつけていたことです。既にライブラリとして提供されているのに、同じようなものを作ってしまうのは勉強にはなるかもしれませんが、「アプリを完成させる」という目的を達成するためには時間の無駄です。
qiita.com
デバッグ、ユーザーテスト
さて、ある程度アプリが形になったら、デバッグとユーザーテストを行っていきましょう。
チームメンバーなどにアプリをシェアするときには、Deploygateを使っていました。
deploygate.com
いちいちxcodeを経由しなくてもアプリを渡すことができるのでとても便利です。
Deploygateでアプリを入るするためには、配布先デバイスのUDIDをすべて登録しなければならないことに注意してください。
qiita.com
申請
テストが済み、いよいよ申請です。
申請については、こちらに載っている通りに進めていくだけで十分でした。
akira-watson.com
画像やアプリの説明文章など、用意しておかなければならないものが多いので、アプリの開発途中で一度こちらに目を通し事前に素材を用意しておくとスムーズだと思います。
現状
実は、このブログを書いている途中でリジェクトをくらってしまい、現在再申請に向けてアプリの改修を行っているとこです。
CGM系のアプリは、コンテンツの質の維持、不正なユーザーの適正な排除など色々と気を使う必要があるみたいです。
無事アプリが審査を通過したら、リジェクトで指摘された点などについてまた追記したいと思います!
それでは、みなさんも楽しいアプリ開発を!
【Swift】GPUImageで動画にフィルターをかけて保存する
8月も半ばを過ぎましたね。
現在10月のリリースを目指して仲間と作っているアプリがありまして、今年の夏休みはほぼ全ての時間をその開発に捧げました。
iosアプリをSwiftで書いているのですが、まだ世に出て1年ほどの言語ということもあって、少し込み入ったことをやろうとするとまとまった日本語の参考記事がほとんど無いという感じです。
で、今回はいつもお世話になっているSwiftコミュニティに貢献するべく、動画関連の処理について書こうと思います。
日本でもこれからもっと動画系のサービスが一般化していくだろうと思います。
よくある、「動画を撮影」→「プレビューしながらフィルターを選択」→「選択されたフィルターを合成して動画を保存」という処理を作ったのですが、結構量が多いので今回は一番需要がありそうな動画にフィルターをかけて保存する部分について紹介します。
今回はGPUImageというフレームワークを使っています。
これは、デフォルトでイケてるフィルターが大量にクラスとして含まれている上に、オリジナルのフィルターもすぐに作れてしまうという優れもののフレームワークです。
僕はcocoapodsで導入しましたが、直接ファイルをプロジェクトに追加するなどの方法もあるので、好きな方法でやると良いと思います。
プロジェクトフォルダ内のPodfileに下記を書いて、
use_frameworks! pod 'GPUImage'
$pod install
を実行するだけなので超お手軽です。
導入が終わったら早速コードを書いていきます。
まずはインポートします。SwiftFilePathはファイルの存在を確認したり、削除するために使いました。
import GPUImage import SwiftFilePath
viewDidLoadの中で色々とやっていきます。
まずはGPUImageの動画読み込みと書き込みのためのインスタンスを定義します。
var movieWriter:GPUImageMovieWriter!//書き込み用 var movieFile:GPUImageMovie!//読み込み用
保存先のパス指定などを行います。
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)//アプリからアクセスできるディレクトリを取得 let documentsDirectory = paths[0] as! String let filePath : String? = "\(documentsDirectory)/temp.mp4"//フィルターをかけたい動画までのパス、あらかじめこちらに動画を用意しておいてください let mediaURL : NSURL = NSURL(fileURLWithPath: filePath!)! //表示用ビュー、プレビューが必要ない場合は無しでもOKです var fileView = GPUImageView() fileView.frame = CGRectMake(0, 0, 480, 480) //一時保存用パス、フォトアルバムに保存する前に動画を置いておく場所です let newFilePath = "\(documentsDirectory)/newTemp.mp4" //一時保存先を空にしておく var contentsPath = Path(documentsDirectory) for path in contentsPath.contents! { if(path.toString() == newFilePath){ path.remove() } }
さらに、保存する動画の設定を定義していきます。
let movieURL = NSURL(fileURLWithPath: newFilePath!)! self.movieWriter = GPUImageMovieWriter(movieURL: movieURL, size: CGSize(width: 480, height: 480),fileType:AVFileTypeMPEG4, outputSettings:nil)//保存する動画のサイズ self.movieWriter.assetWriter.movieFragmentInterval = kCMTimeInvalid//これ書かないと動作が不安定になります self.movieWriter.shouldPassthroughAudio = true movieWriter.encodingLiveVideo = true
あとはフィルターと動画を関連付けていきます。
movieFile = GPUImageMovie(URL: mediaURL) movieFile.playAtActualSpeed = true let filter = GPUImageSepiaFilter()//かけたいフィルターを定義 filter.addTarget(fileView) self.view.addSubview(fileView)//プレビュー画面を追加 movieFile.addTarget(filter) filter.addTarget(movieWriter)
あとは、動画の再生時と終了時に下記のメソッドを呼び出してあげればOKです。
//再生時 movieFile.startProcessing() movieWriter.startRecording() //終了時 movieWriter!.finishRecording() UISaveVideoAtPathToSavedPhotosAlbum(filePath, self, Selector("filePath:didFinishSavingWithError:contextInfo:"), nil)
こまごまと書いてきましたが、意外と簡単ですよね。動画のリアルタイム処理がたったこれだけのコードで実現できてしまいます。
フレームワークの力はすごいですね。
その他、ボタンやプログレスバーを追加したサンプルコードを下に貼っておきます。
そのままコピペすれば動くと思います。xcode6.4,Swift1.2で確認しました。
import UIKit import GPUImage import AssetsLibrary import SwiftFilePath import SpriteKit import MBCircularProgressBar class ExistsVideoFilterViewController: UIViewController { var filterNum: Int = 0 let filters = [ GPUImageSepiaFilter(), GPUImagePolkaDotFilter(), GPUImageGrayscaleFilter() ] var filter: GPUImageFilter! var movieWriter:GPUImageMovieWriter! var movieFile:GPUImageMovie! var newFilePath : String! private var myButtonStart : UIButton! private var myButtonStop : UIButton! var barView = MBCircularProgressBarView() override func viewDidLoad() { super.viewDidLoad() filter = filters[filterNum] let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) let documentsDirectory = paths[0] as! String let filePath : String? = "\(documentsDirectory)/temp.mp4" let mediaURL : NSURL = NSURL(fileURLWithPath: filePath!)! //表示用ビュー var fileView = GPUImageView() fileView.frame = CGRectMake(0, 0, 480, 480) //保存用パス newFilePath = "\(documentsDirectory)/newTemp.mp4" //一時保存先を空にしておく var contentsPath = Path(documentsDirectory) for path in contentsPath.contents! { if(path.toString() == newFilePath){ path.remove() } } let movieURL = NSURL(fileURLWithPath: newFilePath!)! self.movieWriter = GPUImageMovieWriter(movieURL: movieURL, size: CGSize(width: 480, height: 480),fileType:AVFileTypeMPEG4, outputSettings:nil) //"Couldn't write a frame" エラーが出ないための一行 self.movieWriter.assetWriter.movieFragmentInterval = kCMTimeInvalid self.movieWriter.shouldPassthroughAudio = true movieWriter.encodingLiveVideo = true movieFile = GPUImageMovie(URL: mediaURL) movieFile.playAtActualSpeed = true //movieFile.audioEncodingTarget = movieWriter! //フィルター関連 filter.addTarget(fileView) //self.view.addSubview(fileView) movieFile.addTarget(filter) filter.addTarget(movieWriter) makeButton() makeBar() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } func onClickStartButton() { movieFile.startProcessing() movieWriter.startRecording() println("started") var timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("printProgress"), userInfo: nil, repeats: false) } func printProgress(){ //規定のレンダリング時間に達するまでは0.5秒に一度進捗を監視する barView.percent = CGFloat(movieFile.progress*100) println(movieFile.progress) if(movieFile.progress >= 1){ onClickStopButton() }else{ var timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("printProgress"), userInfo: nil, repeats: false) } } func onClickStopButton() { filter.removeTarget(self.movieWriter!) movieWriter!.finishRecording() savePhotoAlbum(newFilePath) println("completed") } func savePhotoAlbum(filePath:String!){ UISaveVideoAtPathToSavedPhotosAlbum(filePath, self, Selector("filePath:didFinishSavingWithError:contextInfo:"), nil) } func filePath(filePath: String!, didFinishSavingWithError: NSError, contextInfo:UnsafePointer<Void>) { if let error = didFinishSavingWithError as NSError? { println("error") } else{ let removePath = Path(newFilePath) removePath.remove() println("success!") } } func makeButton(){ // UIボタンを作成. myButtonStart = UIButton(frame: CGRectMake(0,0,120,50)) myButtonStop = UIButton(frame: CGRectMake(0,0,120,50)) myButtonStart.backgroundColor = UIColor.redColor(); myButtonStop.backgroundColor = UIColor.grayColor(); myButtonStart.layer.masksToBounds = true myButtonStop.layer.masksToBounds = true myButtonStart.setTitle("記録", forState: .Normal) myButtonStop.setTitle("停止", forState: .Normal) myButtonStart.layer.cornerRadius = 20.0 myButtonStop.layer.cornerRadius = 20.0 myButtonStart.layer.position = CGPoint(x: self.view.bounds.width/2 - 70, y:self.view.bounds.height-50) myButtonStop.layer.position = CGPoint(x: self.view.bounds.width/2 + 70, y:self.view.bounds.height-50) myButtonStart.addTarget(self, action: "onClickStartButton", forControlEvents: .TouchUpInside) myButtonStop.addTarget(self, action: "onClickStopButton", forControlEvents: .TouchUpInside) // UIボタンをViewに追加. self.view.addSubview(myButtonStart); self.view.addSubview(myButtonStop); } func makeBar(){ //プログレスバー barView.percent = 0 barView.fontColor = UIColor.orangeColor() barView.progressRotationAngle = 0 barView.progressAngle = 100 barView.progressLineWidth = 10 barView.progressColor = UIColor.redColor() barView.progressStrokeColor = UIColor.redColor() barView.emptyLineColor = UIColor.orangeColor() barView.emptyLineWidth = 10 barView.backgroundColor = UIColor.clearColor() barView.frame = CGRectMake(self.view.bounds.width/2-100, self.view.bounds.height/2-200, 200, 200) self.view.addSubview(barView) } }
以上っす!
引き続き開発頑張りますので、リリースの暁には是非使ってみてください!
【Hubot】HubotとHipChatでお手軽ChatOpsを作る
今回はHubotとHipChatで簡単なChatOpsを作ってみます。
HubotはGitHub社が開発しているbotの開発・実行フレームワークです。
ChatOpsというのは、SlackやHipChatみたいなチャット系サービスから特定のアカウントに向けて特定の言葉を送るとなんらかの処理をやってくれる仕組みみたいなやつです。
使い方としては色々ありそうですが、スマホからメッセージアプリでデプロイできちゃうみたいなのは便利そうですよね。
今回は、HipChatで送ったメッセージに対して決まった処理を返すということをやってみます。
準備する
Macのローカル上で開発していきます。
Hubotはnode.jsで動くので、インストールします。
brew install node
homebrewをインストールしていない場合はこちら
Homebrew — OS X用パッケージマネージャー
からしてください。
次に、Hubotに命令を書くためのcofeescriptをインストールします。
npm install -g yo generator-hubot
オプションの -g はグローバルインストールを指していて、インストールと同時にパスを通してくれるらしいです。
yoはプロジェクトのテンプレートを作ってくれるツールらしいです。
npmコマンドがきかない場合は、パスを通してあげましょう。
vim /.bash_profile export PATH=$HOME/local/node/bin:$PATH source /.bash_profile
プロジェクト作成
適当な場所でプロジェクトを作成します。
mkdir mybot cd mybot
そして、yoを実行します。
yo hubot
すると、Hubotの設定に関して色々聞かれます。
? ========================================================================== We're constantly looking for ways to make yo better! May we anonymously report usage statistics to improve the tool over time? More info: https://github.com/yeoman/insight & http://yeoman.io ========================================================================== Yes _____________________________ / \ //\ | Extracting input for | ////\ _____ | self-replication process | //////\ /_____\ \ / ======= |[^_/\_]| /---------------------------- | | _|___@@__|__ +===+/ /// \_\ | |_\ /// HUBOT/\\ |___/\// / \\ \ / +---+ \____/ | | | //| +===+ \// |xx| ? Owner: keimaejima ? Bot name: mybot ? Description: A simple helpful robot for your Company ? Bot adapter: (campfire) hipchat ? Bot adapter: hipchat create bin/hubot create bin/hubot.cmd create Procfile create README.md create external-scripts.json create hubot-scripts.json create .gitignore create package.json create scripts/example.coffee create .editorconfig _____________________________ _____ / \ \ \ | Self-replication process | | | _____ | complete... | |__\\| /_____\ \ Good luck with that. / |//+ |[^_/\_]| /---------------------------- | | _|___@@__|__ +===+/ /// \_\ | |_\ /// HUBOT/\\ |___/\// / \\ \ / +---+ \____/ | | | //| +===+ \// |xx|
それぞれ次のような意味ですので、適当に入力していきます。
? Owner: (オーナーの名前) ? Bot name: (ボットの名前) ? Description: (ボットの説明) ? Bot adapter: (使うツール、今回はHipChat)
これでインストールが完了するので、試しにHubotを起動して話しかけてみます。
bin/hubot —mybot
mybot> mybot ping
PONG
うまくいきました!
HipChatと連動させる
さて、HipChatと連動させていきます。
HipChat本体はAdapterというものを通してHubotとやりとりをするので、HipChatのアダプターをインストールします。
npm install hubot-hipchat —save
インストールが終わったら、bin/hubotにHipChatの情報を書き込みます。
(事前にボット用アカウントは作っておいてください。)
vim bin/hubot
#!/bin/sh set -e npm install export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH" export HUBOT_HIPCHAT_JID="Hubot 用アカウントのJabber ID" export HUBOT_HIPCHAT_PASSWORD="Hubot 用アカウントのパスワード" export HUBOT_HIPCHAT_ROOMS="ルームの Jabber ID" export HUBOT_LOG_LEVEL="debug" exec node_modules/.bin/hubot "$@"
Jadder IDはHipChatの「Edit Profile」から確認できます。
hubotの振る舞いについては、script以下のファイルに記述していきます。
hubot起動時に拡張子.coffeeのファイルをすべて読み込んでくれるので、ファイルの数と名前はなんでも良いみたいです。
module.exports = (robot) -> robot.respond /ザワールド/i, (msg) -> msg.send "時よ止まれ" robot.respond /てめえは俺を/i, (msg) -> msg.send "怒らせた"
もう一度hubotを起動します。
bin/hubot -a hipchat
エラーなく起動できたら、HipChatから話しかけてみます。
うまくいきました!
今回はシンプルな応答式でしたが、サーバーでコマンドを実行する処理を書いたりしたら色々と使えそうですよね。
引き続き勉強したいと思います。
以上っす!
【Java】配列とHashMapとArrayListの違い
基礎的ですが、とても重要な配列とHashMapとArrayListの違いについて書いておきます。
格納できる値について
まず、格納できる値がそれぞれ異なります。
配列 => 基本データ型、参照型いずれも可
int[] hoge = {1,2,3}//OK String[] hoge2 = {"太郎","二郎","三郎"}//OK
ArrayList => 参照型のみ可
ArrayList<int> hoge = new ArrayList<>();//NG ArrayList<String> hoge2 = new ArrayList<>();//OK
HashMap型 => キーと値ともに参照型のみ可
HashMap<int,String> hoge = new HashMap<>();//NG HashMap<String,int> hoge2 = new HashMap<>();//NG HashMap<String,String> hoge3 = new HashMap<>();//OK
基本データ型というのは、int,boolean,byte,char,short,float,long,doubleの8つの型で、それ以外は全て参照型だと思って良いらしいです。
(こちらの記事がわかりやすいです。『【Java】 基本データ型 と 参照型 の違い』)
長さの取得について
それぞれfor文などで回して使うことが多いと思いますが、その際に長さ(要素の数)を取得するメソッドが異なります。
それぞれ、
(配列).length(); (ArrayList).size(); (HashMap).size();
で取得することができます。
値の重複について
そして、値の重複が許されるかどうかについてですが、結論から言うと3つとも許されませんが、HashMapだけは自分で気をつける必要があります。
たとえば、
HashMap<String,String> map = new HashMap<>(); map.put("apple","ios"); map.put("apple","android"); System.out.println(map.get("apple")); //android
みたいに同じキーを指定して書くと値が上書きされてしまいます。
配列とArrayListのキー(インデックス)が重複してはいけないことは直感的にわかると思うのですが、HashMapだけ勘違いしていたことがあったので一応書いておきます。
以上っす!
【シェルコマンド】lsとcdが使えれば道は開ける
本稿を担当する事になりましたid:muldzです。
私はデータ解析とインフラストラクチャについての記事を多く担当する予定です。
本稿の対象読者はエンジニアに限りません。
PCを使う全ての方が身につけてほしい普遍的な武器を紹介します。
シェルコマンドとは?
映画でハッカー(?)がPCの黒い画面に向かってコマンドを打ち込んでいる様子が想像できますか?
あれがシェルコマンドです。
馴染みがない人は全く使用していないと思います。
シェルコマンドが使えると何が嬉しいか?
メリットは大きく分けて2つあります。
1, 作業の効率化(できる事をもっと速く)
あるファイル(hoge.txt)の名前を変更して大文字のHOGE.txtにしたい とします。
右クリックをして、名前を変更して、ファイル名が変わりました。
当たり前の話です。
では、あるディレクトリ内に100個のテキストファイルがあり、
全てのテキストファイル(~.txt)の名前を大文字のファイル名にしたいとします。
100回右クリックして名前を変更するのは面倒ですよね。
これがシェルコマンドを使うと一瞬で変更できるのです。
2, 設定ファイルへのアクセス(できない事をできるように)
普段PCを触っていると、見えないファイルというのが存在します。
色々な設定が書かれているファイルで、隠しファイルと呼ばれます。
もっと便利にPCを使いたいと思った時に隠しファイルを設定する事は避けて通れません。
シェルコマンドを使うと隠しファイルが容易に変更できるようになります。
しかし、シェルコマンドを覚えるのは大変
シェルコマンドは物凄い数あります。
上記はUNIXのコマンドについてのみ扱った書籍ですが、448ページあります。
コマンドだけで本が1冊書けてしまう程の膨大なコマンドが存在するのです。
必要になった物から覚えよう。使わない物は覚えなくて良い。
これはid:muldzの私見ですが、初めから完璧主義を目指すのは挫折を招きます。
完璧主義を目指した事によって、シェルコマンドを毛嫌いするようになっては非常にもったいないです。
必要性や意味を理解しないまま、ムリをして覚えた知識は決して役に立ちません。
必要が生じたコマンドを必要なタイミングで、覚えていきましょう。
そして、楽しくて便利なシェルコマンドの世界に一歩を踏み出しましょう。
シェルコマンドを使ってみよう!
Macユーザーの方は
Ctrl + Space
を押してください。
すると、
Spotlight検索画面が開きます。
このテキストボックスに"terminal.app"と入力して、
Enterを押すと
Terminalが開きます。
このアプリケーションでシェルコマンドを入力すると、コマンドが実行されます。
ls : ディレクトリの中身を見る
list segmentsの略称
(略称は必ず確認して下さい。意味不明な物はすぐに忘れていきますが、意味とともにコマンドを覚える事で定着が早くなるはずです)
ディレクトリの中身を確認する事ができます。
fuga.txt, hoge.txt, new.txt という3つのテキストファイルと
hogeというディレクトリを確認する事ができます。
cd : ディレクトリを移動する
change directoryの略称
tempディレクトリからhogeディレクトリに移動
"cd .."と入力すると、1つ上の階層に戻る事ができます。
(この例では、temp直下のhogeからtempに戻る)
長ったらしい名前のディレクトリがあった場合、入力するのが大変ですが、
途中まで入力して(この例では"cd to"まで入力)、Tabを押すと、
ディレクトリ名を補完してくれます。
ディレクトリ名を数文字入力したらTabで補完するクセをつけると効率的です。
以上、たった2つのコマンドですが、
この2つであなたは移動と確認というとても重要な事を覚えました。
何をするにも、移動と確認は不可欠なので、もうあなたは立派なシェルコマンド使いです。
この記事であなたが身につけた武器
シェルコマンド
確認 : ls
移動 : cd