Xcodeの勉強の手始めに、ストップウォッチを作ってみるところから始め、おおよそ完成しましたので共有します。
いきさつ
基本的なストップウォッチの作り方はニコニコ動画で学習しました。で、大体のストップウォッチにはスタートボタンとストップボタンとリセットボタンとが別々にあり、なんかもっと直感的に操作できてもいいんじゃないかな?(例えば表示をタップすればスタート/ストップ)と思いましたのでこの改良に昼夜をそそいで参りました。
画面全体のどこか(ViewController.view)をタップすればスタート/ストップ、というのはその日のうちにすぐできました。
で、それでは不満でしたのでストップウォッチ機能を持つUILbel
を継承して作り、このあたりとこのあたりを参考に、触れるUILabel
の継承クラスに改良し、このあたりとこのあたりを参考にデリゲートを実装してラップ機能を持たせることに成功しました。
コード
共有用にStoryBoardを使わないコーディングにしました。
import UIKit
protocol CountNumDelegate: class {
func rapDelegate(lastRap: Int) -> ViewController
}
class TimerView :UILabel {
var timerOn = false
var nsTimer = NSTimer()
var countNum :Int
var lastRap = 0
weak var delegate :CountNumDelegate!
func update() {
countNum++
self.text = timeFormat(countNum)
}
func timeFormat(var num :Int)-> String {
// if num < 0 { num = 0 }
let ms = num % 100
let s = (num - ms) / 100 % 60
let m = (num - s - ms) / 6000 % 3600
return String(format: "%02d:%02d.%02d", arguments: [m,s,ms])
}
override init(frame :CGRect) {
countNum = lastRap
super.init(frame: frame)
self.userInteractionEnabled = true // 地味に必須
let tap = UITapGestureRecognizer()
let swipeRight = UISwipeGestureRecognizer()
swipeRight.direction = UISwipeGestureRecognizerDirection.Right
let swipeDown = UISwipeGestureRecognizer()
swipeDown.direction = UISwipeGestureRecognizerDirection.Down
tap.addTarget(self, action: "startAndStop")
swipeRight.addTarget(self, action: "reset")
swipeDown.addTarget(self, action: "rap")
self.addGestureRecognizer(tap)
self.addGestureRecognizer(swipeRight)
self.addGestureRecognizer(swipeDown)
self.text = timeFormat(countNum)
self.backgroundColor = UIColor.redColor()
self.font = UIFont(name: "Symbol", size: 60.0)
self.textAlignment = NSTextAlignment.Center
self.baselineAdjustment = UIBaselineAdjustment.AlignCenters
// self.layer.cornerRadius = 5 // ?
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func startAndStop() {
if timerOn == false {
nsTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("update"), userInfo: nil, repeats: true)
timerOn = true
self.backgroundColor = UIColor.greenColor()
NSLog("Tap to start")
} else {
nsTimer.invalidate()
timerOn = false
self.backgroundColor = UIColor.redColor()
NSLog("Tap to stop")
}
}
func reset() {
if timerOn == false {
countNum = 0
lastRap = 0
self.text = timeFormat(countNum)
NSLog("Swiped Right to reset")
}
}
func rap() {
lastRap = countNum - lastRap
NSLog("Swiped Down at count \(timeFormat(lastRap))")
if timerOn == true {
delegate.rapDelegate(countNum).draw(6) // ViewControllerのメソッドを呼ぶ
}
}
}
class ViewController: UIViewController, CountNumDelegate {
var timerLabel = [
TimerView(frame: CGRectMake(0, 0, 250, 80)),
TimerView(frame: CGRectMake(0, 0, 250, 80)),
// TimerView(frame: CGRectMake(0, 0, 250, 80)),
]
func rapDelegate(countNum :Int) -> ViewController {
let rapped = TimerView(frame: CGRectMake(0, 0, 250, 80))
rapped.userInteractionEnabled = false // 再稼働禁止
rapped.lastRap = countNum
timerLabel.insert(rapped, atIndex: 1)
rapped.countNum = countNum - timerLabel[2].lastRap
let t = timerLabel[1]
t.lastRap = countNum
t.text = rapped.timeFormat(rapped.countNum)
NSLog("\(t.timeFormat(t.lastRap))")
return self
}
func draw(max: Int) {
// 既存のビューを一度すべて消す
let subviews = self.view.subviews as [UIView]
for v in subviews {
if let timerView = v as? TimerView {
timerView.removeFromSuperview()
}
}
// 並べ直し
var num = 0
for t in timerLabel {
t.center = CGPointMake(self.view.bounds.width / 2, self.view.bounds.height * CGFloat(++num) / 7)
if num <= max {
self.view.addSubview(t)
}
}
}
override func viewDidLayoutSubviews() {
draw(6)
}
override func viewDidLoad() {
super.viewDidLoad()
self.timerLabel[0].delegate = self
self.draw(1)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
特徴
すべてのアクションはジェスチャーで解決します。スタート/ストップはタップで、ストップ中に右スワイプでリセット、起動中に下スワイプでラップを表示します。
苦労した点
ストップウォッチを作ってることを周囲に見せると必ずと言っていいほどラップに言及されるのですが、下スワイプを認識してラップを表示する機能をつけるというのが大変でした。デリゲーションに対する理解がイマイチでoverride func viewDidLoad() {}
にself.timerLabel[0].delegate = self
とするところに気がつかず、TimerView.delegate
にnil
が入っててデリゲーション部分の実装を呼び出せないバグに苦心しました。
既知のバグ
* 一度リセットした後のラップにマイナスの値が入る
参考文献
* UIButtonを動的に作るサンプルコード
* [Objective-C] UIViewをいい感じに上下左右センタリングする
* プロトコルとデリゲートのとても簡単なサンプルについて
* [Swift] 自作UILabelにDelegateでタッチイベントをつける