Edited at

Swiftで受付システムを作った話とCADisplayLink

More than 3 years have passed since last update.


この記事はVASILY DEVELOPERS BLOGにも同じ内容で投稿しています。よろしければ他の記事も御覧ください。


iQONのiOSアプリはまだ全てObjective-Cで記述されています。

Swiftへの移行については「たいしてパフォーマンスが上がるわけでもないし…」と思って渋っていました。

そんな中、オフィスの移転をきっかけに来客の受付システムをiPadアプリで作ることになりました。

スクラッチでアプリを作るのならSwiftで、ということでSwiftで作りました。

今回は、受付システムの社員を呼び出すデータ通信と、トップページの時計に使ったCADisplayLink実装を紹介します。


完成品 (動画)

まずは動画をご覧ください。

呼び出したい社員を選択すると、twilio から各個人の携帯電話に自動音声の電話がかかってきます。

電話呼び出しと同時にSlackにも通知が飛ぶようになっています。


実装

データの流れは下記のようになっています。

アプリからはherokuのAPIをリクエストするだけなので、エラーハンドリングも簡単に済みました。


データ通信

dataflow.jpg


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

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

  3. 社員のケータイに電話(自動音声)と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にサンプルプロジェクトを置いてあるので動かしてみてください。

display_link_sample.gif


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 // ※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

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で扱えるようになるのは、0.36以降の予定です。正式リリースが待たれます



メリットの方が大きいと思ったので日々のコードも新しいものからSwiftで書いていこうと思います。