姑プログラマの技術日記

作ったものをえらそーに解説したら幸せになれるかな

PICOLで使ったアニメーションたち。iOSで使えるアニメーションあれこれ

f:id:naochi_2012:20180716214437p:plain

PICOLではアニメーションもゲームっぽく、いろいろ使っています。

ほぼ全部!アニメーション書き出してみます。

公式ページ

スプラッシュ

UIImageView#animationImages

前の記事でかいたようにUIImageViewのanimationImagesを使用してパタパタ漫画をアニメーションしています。

naochi-interrupt.hatenadiary.com

コードはこんな感じ。

var imageList:[UIImage] = []
for i in (1 ... 35) {
//   let image = UIImage(named: "splash-\(i)")
    let imagepath = Bundle.main.path(forResource: "splash\(i)", ofType: "png")
    let image = UIImage(contentsOfFile: imagepath!)
    imageList.append(image!)
}
imageView.animationImages = imageList

Home画面

CAKeyframeAnimation

上下の雲が、右から左に流れています。 あと、カメラを起動する、バーコード型の丸いボタンがじわーっとアニメーションしています。 丸いボタンは、スプラッシュと同様にImageViewのanimationImagesを使っています。

雲はCAKeyframeAnimationです。

let animation = CAKeyframeAnimation.init(keyPath: "position.x")
animation.isRemovedOnCompletion = true
animation.repeatCount = .infinity
animation.values = [621,-207]
animation.duration = 19.0
animation.fillMode = kCAFillModeBoth
self.headerImageView1.layer.add(animation, forKey: "position.x")

これが、いただいた画像を使ってアニメーションしてみて、どう調節しても継ぎ目のところでアニメーションがかくっとしてしまう。

もともと業務系アプリ作ってた私は、「いいじゃん。ちょびっとだし」とか思ったのですが、WEB屋さんとかゲーム屋さんはアーティストに近いのでどうしても許せないらしく、 なんとかならないの?と言われ続けてがんばったのですが、なかなかできないでいました。

そしたらある日の会議でイケメンの社長が以前WEB系の技術者だったそうで、アドバイスをいただき、そのとおりに実装したらできました。

Twitterの動画ではわかりにくいのでダウンロードして直接みてみてください。

まず、あがってきたデザインのヘッダを3枚並べて、 f:id:naochi_2012:20180716231034p:plain:w450

真ん中のヘッダを左右反転させてくっつけます。 コードで書いてもいいけど、1回のことなので実際に素材をつくりました。

f:id:naochi_2012:20180716231134p:plain:w450

(わかりやすいように真ん中は背景を黒くしました)

それを画面に使うとXibはこんな感じになります。 素材は(2484 x 150)ですが、Retinaなので画面サイズとしては1242 x 75)

f:id:naochi_2012:20180716231300p:plain:w600

CAAnimationはUIViewと座標系が違うので、この初期値の状態でX座標は真ん中の650という値になります。

f:id:naochi_2012:20180716234153p:plain:w600

そして、端っこまできっかりアニメーションしてしまうとアニメーションの最初の画面と最後の画面に違いが出てしまうためにかくついてしまうので、同じになるように、3つめのヘッダの位置の最初でアニメーションが切り替わるようにしたら!綺麗に動くようになりました。

f:id:naochi_2012:20180716235601p:plain:w600

バーコードカメラのバーコード検出アニメーション

UIView.animation

動画斜めにしなきゃよかった

UIView.animate(withDuration: 0.2,
    delay: 0.0,
    options: .repeat,
    animations: { () -> Void in
    borderView.alpha = 0
},  completion: { _ in
     borderView.alpha = 1
})

トースト出したり、ダイアログを出したりするところでも使っていますが割愛。UIViewのアニメーションです。 iOSでアプリ作った人なら1回は使ったことがあるはず。

囲みを斜めにしてないのはほんとすみません。

査定演出アニメーション

HTML

