iQONのiOSアプリはまだ全てObjective-Cで記述されています。 Swiftへの移行については「たいしてパフォーマンスが上がるわけでもないし…」と思って渋っていました。 そんな中、オフィスの移転をきっかけに来客の受付システムをiPadアプリで作ることになりました。 スクラッチでアプリを作るのならSwiftで、ということでSwiftで作りました。 今回は、受付システムの社員を呼び出すデータ通信と、トップページの時計に使ったCADisplayLink実装を紹介します。
完成品
呼び出したい社員を選択すると、twilioから各個人の携帯電話に自動音声の電話がかかってきます。 電話呼び出しと同時にSlackにも通知が飛ぶようになっています。
実装
データの流れは下記のようになっています。 アプリからはherokuのAPIをリクエストするだけなので、エラーハンドリングも簡単に済みました。
データ通信
iPadから呼び出したい社員を選択すると、アプリからHerokuのAPIにリクエスト
HerokuのAPIがSlackとtwilioにリクエスト
社員のケータイに電話(自動音声)と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 } }
secondLayer.speed = 0.0 CALayer
のspeedを0にして、自動でアニメーションが動かないようにするdisplayLink.frameInterval = 1.0
1フレームごとに処理を実行する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で書いていこうと思います。