まちお開発日記 ~ machio Development Diary ~

思いつきで技術的なことをつらつらと

イルミネーションもいいけど僕の『正弦関数 in Swift』で作ったハート群もめっちゃ綺麗だから見て見て

この記事は Hakusan mafia Advent Calendar 2017 の24日目の記事です。どの辺がマフィアなのかはよくわかってません。。。

はじめに

クリスマスイブですね(記事出した時はそうなんです)。街には笑顔が溢れて、活気に満ちていることと思います。カップルで幸せな時間を共にする人、家族で温かい食卓を囲む人、孤独を噛みしめる人など、色々な過ごし方の人がいると思います。

中には クリスマスなのにSwiftでハートがセクシーに動くAnimationが作れなくて困ってる人 もいるでしょう。今回はそんな人たちのための記事です。

Swiftでは基本的なAnimationは数行で実装できるようになっている(本当に素敵)のですが、今回は標準では積まれていないゆるやかな往復運動正弦関数を用いて実装したいと思います。

https://gyazo.com/fe84349e823b9a2ab50167daac931885

これから触れるAnimationは実際に僕の会社のサービス PinQul でも使われているものの一部で結構我ながら気に入ってます。

今回はこのハートの動きの部分が純Swiftでどうやって動かされているのかを見ていきたいと思います。

方針

このハートのAnimationは3つの小さなAnimationに分解できます。

  1. だんだんopacityが小さくなるAnimation
  2. y方向に一定速度で上昇していくAnimation
  3. x方向に緩やかに振動するAnimation

です。1と2に関してはCABasicAnimationというswift組み込みのAnimationで簡単に実装できるので、今回は3をメインに触れていきたいと思いますが、一応Animationの基礎についても触れていきます。

CAAnimationの基礎

僕の記事なんて読まなくても、素晴らしい記事がQiitaにたくさん落ちています。

qiita.com

その中でも今回必要なものだけを抜粋しました。

CABasicAnimation