査定のカッコイイアニメーションはデザイン会社の ビジネスが進化するモバイル・コンテンツを : : caeruX [カエルエックス] さんが作ってくれました。私はWKWebviewを貼り付けただけです

func loadHtml(){
  webView = WKWebView.init(frame: self.animationView.frame)
  animationView.addSubview(webView)
  webView.addSubview(self.skipButton)
  let fileURL = URL(fileURLWithPath: Bundle.main.path(forResource:"assessment_ver2", ofType: "html")!)
  webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
}

強いて言えば、私の古い端末(といってもiPhone6Plus)と、iPhoneXとかでは再生速度が違い、アニメーションの終了時間を端末ごとに切り替えて終わりを検出していることぐらいでしょうか。

チケット演出アニメーション

CAKeyframeAnimation + UIViewAnimation + SpliteKit

スロットがくるくるまわるのはCAKeyframeAnimationです。つなげてアニメーションしているのは上で書いた雲と同じ原理。 サーバーから受け取ったチケットのパーセンテージでアニメーションの終了座標を切り替えています。

コインがバラバラ落ちてくるのはSpliteKitを使いました。

qiita.com

雪をコインの画像に置き換えて、パーセンテージごとに数を増やしただけです。 こんなのが簡単に作れるなんて、すごいいい時代ですね。

残り時間アニメーションのあとのパルスのようなアニメーションはCAKeyframeAnimation 長い画像にマスクをかけてマスクをアニメーションしています。

あ、でも、要所要所でぽよんとか、ぴょんとか入れたのは割愛。

PICOLを丸裸。使ったものとか書いてみる

PICOL第三弾目です

naochi-interrupt.hatenadiary.com

naochi-interrupt.hatenadiary.com

体制

開発に携わる人はざっくり分けて

  • マネジメントする人
  • サーバー側の人
  • UIデザインする人
  • クライアントのアプリの人

って感じで私が担当したのは最後のアプリの人のところです。

プロジェクト管理ツール

  • Kibela
    • 議事録を残す
  • Github
    • ソース管理とクライアント側のAPIドキュメント
  • Slack
    • GithubのログとかTrelloでメンションされたときに通知が届くのが便利だった
  • Trello
    • タスク、バグ管理

技術的なやつ

サーバー側

クライアント

  • swift
    • XCTest,UITestとか
  • Crashlytics,Fabric
  • Alamofire
  • RXSwift
  • jazzy

なんか、クライアントはswiftって書いてそれで終わりなんだけど、ひねり出してみた。

サーバーの方が数段働き者です。もっとあるし!とかはらさんにいわれますたぶん。AWSのS3にあげるのもAPIがやってくれてるし。クライアント側からはノータッチです。

Alamofireとかとかも色々書いていますが、SwaggerCodegenがAPIにアクセスするときのモデルとか、API接続用のクラスとか自動生成してくれましたので使っています。

APIドキュメントと、クライアント側のモックサーバーも作ってくれまして。これはすごいって思いました。

qiita.com

自分のMacにサーバーが立っているので、データを自由にいじってテストできるし、すごく便利でした。最初、nameがintになってて有無を言わさず全然データがとれないとかそういうのが多発しましたが、そのうち慣れてきたし。

ビルドスキームはこんなかんじ

  • ローカルデバッグの時
    • ローカルに立てたRubyのサーバーでApiを投げる。あ、なのでローカルにPostgreSQLのDBが立ってます。開発機にデプロイする前の開発中のサーバーのソースも、指定されたブランチをcloneしてrake db:migrateとかして、ローカルでサーバー立てれば開発が進められます。この辺のことは、前の仕事でRuby on RailsでWebサイトを立ち上げていたのでなんとかできた。やっといてよかった。
  • みんなでデバッグの時
    • FabricでAdhocを配信、接続先は開発用サーバー
  • 本番ビルド
    • 本番機接続

それにしても、もっとかっこいい言葉で説明できるようになりたいです。

PICOLで発見したこと。アセットカタログは計画的に利用したほうがいい

