質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.32%

画面遷移ができません。カウントダウンアプリを作成しています。

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 282

前提・実現したいこと

初心者です。
・最初の画面(ViewController)でpickerを使用して時間を選択してもらう
・開始のボタンを押して画面遷移をさせる
・遷移先(NextViewController)で「00:00:00」のようにlabelに時間を表示させ、カウントダウンを進める

上記のようなアプリを作ろうと思っています。

エラーは出ていないのですが、画面遷移ができず、開始ボタンを押すとそのまま固まってしまいます。
画面遷移のやり方を教えていただきたいです。
また、値の渡し方がこれで合っているのかもわかりません。そのことに関してもアドバイスが欲しいです。

初心者で何もわからず、根本的なところが間違っているかもしれないのですが、それがどこだかわからないです。どなたか分かる方いらっしゃいましたら、よろしくお願いします。

該当のソースコード

import UIKit


class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource{

    @IBOutlet weak var start: UIButton!
    @IBOutlet weak var time: UIPickerView!

    let data = [[Int](arrayLiteral: 15,30,45,60,90,120)]

    var pickerdata:String!
    var timeTotal:Int!



    @IBAction func start(_ sender: Any) {

        let storyboard: UIStoryboard = self.storyboard!
        let timer = storyboard.instantiateViewController(withIdentifier: "toNextViewController") as! NextViewController

        timer.getTime = timeTotal

        self.present(timer, animated: true, completion: nil)

        total()

        self.performSegue(withIdentifier: "toNextViewController", sender: nil)

    }


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        time.dataSource = self
        time.delegate = self
    }




    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return data.count
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return data[component].count
    }


    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return String(data[component][row])
    }

        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            total()
            }

    //    pickerの数字をたすメソッド
        func total() {
            timeTotal = data[0][time.selectedRow(inComponent: 0)] * 60
        }

}
import UIKit

class NextViewController: UIViewController {

    var timer = Timer()
    var count = 0
    var getTime:Int!

    @IBOutlet weak var timerLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        runTimer()
        timer.invalidate()
        count = 0
        timerLabel.text = timeString(time: TimeInterval(getTime))
    }


        // カウントダウンをする関数
        @objc func updateTimer() -> Int {

            count += 1
            //時間 = pickerで設定する時間 ー count
            let remainCount = getTime! - count
            timerLabel.text = timeString(time: TimeInterval(remainCount))

    //            timerLabel.text = "\(remainCount)"

    //        0秒になったら止まる
            if remainCount == 0 {
                timer.invalidate()
                timerLabel.text = "00:00:00"
            }
                return remainCount

        }

        //タイマーを動かす関数
        func runTimer() {
            timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTimer), userInfo: nil, repeats: true)
        }

        //00:00:00に変える処理
          func timeString(time: TimeInterval) -> String {
              let hour = Int(time) / 3600
              let minutes = Int(time) / 60 % 60
              let second = Int(time) % 60

              return String(format: "%02d:%02d:%02d", hour, minutes, second)
          }


        override func viewDidAppear(_ animated: Bool) {

            timerLabel.text = timeString(time: TimeInterval(getTime))
            print(getTime ?? "this is nil")

        }

}

試したこと

segueに問題があるのかと思い、開始ボタンからsegueをつけたり、ViewControllerからsegueをつけたりしているのですができませんでした。現在はViewControllerから関連づけています。

補足情報(FW/ツールのバージョンなど)

Swift5でコーディングしています。
こちらのサイトを参考にして作成しました。
https://qiita.com/katsualonso14/items/bf64e2201e8fc73125e0

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

エラーは出ていないのですが、画面遷移ができず、開始ボタンを押すとそのまま固まってしまいます。

直接的な原因は、NextViewControllerで

    override func viewDidLoad() {
        super.viewDidLoad()
        runTimer()
        timer.invalidate() // <-ココが問題
        count = 0
        timerLabel.text = timeString(time: TimeInterval(getTime))
    }


という具合に、タイマーをinvalidate(無効にする、という意味)しているからだと思います。

画面遷移のやり方を教えていただきたいです。

提示していただいたソースを見ると、

       let storyboard: UIStoryboard = self.storyboard!
        let timer = storyboard.instantiateViewController(withIdentifier: "toNextViewController") as! NextViewController

        timer.getTime = timeTotal

        self.present(timer, animated: true, completion: nil)

        total()

        self.performSegue(withIdentifier: "toNextViewController", sender: nil)


とありますが、

  • StoryBoardで作成したViewControllerのインスタンス化
  • Segueでの画面遷移

の2つを混同して使われているように思えます。

元記事ではSegueを使っていませんので、前者に統一するのがいいかと思います。

また、値の渡し方がこれで合っているのかもわかりません。そのことに関してもアドバイスが欲しいです。

  • StoryBoardで作成したViewControllerのインスタンス化

で値渡しをするのであれば、この方法で問題ないかと思います。Segueを使う場合にはまた異なってきます。

その他、配列の初期化方法や、PickerViewに渡すデータの扱い方など、先々問題に繋がりそうな部分もありましたので、コメントを入れてみました。

もしわからない点があればコメントいただくか、内容によっては新たにご質問いただければと思います。

