34905


アナログ時計を作ろうとしています。長針短針秒針を生成し現在時刻を元にその角度を調整するところまではできたのですが、毎秒ごとに更新されず止まったままの時計になってしまっています。どうしたら本来の時計らしく毎秒毎秒秒針が動いて正確な時間を知らせてくれるようになるでしょうか。  
以下親クラスのコードです。

[code]

    import UIKit
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
        }
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    }
    
[/code]

以下サブクラスのコードです。

[code]

    import UIKit
    
    class Line: UIView {
        var sHand = UIBezierPath()
        var mHand = UIBezierPath()
        var hHand = UIBezierPath()
    
    
        func setTheClock()
        {
            let cal = Calendar(identifier: .gregorian)
            let now = Date()
    
            let ss = Double(cal.component(.second, from: now))
            let mm = Double(cal.component(.minute, from: now))
            let hh = (Double(cal.component(.hour, from: now)) + mm/60).truncatingRemainder(dividingBy: 12)
    
            let sRad = .pi*ss/30
            let mRad = .pi*mm/30
            let hRad = .pi*hh/6
    
    
            sHand.move(to: CGPoint(x: 100, y: 100))
            sHand.addLine(to: CGPoint(x: 100 + 100*sin(sRad), y: 100 - 100*cos(sRad)))
            UIColor.yellow.setStroke()
            sHand.lineWidth = 0.5
            sHand.stroke()
    
            mHand.move(to: CGPoint(x: 100, y: 100))
            mHand.addLine(to: CGPoint(x: 100 + 80*sin(mRad), y: 100 - 80*cos(mRad)))
            UIColor.blue.setStroke()
            mHand.lineWidth = 1
            mHand.stroke()
    
            hHand.move(to: CGPoint(x: 100, y: 100))
            hHand.addLine(to: CGPoint(x: 100 + 60*sin(hRad), y: 100 - 60*cos(hRad)))
            UIColor.red.setStroke()
            hHand.lineWidth = 2
            hHand.stroke()
        }
    
    
    
        override func draw(_ rect: CGRect)
        {
            setTheClock()
            _ = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(setTheClock), userInfo: nil, repeats: true)
        }
    
    }
    
[/code]


CC BY-SA 3.0
2017-05-21T14:49:15.573
34903
2017-05-21T21:34:55.367
20671
question
swift xcode
アナログ時計が毎秒ごとに更新されない


あなたのコードの中でうまく動かない原因は2つあります。

  * タイマーで1秒ごとに`setTheClock()`を呼び出している
  * `sHand`, `mHand`, `hHand`をインスタンス変数にしてしまっている

* * *

まずは1点目ですが、あなたが`setTheClock()`のようなスタイルで描画用の命令を並べた場合、それが正しく画面に反映されるのは適切に描画用のコンテキストを設定してある場合のみです。ご自身のコードをXcodeで実行したら、何やらエラーメッセージが1秒ごとにデバッグコンソールに表示されたりはしないでしょうか。

> `May 22 01:36:46 AnalogClock[69711]
> <Error>:CGContextSetStrokeColorWithColor: invalid context 0x0. If you want
> to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental
> variable.`  
>  (以下同様のメッセージが同じ1秒間にずらりと表示される。)

iOSが`draw(_:)`メソッドを呼ぶ場合には、上記の「適切に描画用のコンテキストを設定」をやってくれるので、最初の1回、つまり`draw(_:)`の中から呼ばれた時には`setTheClock()`は正しく動作するでしょうが、それ以降はずっとタイマー処理の中から直接`setTheClock()`が呼ばれますので、それらの中で実行された描画用の命令に関しては「(描画用の)コンテキストがおかしい」と言う上記のようなエラーになってしまいます。

もちろん自前で`draw(_:)`を呼ぶのはいろいろなドキュメントに書いてあるように禁止です。

ここは発想を変えて、 **1秒ごとに描画用の命令を自前で呼び出すのではなく、1秒ごとにiOSが`draw(_:)`を呼び出すように仕向ける**
ようにしないといけません。

変更点を最小にしようと思うと、こんなコードができるのですが、

[code]

    //`Timer`で起動されるメソッドは`Timer`型の引数を1個取らないといけない
    @objc func updateSelf(_: Timer) {
        //このviewの再描画が必要であることをiOSに教えてやる
        //-> 次にメインスレッドが空いた時に`draw(_:)`がiOSから呼ばれる
        self.setNeedsDisplay()
    }
    
    override func draw(_ rect: CGRect) {
        setTheClock()
        _ = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateSelf), userInfo: nil, repeats: true)
    }
    
[/code]

これだと、iOSが`draw(_:)`を呼び出すたびに新しいタイマーが作成されるので、アプリ内でどんどんアクティブなタイマーが増え続け、そのうち処理が破綻してしまいます。一般的にアプリの側から各viewの`draw(_:)`がいつ呼び出されるかを特定するのは困難ですから、
**`draw(_:)`の中では画面描画以外の処理は可能な限り何もしない** と言うのが鉄則です。

この点を踏まえると、やはりこの手のコードはviewではなく、view controllerに記述すべきでしょう。

ViewController.swift:

[code]

    import UIKit
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        //当然storyboard上で正しくIBOutletの接続を行っておくこと
        @IBOutlet weak var clockView: Line!
    
        var clockUpdateTimer: Timer? = nil
    
        @objc private func updateSelf(_: Timer) {
            self.clockView.setNeedsDisplay()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            clockUpdateTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateSelf), userInfo: nil, repeats: true)
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            //画面から消えたら必ずタイマーを無効化する
            clockUpdateTimer?.invalidate()
            clockUpdateTimer = nil
    
            super.viewDidDisappear(animated)
        }
    }
    
[/code]

Line.swift: (抜粋)

[code]

        //`updateClock(_:)`はview側には不要
    
        override func draw(_ rect: CGRect) {
            setTheClock()
            //描画以外のことは何もしない
        }
    
[/code]

ここまでの修正をしたら、2点目についての部分を読む前に一度動作確認をしておいてもらった方が良いかもしれません。

* * *

さて、2点目ですが、なぜ`UIBezierPath`をインスタンス変数にしてしまってはいけないのかと言うと、
**`UIBezierPath`は画面への描画命令を貯めておくための箱**
のようなものだからです。実際に動かしてもらえば(秒針に関しては)一目瞭然なのですが、一度`setTheClock()`が呼び出されると`sHand`が保持する`UIBezierPath`の中身は

[code]

        sHand.move(to: CGPoint(x: 100, y: 100)) //*
        sHand.addLine(to: CGPoint(x: 100 + 100*sin(sRad), y: 100 - 100*cos(sRad))) //*
        UIColor.yellow.setStroke()
        sHand.lineWidth = 0.5 //*
        sHand.stroke()
    
[/code]

で設定された内容(*)が保持されているわけですが、次にもう一度`setTheClock()`が呼び出された場合にそれらの内容が消えてしまうわけではなく、そこにさらに新しい命令が付け足されていきます。こうして何度も`setTheClock()`が呼ばれる間に`sHand`は秒針を何本も描くための描画命令を溜め込むことになってしまうわけです。

これを解消するためには、普通にローカル変数にしてしまうだけです。

Line.swift:

[code]

    import UIKit
    
    class Line: UIView {
    
        func setTheClock() {
            let cal = Calendar(identifier: .gregorian)
            let now = Date()
    
            let ss = Double(cal.component(.second, from: now))
            let mm = Double(cal.component(.minute, from: now))
            let hh = (Double(cal.component(.hour, from: now)) + mm/60).truncatingRemainder(dividingBy: 12)
    
            let sRad = .pi*ss/30
            let mRad = .pi*mm/30
            let hRad = .pi*hh/6
    
            let sHand = UIBezierPath() //<-
            sHand.move(to: CGPoint(x: 100, y: 100))
            sHand.addLine(to: CGPoint(x: 100 + 100*sin(sRad), y: 100 - 100*cos(sRad)))
            UIColor.yellow.setStroke()
            sHand.lineWidth = 0.5
            sHand.stroke()
    
            let mHand = UIBezierPath() //<-
            mHand.move(to: CGPoint(x: 100, y: 100))
            mHand.addLine(to: CGPoint(x: 100 + 80*sin(mRad), y: 100 - 80*cos(mRad)))
            UIColor.blue.setStroke()
            mHand.lineWidth = 1
            mHand.stroke()
    
            let hHand = UIBezierPath() //<-
            hHand.move(to: CGPoint(x: 100, y: 100))
            hHand.addLine(to: CGPoint(x: 100 + 60*sin(hRad), y: 100 - 60*cos(hRad)))
            UIColor.red.setStroke()
            hHand.lineWidth = 2
            hHand.stroke()
        }
    
        override func draw(_ rect: CGRect) {
            setTheClock()
        }
    
    }
    
[/code]

一般的には、ローカル変数の方が最適化が効きやすいなどいろいろなメリットがありますから、必要ない限りインスタンス変数は増やさない方が良いと思った方が良いでしょう。

* * *

とりあえず、上記2点を修正すれば、毎秒アナログ時計画面が更新されると言う目的は達成できるかと思います。お試しください。


CC BY-SA 3.0
2017-05-21T17:36:42.450
34905
2017-05-21T17:36:42.450
13972
34903
answer


すでにプログラムコードに関する回答がついていますが、実践的な面から見るとすると、時計の針を回転させるのは、座標変換（`CGAffineTransform`）を使うのが、スタンダードな手法です。

[Apple Developer - API Reference - Core Graphics -
CGAffineTransform](https://developer.apple.com/reference/coregraphics/cgaffinetransform-
rb5)

[code]

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        arrow.transform = arrow.transform.rotated(by: CGFloat.pi / 6.0)
    }
    
[/code]

上のコードは、`arrow`という`UIView`のサブクラスを、画面タッチするたび、60度ずつ回転します。`UIView`のメソッド`draw(_
rect:)`を使わずに回転ができる点に注目してください。これに、`UIView`のアニメーションメソッドを組み合わせるといいでしょう。


CC BY-SA 3.0
2017-05-21T21:34:55.367
34906
2017-05-21T21:34:55.367
18540
34903
answer