最近、iPhoneも性能が上がってあんまりメモリのこととか気にしないでガンガン作ることが多いのかも。

naochi-interrupt.hatenadiary.com

前回書いたようにPICOL(ピコル)-バーコードをピコってお金に変える”スピード買取”アプリはゲームっぽい楽しさを意識して作ったので、アニメーションとかとにかく多い。

まずはスプラッシュからアニメーションです。

ここだけの話ですが、国民的麦わら海賊団的な雑誌のアプリのスプラッシュがキラッとしてるのは私が作ったものです。

PICOLのスプラッシュはなんかかわいいので、よくあるAppDelegateのdidFinishLaunchingWithOptionsに一回挟んでおわり系ではなく、アプリをバックグラウンドに落として一定時間すぎてアプリをアクティブにしたらもう一度スプラッシュから始まるような作りにしています。

このアニメーションは、CABasicAnimationとか、そっち系のコードでアニメーションするのではなく、パタパタ漫画です。gifアニメーションみたいな、そういうの。

これはUIImageViewに配列でイメージを渡すという。ごくごく簡単なもの。 まあ、誰でもできる。

var imageList:[UIImage] = []
for i in (1 ... 35) {
     let image = UIImage(named: "splash-\(i)")
     imageList.append(image!)
}
imageView.animationImages = imageList

スプラッシュはアセットカタログに入れてあるpng35枚なので、このような書き方です。 ただ、メモリ量が大きいのです。使用メモリが多いと、なにかのきっかけでクラッシュしたりとか、動きがもっさりしたりとか、いやな動きが多くなります。 この感じでスプラッシュを作ってた時のメモリは恥ずかしながらこんなかんじ。 画像を最適化したり、色々やった後でこれ。

f:id:naochi_2012:20180621140850p:plain

このときは、アプリが動いている時ずっとこんな感じのメモリで続いてて、アプリを一旦バックグラウンドに落としてすぐフォアグラウンドに戻すとガクッとメモリが減る。

InstrumentsのLeaksとか見てもメモリリークはしてないし試しにスプラッシュやめて見たらメモリが減ったわけです。

この配列に入ってるイメージが解放されなくて残ってるんだな〜〜〜といろいろやりました。

autoreleasepoolいれたり。

autoreleasepool(invoking: {
    var imageList:[UIImage] = []
    for i in (1 ... 35) {
        let image = UIImage(named: "splash-\(i)")
        let image = UIImage(contentsOfFile: imagepath!)
        imageList.append(image!)
     }
     imageView.animationImages = imageList
})

アニメーション終わってから涙ぐましい解放したりして。StackOverflowに書いてあったし。

imageView.startAnimating()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
    UIView.animate(withDuration: 1.0,
        delay: 0.1,
        options: .curveEaseInOut,
         animations: { () -> Void in
        self.imageView.alpha = 0
    }, completion: { _ in
        self.imageView.image = nil
        self.imageView.stopAnimating()
        self.imageView.animationImages = nil
        self.imageView.layer.sublayers = nil
        self.imageView.removeFromSuperview()
})

全然ダメでした。

そしたら、急に思い出しました。そう言えばアセットカタログがなかった昔は、普通にイメージをフォルダで追加してたなって。 そして、業務用のアプリは こういう書き方あんましちゃいけなかったんだっけってのを。

UIIimage* image = [UIImage imageNamed:@"imagename.png"];

こう書く。そうするとイメージがキャッシュされないんだって。

NSString* path = [[NSBundle mainBundle] pathForResource:@"imagename" ofType:@"png"];
UIImage* image = [[UIImage alloc] initWithContentsOfFile:path];

ってことで、アセットカタログに入れてあったイメージを使わず、昔の通りにファイルで追加して、呼び出して見た。

var imageList:[UIImage] = []
for i in (1 ... 35) {
//   let image = UIImage(named: "splash-\(i)")
    let imagepath = Bundle.main.path(forResource: "splash\(i)", ofType: "png")
    let image = UIImage(contentsOfFile: imagepath!)
    imageList.append(image!)
}
imageView.animationImages = imageList

