【Swift】TimerクラスのscheduledTimerメソッドでアニメーションの使い方

2018年7月31日

テックキャンプでSwift/Xcodeの学習をしていますがLesson4から難しめになってきたように思います。というのもこの回からアニメーションをやります。

アニメーションは画面上のオブジェクトを時間とともに描写位置を変えていくことで実現します。

そのためのクラスがTimerでメソッドはscheduledTimerです。今日はこれらのクラスについてまとめるとともに自分が作りたいアプリ(Peek-a-booアプリ)の前進になるようなテストアプリを作っていこうと思います。

scheduledTimerを使う前のオブジェクトの配置

ボタンを2個用意して、「いないいない」「ばあ」をニョキッと表示させるだけのアプリです。

まずはXcodeで新規プロジェクトの作成です。プロジェクト名は「myTimerTest」です。

今回はスマホの画面を横にします。画面下部にあるOrientationのところで横向きスマホをクリクして横にします。

ボタンを2個用意します。画面右下のオブジェクトライブラリからButtonを2個、storyboard上に配置しました。またボタン文字を記述しました。

このボタン2個をAction接続でソースコードと接続します。

まずはAssistant editorボタンをクリックして画面右側にソースコードを表示し
ボタン2個ともソースコード上にcontrolを押しながらドラッグして接続。

これでButtonをタップしたときに起こすメソッドが定義できました。

@IBAction func tapBtn1と
@IBAction func tapBtn2がメソッドです。

ここにそれぞれのボタンが押されたときの処理を記述していきます。

この時点でシミュレーターを起動してみます。シミュレーターのスマホは縦になっているので横向きにします。画面上部にあるHardware→Rotate Leftとポイントすればスマホが横向きになります。

いまはボタンが2個配置されているだけです

まずはボタン1が押された時に「いないいない」を表示し、ボタン2が押された時に「ばあ」を表示するだけの実装をします。

@IBAction func tapBtn1(_ sender: UIButton) {
    print("tap btn1")
    let label = UILabel()
    label.text = "いないいな~い"
    label.font = UIFont(name: "HiraginoSans-W6", size:40)
    label.sizeToFit()
    label.frame.origin = CGPoint(x: (self.view.frame.width/2 - label.frame.width/2), 
                                 y: (self.view.frame.height/2 - label.frame.height/2))
    self.view.addSubview(label)
}

2行目はprintでこのボタンが押されたときにこのメソッドを通過するか表示しているだけです。

3行目でUILabelのインスタンスを生成しています。「いないいな~い」はlabelで表示します。

4行目はlabe.textで文字列を設定し、fontプロパティでフォントファミリーやフォントサイズを指定しました。

5行目はフォントサイズを指定したあとにlabel.sizeToFitメソッドでラベルのサイズを文字の大きさに合わせて拡大しています。

6行目はlabel.frame.originはラベルを表示するx,y座標を指定します。ラベルの左上角です。

画面中央にラベルを表示させたいので、スマホの画面の横幅÷2からラベル幅÷2を引いた座標をXにします。

同様にy座標は、スマホの縦長÷2からラベル高さ÷2を引いた座標をYにします。これでラベルが中央に表示されます。

と、思ったらこんな面倒な計算しなくたって便利なプロパティがあったよ。

label.center.x = self.view.center.x
label.center.y = self.view.center.y

たったこれだけでいいんじゃ~~~!

最後にViewController自身(self)のview.adSubviewでラベルを乗せて表示、となります。これが基本形です。

次にボタン2が押されたときは「ばあ」のみを表示させたいので、「いないいない」のラベルは消えて欲しいワケですので、そのように実装していきます。

ラベルはViewControllerのましたに記述してどこからでも触れるようにしました。以下のようなソースコードになりました。

