Swiftで会社の受付アプリを作った話とCADisplayLink

f:id:vasilyjp:20180927112657j:plain

iQONのiOSアプリはまだ全てObjective-Cで記述されています。 Swiftへの移行については「たいしてパフォーマンスが上がるわけでもないし…」と思って渋っていました。 そんな中、オフィスの移転をきっかけに来客の受付システムをiPadアプリで作ることになりました。 スクラッチでアプリを作るのならSwiftで、ということでSwiftで作りました。 今回は、受付システムの社員を呼び出すデータ通信と、トップページの時計に使ったCADisplayLink実装を紹介します。

完成品

www.youtube.com

呼び出したい社員を選択すると、twilioから各個人の携帯電話に自動音声の電話がかかってきます。 電話呼び出しと同時にSlackにも通知が飛ぶようになっています。

実装

データの流れは下記のようになっています。 アプリからはherokuのAPIをリクエストするだけなので、エラーハンドリングも簡単に済みました。

データ通信

iPadから呼び出したい社員を選択すると、アプリからHerokuのAPIにリクエスト

  1. HerokuのAPIがSlackとtwilioにリクエスト

  2. 社員のケータイに電話(自動音声)とSlackの通知が飛びます

アプリの実装

ネイティブ側では下記のようなことをやっています。

・ Swift製通信ライブラリのAlamofireを使用

・ CAGradientLayerでiPhoneのロック解除のようなシマーアニメーションを実装

・ 時計のアニメーションをCADisplayLinkを使って正確に描画

・ UICollectionViewFlowLayoutをオーバーライドして、セル追加アニメーションを実装

・ 呼び出しリクエスト中にAudioToolbox.frameworkで効果音を無限ループさせる

・ 通信中の波形アニメーションにCADisplayLinkを使ってアニメーション

・ アプリアイコンは基本的に見せないので、identiconでデザイン工数ゼロ その中の一つ、今回はCADisplayLinkの実装を紹介します。

時計のアニメーションとCADisplayLink

アナログ時計のような無限に動き続けるアニメーションを実装するとき、NSTimerやdispatch_afterを使って0.01秒ごとに位置を修正する処理を実行するような実装が考えられます。 受付システムの時計のアニメーションの実装には、CADisplayLinkを使用しています。 CADisplayLinkは画面のリフレッシュレートと同期して描画させるタイマーオブジェクトです。

vs NSTimer

NSTimerを使ったり、dispatch_afterをループしても同じような処理を実装できます。 例えば、NSTimerのインターバルを1秒に設定して、その処理の実行時間が1.5秒だった場合、処理実行中は次のタイマー処理がスキップされ、処理が実行されるのは2秒間隔になってしまいます。(いわゆるフレームスキップ) これはCADisplayLinkでも同じことです。CADisplayLinkの場合でもスキップはありますが、あくまでも呼び出されるタイミングは画面の更新に同期するのでアニメーションを使うには効率が良いのです。

CADisplayLinkの実装

CADisplayLinkオブジェクトを生成時にターゲットとメソッド名を登録します。 生成したCADisplayLinkオブジェクトをNSRunLoopのメインループに追加すると、画面描画のリフレッシュごとに登録したメソッドが呼ばれます。

let displayLink = CADisplayLink(target: self, selector: Selector("update:")) displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) 

円を描く実装

実装の一部を一つのViewControllerにまとめました。 GitHubにサンプルプロジェクトを置いてあるので動かしてみてください。 こんなアニメーションが動きます。

import UIKit

class ViewController: UIViewController {

    private let secondLayer = CAShapeLayer()

    override func viewDidLoad() {

        super.viewDidLoad() // 円のレイヤー

        let frame = view.frame let path = UIBezierPath() path.addArcWithCenter(

        CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)), radius: frame.width / 2.0 - 20.0, startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI + M_PI_2), clockwise: true)

        secondLayer.path = path.CGPath

        secondLayer.strokeColor = UIColor.blackColor().CGColor

        secondLayer.fillColor = UIColor.clearColor().CGColor secondLayer.speed = 0.0 // ※1

        view.layer.addSublayer(secondLayer) // 円を描くアニメーション

        let animation = CABasicAnimation(keyPath: "strokeEnd")

        animation.fromValue = 0.0

        animation.toValue = 1.0

        animation.duration = 60.0

        secondLayer.addAnimation(animation, forKey: "strokeCircle") // CADisplayLink設定         let displayLink = CADisplayLink(target: self, selector: Selector("update:"))      

        displayLink.frameInterval = 1.0 // ※2

        displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)

    }

    func update(displayLink: CADisplayLink) {  // timeOffsetに現在時刻の秒数を設

        let time = NSDate().timeIntervalSince1970 let seconds = floor(time) % 60

        let milliseconds = time - floor(time) secondLayer.timeOffset = seconds + milliseconds // ※3

    }

} 
  1. secondLayer.speed = 0.0 CALayerのspeedを0にして、自動でアニメーションが動かないようにする

  2. displayLink.frameInterval = 1.0 1フレームごとに処理を実行する

  3. secondLayer.timeOffset = seconds + milliseconds アニメーションの進捗具合を設定する CALayerの speed = 0.0 の状態で、timeOffsetを操作しているので、円が一周しても、また最初から描画が始まって無限に動き続けます。

Swiftで書いてみて思ったこと

メリット

  • コード量が減る   

  • .h/.m → .swift 一つになります   

  • 型推論

  • Objective-Cでの不便なことが改善   

  • 文字列の扱い、Switch、enum

  • Optional Value (?, !)  

  • ビルドせずに静的解析でバグに気付くことができます

  • iOSエンジニアに以外の可読性向上  

  • Objective-Cの独特な記法がなくなって、iOSエンジニア以外でもなんとなく理解できるぐらい読みやすくなりました

  • これがきっかけでAndroidエンジニアがplaygroundを触るようになりました

  • 普段アプリをObjective-Cで作っている人には、たいして難しくない (簡単というわけでもないですが)

  • 何より書いていて楽しい

デメリット

  • Xcodeでメソッドの定義に飛ぶショートカット(Cmd+Ctrl+J)があまり効かない  

  • optionキーを押しながら、コードをクリックして回避しています

  • ライブラリの利用が面倒   

  • Alamofireをgitのsubmoduleを使ってインストールしましたがかなり面倒でした   

  • CocoaPodsでSwiftライブラリが扱えるようになるのは、0.36以降の予定です     正式リリースが待たれます メリットの方が大きいと思ったのでiQONのコードも新しいものからSwiftで書いていこうと思います。

カテゴリー