そしたら、驚きの結果が

f:id:naochi_2012:20180621140838p:plain

アセットカタログに入れるのは、何回も使うものだけにした方がいいんですね。つい便利なので文明の力に甘えきってしまっていて反省しました。

私にはすごい発見で、目からウロコがぼろぼろ落ちましたが、別にメモリなんかたいしたことないって? で?って思っちゃダメ。

ていうか、みんなガシガシアセット使ってるよね。

PICOLのこだわり。チュートリアルページ

PICOLというアプリのiOSアプリを担当させていただいて作りました。

公式ページ

PICOLは、バーコードを使って査定する買い取りアプリなのですが、もともとのコンセプトが

「ゲームっぽさ」

を目指しているのもあり、よくある標準UIKitのタブビューやナビゲーションバーを使わないデザインで画面デザインが上がってきました。

PICOLのホーム画面 f:id:naochi_2012:20180618135124p:plain:w180

チュートリアルもHOME画面ににたデザインで仕上がってきたので、作ったチュートリアルはこんなかんじ。

どこがこだわりなんだ。普通じゃない?ものすごく普通じゃない?って思ったアナタ。

普通はこっちです。

ね。こっちでも全然大丈夫じゃないですか。チュートリアルなんか1回しか見ないものだし、そこに力入れるのも。。って感じもするし。

でも、やだったんです。ページコントロール(画面下のページ番号が点々ででてるやつ)が一緒に横にスクロールしちゃったりするの。

そうなると画面の上下の赤い雲もヘッダとフッタな訳で。これも動いちゃやだなって思いました。ああ、そうなると黒いページ番号も固定したいじゃないかーって、さらに思ってしまいました。

社長がイケメンだしチュートリアルから気持ちいいものを使ってもらいたいですしね。

では、どうやって実装したのか図解します。

まず、チュートリアルは横スワイプでページを切り替えるだけの機能なのでUIPageViewControllerを使用しました。UIScrollViewかどちらかなのかなって思います。

f:id:naochi_2012:20180618161120p:plain:w400

横にスワイプして表示されるチュートリアルのそれぞれのページは、UIPageViewControllerDelegateのメソッドで、今表示するページを指定すて返すような仕組みになっています。表示はこれにお任せです。

 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
}
 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
}

f:id:naochi_2012:20180618161130p:plain:w400

そして、ヘッダ、フッタと、ページコントロールを持つ、固定したいビューをUIViewとして別のnibに作り初期化したら最初のUIPageViewControllerのviewDidLoadで

        self.view.addSubview(tutorialHeaderView)

とやるだけ。 そうすると固定されたヘッダビューの出来上がりとなります。

f:id:naochi_2012:20180618161140p:plain:w400]

ただし。これだけだと上にヘッダのビューを重ねているので、ページをスワイプ するときのタッチイベントがヘッダビューに取られてしまうので、ヘッダビューのクラスに hittestをつけて、nibからイベントを下のビューに逃したいViewに適当なtagをつけておきます。 そうするとスワイプ のイベントも効くようになります。

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        //真ん中のviewに触られたら下のviewにタッチイベントを透過したい
        if view?.tag == 111 {
            return nil
        }
        return view
    }
    

サンプルコード、時間できたらあげると思う。

はじめに

きのう、客先でこんなこと聞かれました。

お客様「仕事としてあなたはPM?SE?」

私「PGです。(キリッ)」

お客様「私は、数百人のプロジェクトのPMを数個抱えて仕事してきたのですが、、、(続く)」

会社にとってはPMえらいにしたいでしょうね。大勢の人を統率しないといけないし、単価も高いし。

でも、上下関係ではないと思っています。単なる職種の違いだけ。

じぶんはじぶんがやりたい仕事をつづけていきたいので、独立しました。

プログラミング?覚えなくていいよ。キミはプログラマを雇って使えばいいんだから。って誰かが言ったみたいだけど、 なら、使ってください。たぶんいい仕事します。