import UIKit

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource{

    @IBOutlet weak var start: UIButton!
    @IBOutlet weak var time: UIPickerView!

    // MARK: 変更
    // Int(arrayLiteral: ) はコンパイラが使う内部向け表現なので使わない
    // また、今回の目的だと一次元配列で済むため、次のように書き換える
    // let data = [[Int](arrayLiteral: 15,30,45,60,90,120)]
    let data = [15, 30, 45, 60, 90, 120]

    var pickerdata:String!
    // MARK: 変更
    // Int!型のように、!で宣言するオプショナル型の利用は極力避ける。
    // 今回の場合は単純にゼロで初期化すれば良い。
    //var timeTotal:Int!
    var timeTotal = 0

    @IBAction func start(_ sender: Any) {

        let storyboard: UIStoryboard = self.storyboard!
        let timer = storyboard.instantiateViewController(withIdentifier: "toNextViewController") as! NextViewController

        // MARK:
        // もし、timeTotalがオプショナル型で宣言されていたら、この時点で実行時エラーになる可能性が高い
        timer.getTime = timeTotal
        self.present(timer, animated: true, completion: nil)

        // MARK: 削除
        // present()で表示したのであれば、下記の行は不要
        //total()
        //self.performSegue(withIdentifier: "toNextViewController", sender: nil)

    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        time.dataSource = self
        time.delegate = self

        // MARK: 追加
        // timeTotal の初期値を900(秒)にする。
        // 一番最初からピッカービューを動かさずに画面を遷移させても、初期値が代入されないことへの対応
        timeTotal = 15 * 60
    }

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        // MARK: 変更
        // PickerView に使うのは1列だけなので、今回は決め打ちで値を返す
        //return data.count
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        // MARK: 変更
        // 今回使うのは一次元配列なので、配列の要素数を .count を使って返す
        //return data[component].count
        return data.count
    }


    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        // MARK: 変更
        //return String(data[component][row])
        return String(data[row])
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        total()
    }

    //    pickerの数字をたすメソッド
    func total() {
        // MARK: 変更
        // 一次元配列になったため計算方法を変更
        //timeTotal = data[0][time.selectedRow(inComponent: 0)] * 60
        timeTotal = data[time.selectedRow(inComponent: 0)] * 60
    }

}
import UIKit

class NextViewController: UIViewController {

    var timer = Timer()
    var count = 0
    var getTime:Int!

    @IBOutlet weak var timerLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        runTimer()
        // MARK: 削除
        // invalidate()はタイマを止めるメソッドのため、記述するとタイマは動かずカウントダウンしない。
        //timer.invalidate()
        count = 0
        timerLabel.text = timeString(time: TimeInterval(getTime))
    }

    // カウントダウンをする関数
    @objc func updateTimer() -> Int {

        count += 1
        //時間 = pickerで設定する時間 ー count
        // MARK: 変更
        // getTimeはオプショナル型だが、!で宣言されたオプショナル型なので、アクセス時のアンラップは不要
        //let remainCount = getTime! - count
        let remainCount = getTime - count
        timerLabel.text = timeString(time: TimeInterval(remainCount))

        //            timerLabel.text = "\(remainCount)"

        //        0秒になったら止まる
        if remainCount == 0 {
            timer.invalidate()
            timerLabel.text = "00:00:00"
        }
        return remainCount

    }

    //タイマーを動かす関数
    func runTimer() {
        // MARK: コメント
        // scheduledTimerは呼び出された直後から指定された関数の呼び出しを始める。
        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTimer), userInfo: nil, repeats: true)
    }

    //00:00:00に変える処理
    func timeString(time: TimeInterval) -> String {
        let hour = Int(time) / 3600
        let minutes = Int(time) / 60 % 60
        let second = Int(time) % 60

        return String(format: "%02d:%02d:%02d", hour, minutes, second)
    }

    // MARK: 削除
    // viewDidLoad()で同じような処理を行なっているので、どちらか一方に記述する
//    override func viewDidAppear(_ animated: Bool) {
//
//        timerLabel.text = timeString(time: TimeInterval(getTime))
//        print(getTime ?? "this is nil")
//
//    }

}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/06/17 21:59

    とても丁寧な回答、ありがとうございます。色々と勘違いをしていたことがわかりました。
    重ねて質問失礼いたします。

    最初の画面(ViewController.swift)の27行目
    let timer = storyboard.instantiateViewController(withIdentifier: "toNextViewController") as! NextViewController
    にて、Thread 1: signal SIGABRT というエラーが出ています。

    関連付けなどの確認はしたのですが、他に考えられる原因はありますでしょうか?

    キャンセル

  • 2020/06/18 05:50

    この場所で実行時エラーが出るということは、
    1. instantiateViewControllerで指定しているIdentifierが間違っている(存在しない)
    2. as!による強制ダウンキャストに失敗している
    関連付けに間違いはないというお話ですが、この辺りを中心として再度確認されてはいかがでしょうか。
    ご質問の段階では実行時エラーはなかったはずですので、どこかを間違って修正したことが考えられます。

    どうしても見つからなければ、新しくプロジェクトを立ち上げて、ソースコードだけコピーするのも一つかと思います。
    のどちらかしかないと思います。

    キャンセル

  • 2020/06/18 23:01

    ご返信ありがとうございます。identifierが間違っていたらしく、修正したところ動きました!
    丁寧な回答、感謝します。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.32%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る