class ViewController: UIViewController {
    let label1 = UILabel()
    let label2 = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func tapBtn1(_ sender: UIButton) {
        print("tap btn1")
        label2.text = "" //「ばぁ」は消す
        label1.text = "いないいな~い"
        label1.font = UIFont(name: "HiraginoSans-W6", size:40)
        label1.sizeToFit()
        label1.center.x = self.view.center.x
        label1.center.x = self.view.center.y
        self.view.addSubview(label1)
    }
    @IBAction func tapBtn2(_ sender: UIButton) {
        print("tap btn2")
        label1.text = "" //「いないいない」は消す
        label2.text = "ばぁっ!"
        label2.font = UIFont(name: "HiraginoSans-W6", size:40)
        label2.sizeToFit()
        label2.center.x = self.view.center.x
        label2.center.y = self.view.center.y
        self.view.addSubview(label2)
    }

さてここからアニメーションの実装をしていきます。

scheduledTimerを使ってアニメーションを実装

btn1をタップしたら「いないいない」Labelが徐々に大きくなるようにTimerクラスのscheduledTimerメソッドを使います。

このメソッドは「アニメーションをするためのメソッド」ではありません。このクラスではタスクのスケジュールを管理し何かしらのメソッドを実行したり停止したり、設定した時間ごとに繰り返したい場合に使います。

scheduledTimerメソッドのリファレンスを見ると定義部分は以下のようになっていました。

open class func scheduledTimer(
	timeInterval ti: TimeInterval,
	target aTarget: Any,
	selector aSelector: Selector,
	userinfo: Any?,
	repeats yesOrNo: Bool) -> Timer

scheduledTimerメソッドは引数が難しいですね。

例として以下のように記述します。

Timer.scheduledTimer(
    timeInterval: 0.001, //秒
    target: self,
    selector: #selector(self.label1kakudai(_:)), //実行するメソッド
    userInfo: 1, //何かしらのパラメータ
    repeats: true //繰り返し
)

scheduledTimerメソッドの引数

引数は以下のように指定、意味があります。

第1引数 timeInterval インターバルの時間です。秒を指定する。1/1000と書けば1000分の1秒になる。例えばアニメーションしようと思って繰り返しで0.001を指定すると0.1よりなめらかに動く。
第2引数 target メソッドがどこにあるか場所を指定。selfだとViewControllerになっている。のでその中に動かしたいメソッドを書くことになります。
第3引数 selector タイマーが起動したときに実行するメソッド名を書く。 ここではself.label1kakudaiメソッドを指定しました。引数にはTimer自身を入れます。
第4引数 userInfo パラメータとして渡したい何かを記述。何型でもよくIntの1や、拡大させるLabelを渡しても良い。しかし渡した先でas!を使って型を明示する必要がある。渡すものがなければnilでもいい。
第5引数 repeats Bool型で繰り返すか否かの設定を行います。繰り返す場合はtrueで、1度だけ実行する場合はfalseにします。

このTimerで呼ばれるメソッドにlabel1kakudai()を指定しました。なのでこのメソッドを実装していきます。

例えば以下のように実装しました。

@objc func label1kakudai(_ sender: Timer){
    let sokudo = sender.userInfo as! CGFloat
    label1.font = label1.font.withSize(label1.font.pointSize + sokudo) 
    label1.sizeToFit()
    label1.center.x = self.view.center.x
    label1.center.y = self.view.center.y
    if label1.font.pointSize >= 50 {  sender.invalidate()  }
}

2行目でuserInfoをCGFloat型で取り出してsokudoという定数に入れています。
これはuserInfoで拡大する文字のスピード(フォントサイズにいくつ足すか)を渡すようにした例です。

label1kakudaiメソッドの引数はTimer自身が入ってきてこのメソッド内ではsenderという変数名で使います。

label1.font = label1.font.withSize(label1.font.pointSize + sokudo)

は現在のLabel1のフォントサイズにsokudoという定数(1)を足して、現在のLabelのフォントサイズに再設定しています。つまりフォントサイズを1大きくしているわけです。

最後にif文でフォントサイズが50位上になったら、Timerを停止するようにsender.invalidateを呼んでいます。

例2
もしuserInfoにLabel1を渡すならこのように記述。Label1はViewControllerクラスの変数なので渡さなくても使えるけど、渡し方のサンプルとしてメモした。

class ViewController: UIViewController {
    let label1 = UILabel()
    let label2 = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func tapBtn1(_ sender: UIButton) {
        print("tap btn1")
        label2.text = ""
        label1.text = "いないいな~い"
        label1.font = UIFont(name: "HiraginoSans-W6", size:10)
        label1.sizeToFit()
        label1.center.x = self.view.center.x
        label1.center.y = self.view.center.y
        self.view.addSubview(label1)
 
        Timer.scheduledTimer(
            timeInterval: 0.01,
            target: self,
            selector: #selector(self.kakudai(_:)),
            userInfo: label1, //ここでlabelをわたしてみる
            repeats: true
        )
    }
    