SwiftでUIViewの属性の1つをAnimationで変化させたいと思った時、大概の場合は

  • 何を変化させるか(keyPath
  • 初期値(fromValue
  • 終了値(toValue
  • 期間(duration

を指定するだけで簡単に実装できます。これを可能にしているのが CABasicAnimation です。

例)

//
// 透明からだんだんViewが現れるAnimation
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300
verticalAnimation.duration = 3.0
heartView.layer.add(verticalAnimation, forKey: nil)

https://gyazo.com/8200a6b47e063a18dd059bb725e07d97

※CABasicAnimationを用いてAnimationで変化させられる要素(keyPath)は以下を参照してください。

developer.apple.com

これでも十分綺麗ですが、まだちょっと物足りないですね。

CAAnimationGroup

実はCAAnimationはいくつでも組み合わせることができます。CAAnimationGroupというクラスを使っていきます。

CAAnimationGroupのインスタンスがもつ animations プロパティにCAAnimationの配列を渡すことで、それらのAnimationが1つに集められた1つのAnimationを作成することができます。

例)

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
        
let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300

let animationGroup = CAAnimationGroup()
animationGroup.duration = 3.0
animationGroup.animations = [opacityAnimation, verticalAnimation]
heartView.layer.add(animationGroup, forKey: nil)

https://gyazo.com/a4dbe9ca175efcc04483ddd394397b67

透明度を追加したことで儚げな感じがプラスされましたね。人の心は移ろいやすいものです

もちろん配列に追加していけば3つでも4つでもAnimationを組み合わせることができます。

本題の正弦関数Animationについて

Swiftでの正弦関数

Swiftの標準ライブラリである Foundation は豊富に数学関数を含んでいます。

三角関数関連だけでも以下のラインナップです。完璧に揃ってます。

let x = CGFloat(1.0) //単位はラジアン

// 通常のサイン・コサイン・タンジェント
sin(x)
cos(x)
tan(x)

// アークサイン・コサイン・タンジェント
asin(x)
acos(x)
atan(x)

// ハイパボリックサイン・コサイン・タンジェント
sinh(x)
cosh(x)
tanh(x)

// アークハイパボリックサイン・コサイン・タンジェント
asinh(x)
acosh(x)
atanh(x)

今回はその中でも正弦関数(sin)をつかって滑らかなAnimationを実装していきます。

CAKeyframeAnimation

CAKeyframeAnimation は経過時間の配列とそれに対応する値の配列を与えることで、その途中経過をよしなに補完したAnimationを作成してくれます。

例) 公式Documentのサンプル

let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorAnimation.values = [UIColor.red.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor]
colorAnimation.keyTimes = [0, 0.5, 1]
colorAnimation.duration = 2

値や経過時間の差分に変化をもたせることで、CABasicAnimationより自由度の高いAnimationを作成することができます。

今回は正弦関数を使って position.x の値を変則的に動かすことでゆるやかなカーブを描かせていきます。

ゆるやかな往復運動の実装

あらかじめAnimationの duration(時間の長さ) と frameCount(コマ数) を指定しておきます。

そして horizontalAnimation という名前で宣言しておきます。

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.duration = duration

keyTimes(値を指定する先の経過時間)を設定する

今回 keyTimes に関しては均等に与えたいので duration をframeCountで均等に割った経過時間を指定します。

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

// let duration = 3.0
// let frameCount = 30
// let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
// horizontalAnimation.duration = duration

horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

values(keyTimesに対応する値の組み合わせ)を設定する

ここで正弦関数を用いて動きに遊びをもたせます

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

// let duration = 3.0
// let frameCount = 30
// let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
// horizontalAnimation.duration = duration

// horizontalAnimation.keyTimes = (0...frameCount).map({
//    let dividedTime = Double($0) * duration / Double(frameCount)
//    return NSNumber(value: Double(dividedTime))
// })

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

完成形

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.duration = duration

horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

heartView.layer.add(horizontalAnimation, forKey: nil)

https://gyazo.com/d3f70babd6d6e27fad5bf7049a34b47b

いい感じでゆらゆらしてます!

完成!

今までの2つの Animation と組み合わせて再生してみましょう。

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
        
let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

let animationGroup = CAAnimationGroup()
animationGroup.duration = duration
animationGroup.animations = [opacityAnimation, verticalAnimation, horizontalAnimation]
heartView.layer.add(animationGroup, forKey: nil)

https://gyazo.com/07a1abac976314c59d1092b4ab451003

素敵なAnimationが完成しました!!

まとめ

記事書いてて思ったけど "hyperbolic" ってカタカナで ハイパボリック って書いた瞬間急にダサいよね

Firebase Authentication の idToken をサーバーの認証に使い自サービスのUserと紐づけた話(iOS編)

これは iOS Advent Calendar 2017 の17日目の記事です。

はじめに

はじめまして、Flatt という会社でエンジニアをしている machio(まちお) と申します。iOSはまだ書き始めて半年もたたないひよっこですが、これくらいのプレッシャーでもないと勉強しないだろうということで今回エントリーしました。

現在 PinQul というライブショッピングのアプリを運営しています。作り始めた時はiOS初学だった僕ですが、最高のメンターさんと今をときめくFirebaseの力のおかげでこのアプリを2ヶ月で仕上げることができました。

タイトルの通りPinQulでは "Firebase Authentication の idToken をサーバーの認証に使い自サービスのUserと紐づけて使用" しています。

このケースに関する記事は僕の知る限りこの世にあまり存在しないので、Firebaseへの恩返しの意味もこめて、この認証のiOS側の実装の方法とそこで苦労したことについて書こうと思います(Firebaseのcalendarでやれ)。

Firebase is 何?

http://www.tristatetechnology.com/blog/wp-content/uploads/2016/09/firebase11.jpg

Firebase は俗にいう mBaaS(Mobile Backend as a Service)の1つです。2014年にGoogleに買収されて一躍有名になりました。詳細は以下に素晴らしいQiitaの記事を添付して割愛させていただきます。

qiita.com

正直 使い勝手が良すぎる & なんでもできてしまう のでもう本格的にサーバーレスで生きていけるなと思ってしまうほど素敵なサービスです(唯一欠点を挙げるとすれば日本語の公式リファレンスが古いのでサンプルコードがたまにアレです)。

Firebase Authenticationとは

公式のリファレンス によると

Firebase Authentication には、バックエンド サービス、使いやすい SDK、アプリでのユーザー認証に使用できる UI ライブラリが用意されています。Firebase Authentication では、パスワード、電話番号、一般的なフェデレーション ID プロバイダ(Google、Facebook、Twitter)などを使用した認証を行うことができます。

です。いろんなログイン方法が簡単に実装できるだけではなく、それらでログインしたUserをFirebaseのsdkを通すだけで一元に管理できるのでとても素敵です。

f:id:k0mach1:20171217015956p:plain

そしてその方法で認証したUserには idToken なるものが発行されます。これは他のFirebaseのサービスへのアクセスに使用するというのがスタンダードな使い方だと思うのですが、公式のsdkを使ってあれこれするだけで自サービスのサーバーサイドの認証にも使えます

実際の実装

手順としては

  1. Firebaseを通してログイン完了
  2. クライアントでidTokenを取得
  3. それに該当するJWT(JSON Web Token)をリクエストにのせて送信
  4. サーバー側でそれを検証することで認証

という感じです。1の実装に関しては 公式リファレンスに詳しく書いてある & すでに世にたくさん記事があるので割愛したいと思います。

1さえ超えてしまえばclientでやらなければいけないことはとても単純です。idTokenはとても簡単に取得することができます。

import Firebase

Auth.auth().currentUser?.getIDToken { idToken, error in
    if let error = error {
        // エラーハンドリング
        return
    }

    if let idToken = idToken {
        // idTokenが使える
    }
}

そしてリクエストのheaderにtokenを配置します。PinQulではHTTPクライアントに @ishkawa さんの APIKit を使わせていただいているので、以下のような感じになります。

import APIKit

struct HogeRequest: APIKit.Request {

    var idToken: Stringvar headerFields: [String: String] {
        return ["Authorization": "Bearer \(idToken)"]
    }

    …
}

Auth.auth().currentUser?.getIDToken に渡すコールバックの中でhogehogeしたり、RxつかったりでRequestにidTokenをつけることができれば、あとは送るだけです。とんでもなく簡単です。

あとはこのidTokenを検証した後decodeしてuuidなどの情報を得ます。

単に実装するだけならばこれだけなのですが、PinQulでは一点めんどくさいところがあったので最後にそれに触れて終わりたいと思います。

ログインが必要な機能とそうでない機能がある。。。

これは少しめんどくさかったです。世の中のライブストリーミングのサービスのほとんどがライブを見るだけであればログインをスキップできるようになっています。PinQulも御多分に洩れずそういった実装にしているので認証(idToken)が必要な場合とそうでない場合があります。

なのでリクエストを飛ばす時にそのリクエストに認証が必要かを判断する必要があります。なのでその処理に特化したprotocolを用意して、各Requestにそれを継承させ、認証がいるかいらないかの判断をジェネリクスを使って行わせることにしました。

import APIKit

protocol PinQulRequestProtocol: APIKit.Request {

    …

    var needLogin: Bool { get set }
    var idToken: String? { get set }

    …

}

struct HogeRequest: PinQulRequestProtocol {

    var needLogin
    var idToken: String?
    …

    var headerFields: [String: String] {
        if needLogin,
            let idToken = idToken {
                return ["Authorization": "Bearer \(idToken)"]
        }
    }

    …
}

実際に認証が必要かを判断してリクエストを送る処理は APIKit の Session にextensionを貼って実装しています。

import APIKit

extension Session {
    class func sendPinQulRequest<T: PinQulRequestProtocol>(pinQulRequest: T, handler: @escaping (Result<T.Response, SessionTaskError>) -> Void) {
        if pinQulRequest.needLogin {
            Auth.auth().currentUser?.getIDToken { idToken, error in
                if let error = error {
                    // エラーハンドリング
                    return
                }

                var pinQulRequest = pinQulRequest
                pinQulRequest.idToken = idToken
                self.send(pinQulRequest, handler: handler)
            }
        } else {
            self.send(pinQulRequest, handler: handler)
        }
        
    }
}

これでiOS側の実装は完璧です!

あとはサーバー側でこれを検証して、正常に認証できたらdecodeしてデータを取り出しましょう。 公式のリファレンスに詳細に書いてあるので問題ないと思います。

ID トークンを確認する  |  Firebase

おわりに

Xcodeも今ではとても使いやすくなっていますし、Firebaseなんていうとても素敵なサービスが現れてアプリの開発のハードルがとても下がり、僕のような初学者でもゴリゴリアプリが作れるようになりました。

この記事で利益を被るのは本当にごくわずかな人だと思いますが、最後まで拙い文章をよんでいただきありがとうございます。まだまだ本当に力不足なのでマサカリ大歓迎です。バンバンお願いします!

PinQulは以下のURLからインストールできます。ぜひ手にとって使って見てください!

「PinQul(ピンクル)」をApp Storeで

多人数でのiOS開発におけるStoryboardとの向き合い方(1画面1Storyboard)

※僕が試行錯誤したストーリーが前半を占めてるので興味がなければ読み飛ばしてください

Storyboardと付き合う

StoryboardやAutoLayoutの出現によって、iOSアプリケーション開発に置けるViewの実装のハードルは確実に低くなりました。弊社のプロダクトでも惜しみなくStoryboardを使用しています。

しかし多くの方がStoryboardとの向き合い方について頭を悩まされてきたと思います。

例として以下のCookPadさんの記事では

1つのStoryboardにViewControllerを複数配置して、Segueで画面遷移を実装(しかしViewは再利用したいのでxibファイルの形式で切り分ける)

という手法が取られています。

techlife.cookpad.com

一瞬でこれだ!と思い、採用させていただきました。似たようなUIを何度も再利用していくアプリとの親和性は相当高い思います。

この設計で一生開発していくんだと思っていました。最初は、、、

Storyboardの闇に触れる

正直1人で開発を進めていくのであれば、1Storyboardに全て詰め込んでしまうのが一番シンプルで楽かなと思います。

しかし複数人開発になった瞬間にそういうわけにはいきません。 恐るべきことに異なるbranchで同じStoryboardを触るとほぼ確実にconflictが起きます

しかもこのconflictが厄介で、conflictが発生した瞬間そのファイルはxcodeで開くことができなくなり、コードを見て愚直に1つ1つ問題を解消するしかなくなります。それも実力不足の僕の場合高確率で失敗するので、泣く泣く片方の変更を捨てることもしばしばです。

PRをだすたびに赤く染まるGithubの画面。「いっそのことStoryboardなんて使わなければよかった」、そんな考えが頭をよぎりました。

1画面1Storyboardという選択

そんなある日、某グルメサービスのiOSエンジニアさんとお話する機会があり、この悩みをぶつけたところ、「弊社は1画面につき1Storyboardでやってますね」という答えが帰って来ました。

確かに画面ごとにファイルを分けて画面ごとにタスクを分担すれば、まずconflictは起こりません。最初は「何を言ってるんだ、Segueを殺して何が楽しい」と憤ってた僕ですが、これが最高でした。

以下で詳しく説明します。

具体的な方法

別のStoryboardのViewControllerに移動するのは実は大して難しくありません。

let storyboard = UIStoryboard(name: "Hoge", bundle: nil) // storyboardのインスタンスを名前指定で取得
let nextVC = storyboard.instantiateInitialViewController() as! UIViewController // storyboard内で"is initial"に指定されているViewControllerを取得
self.present(ViewControllerVC, animated: true, completion: nil) // presentする

みたいに3行でいけます。基本的には

  • 1ViewControllerにつき1Storyboardを用意してあげる
  • 各Storyboardに適切な名前をつける
  • 上記のコードで画面遷移!

これで大丈夫でしょう。ですがStoryboard経由でViewControllerを取得してる感が鬱陶しいので、弊社ではもう少し精錬して使用しています。

以下の3STEPです。

1. 以下のようなprotocolを定義します。

protocol StoryboardInstantiable {
    static var storyboardName: String { get }
    static var bundle: Bundle? { get }
}

extension StoryboardInstantiable where Self: UIViewController {
    static var bundle: Bundle? {
        return nil
    }
    static func instantiate() -> Self {
        let storyboard = UIStoryboard(name: storyboardName, bundle: bundle)
        return storyboard.instantiateInitialViewController() as! Self
    }
}

2. これを各ViewControllerに継承させる

storyboardNameをそれぞれで定義しましょう。

class HogeViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 諸々の処理
}