    //userInfoにLabelを渡した場合のメソッド
    @objc func kakudai(_ sender: Timer){
        let tmpLabel = sender.userInfo as! UILabel //userInfoはUILabelとして設定
        tmpLabel.font = tmpLabel.font.withSize(tmpLabel.font.pointSize + 1) //フォントサイズは+1固定にした
        tmpLabel.sizeToFit()
        tmpLabel.center.x = self.view.center.x
        tmpLabel.center.y = self.view.center.y
        if tmpLabel.font.pointSize >= 50 {sender.invalidate()}
    }

しかしuserInfoに2つの値をもたせるようなことはできないのだろうか。
たしか、swifって型の違う値を配列に入れて渡せたような気がするが。

「いないいない」と「ばぁ」をそれぞれ拡大するようにソースコードを編集した。全体的なソースは以下のようになった。

import UIKit

class ViewController: UIViewController {
    let label1 = UILabel()
    let label2 = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    @IBAction func tapBtn1(_ sender: UIButton) {
        print("tap btn1")
        label2.text = ""
        label1.text = "いないいな〜い"
        label1.font = UIFont(name: "HiraginoSans-W6", size:10)
        label1.sizeToFit()
        label1.center.x = self.view.center.x
        label1.center.y = self.view.center.y
        self.view.addSubview(label1)
 
        Timer.scheduledTimer(
            timeInterval: 0.01,
            target: self,
            selector: #selector(self.kakudai(_:)),
            userInfo: label1,
            repeats: true
        )
    }

    
    @IBAction func tapBtn2(_ sender: UIButton) {
        print("tap btn2")
        label1.text = ""
        label2.text = "ばぁっ!"
        label2.font = UIFont(name: "HiraginoSans-W6", size:10)
        label2.sizeToFit()
        label2.center.x = self.view.center.x
        label2.center.y = self.view.center.y
        self.view.addSubview(label2)
        
        Timer.scheduledTimer(
            timeInterval: 0.005,
            target: self,
            selector: #selector(self.kakudai(_:)),
            userInfo: label2,
            repeats: true
        )
    }
    
    //userInfoにLabelを渡した場合のメソッド
    @objc func kakudai(_ sender: Timer){
        let tmpLabel = sender.userInfo as! UILabel
        tmpLabel.font = tmpLabel.font.withSize(tmpLabel.font.pointSize + 1)
        tmpLabel.sizeToFit()
        tmpLabel.center.x = self.view.center.x
        tmpLabel.center.y = self.view.center.y
        if tmpLabel.font.pointSize >= 50 {sender.invalidate()}
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

btn1を押したときはlabel1に「いないいない」を設定しTimerを0.001秒ごとに起動、label1のフォントサイズを1ずつ拡大。
btn2を押したときはlabel2に「ばぁ」を設定しTimerを0.005秒ごとに起動、label2のフォントサイズを1ずつ拡大。

これで以下のような動きを実現することができました。

これでscheduledTimerメソッドの使い方がわかりました。

peek-a-boo!