extension HogeViewContoller: StoryboardInstantiable {
    static var storyboardName: String {
        return "Hoge"
    }
}

3. 遷移のコードは2行に

let nextVC = HogeViewController.instantiate() // これだけでStoryboardに紐づいたHogeViewControllerを取得
self.present(ViewControllerVC, animated: true, completion: nil) // presentする

かなりシュッとしたと思います!ViewControllerベースで次の画面取得してる感が好きです。

終わりに

このやり方はあくまで一例であって、他にも色々な設計があり、それぞれが長所・短所を持っていると思います。その開発の環境に応じた最適なものを採用するのが一番ですが、多人数開発をするとき、この手法をチラッと思い出していただけると本望です。

React Nativeでinline要素を実装する方法

React Nativeのstyleにはdisplay属性が存在しない

HTML / CSSならまずspanタグを使えばいいし、最悪 “display: inline” で無理やりinline要素にしてしまえばいいわけですが、React NativeのStyleSheetには生憎displayを指定できる要素がない。

なのでflexを使用してinline要素を自分で実装する必要がある。

解決策

具体的には以下のコードを横並びにしたいコンポーネントの親に追加する。

//inline要素にしたいものの親コンポーネント
contentsWrapper: {
    flexWrap: 'wrap', // flexコンテナからはみ出した時、改行して表示されるようにする
    alignItems: 'flex-start', // flexアイテムを左詰めにする
    flexDirection:'row' // flexアイテムの並ぶ向きをHorizontalにする
}

これでinline要素のような挙動を実装できます。