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

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

新規登録して質問してみよう
ただいま回答率
85.35%
iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

794閲覧

カウントダウンの数値を別ViewControllerのラベルに表示させたい

Orihalcon

総合スコア5

iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2020/02/23 15:56

CountDown classで実装したカウントダウンタイマーによって0.01秒ごとに変化する数値を、GameViewControllerのLabelに反映させたいです。(カウントダウンを実装させているファイルと、その数値を表示させたいファイルが別ということです)

ソースコードについてはもう少し下の方で記載しております。

1つのView内でカウントダウンを実装させて、それをLabelに反映させることは可能なのですが、そのViewには他にも色々なことを実装させたいため、別Modelでカウントダウンを実装させようと考えました。
ネットの情報を探し回りましたが、解決することが出来なかったため、ここで質問させて頂きました。
swiftを勉強して初めて作るアプリになります。

発生している問題・エラーメッセージ

GameViewController内の

GameViewController

1timerLabel.text = timerCountString

の部分で下記エラーが発生してプログラムが止まってしまいます。

Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

該当のソースコード

GameViewController

1import UIKit 2 3class GameViewController: UIViewController { 4 5 @IBOutlet weak var timerLabel: UILabel! //ここに数値を反映させたい 6 var countDown = CountDown() //カウントダウンを実装しているclass 7 var timerCountString: String = "" //この変数をtimerLabel.textに入れる 8 9 10 override func viewWillAppear(_ animated: Bool) { 11 super.viewWillAppear(animated) 12 13 countDown.startTimer() //別クラスの関数を発動 14 } 15 16 func updateTimerLabel() { // カウントダウンをこのclassに反映させるための関数 17 print(timerCountString) //ここまでは値が渡ってきている 18 timerLabel.text = timerCountString //ここでエラーが発生する 19 }

CountDown

1import Foundation 2 3class CountDown { 4 5 var t = Double() 6 var timer = Timer() 7 var startTime: TimeInterval? = nil 8 var timerCountString: String = "" 9 10 func startTimer() { 11 startTime = Date.timeIntervalSinceReferenceDate 12 timer = Timer.scheduledTimer( 13 timeInterval: 0.01, 14 target: self, 15 selector: #selector(update), 16 userInfo: nil, 17 repeats: true) 18 } 19 // 10.00秒から0.01秒ずつカウントダウンさせたい 20 @objc func update() { 21 if let startTime = startTime { 22 t = 10.00 - (Date.timeIntervalSinceReferenceDate - startTime) 23 } 24 if t <= 0 { // 残り0秒になったらタイマーを止める 25 t = 0 26 timer.invalidate() 27 } 28 timerCountString = String(format: "%.2f", t) //小数点を制限 29 let gameVC = GameViewController() 30 gameVC.timerCountString = timerCountString // 数値を渡す 31 gameVC.updateTimerLabel() // gameVCの関数を発動 32 } 33}

最初は

CountDown

1let gameVC = GameViewController() 2gameVC.timerLabel.text = timerCountString // Labelのtextに直接値を渡す

のように記述していましたが、色々調べたところ、いったんLabelがあるviewに値を渡す方が良さそうだったので、いったん値を渡す内容に書き換えて、それを0.01秒ごとにLabel.textに反映させたいがために、関数を関数で呼ぶような形になりました。がそれは今回の直接的な原因ではありませんでした・・・。

試したこと

その1

CountDownに実装している内容をGameViewControllerに実装すると、きちんとLabel.textに反映します。
しかし、今回は別Modelで実装した内容をGameViewControllerに反映させたいです。

その2

GameViewController

1func updateTimerLabel() { 2 print(timerCountString) 3 timerLabel.text = "test" // ここを変更 4 }

のようにtimerLabel.textに代入する内容を簡略化した場合でも、同様のエラーは発生します。
ですので、代入する値自体が問題ではないと考えております。

その3

GameViewController

1func updateTimerLabel() { 2 print(timerCountString) 3 }

だけの記述にすると、
9.99
9.98
9.97
9.96
9.95
のようにアウトプットされて0.00まで問題なく進むため、ここまで値は渡ってきていることは確かだと思うのですが、これを

GameViewController

1timerLabel.text = timerCountString

のようにLabelのtextに反映させようとするとエラーが発生するため、このLabel.textと関数を使ったファイル間のデータ移動の何かに問題があるのだと思うのですが、未だに原因を掴めておりません。

お手数をおかけしますが、この問題を解決するためのアドバイスを頂ければ幸いです。
よろしくお願い致します。

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

Version 11.3.1 (11C504)

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

エラーが出る直接の原因ですが、アプリが立ち上がったときのGameViewControllerクラスのインスタンスと、CountDownクラスの内部で宣言しているgameVCGameViewControllerクラスのインスタンス)の実体が全く別のものだからです。

クラス名は全く同じですが、メモリ上の実体(インスタンス)は全く別です。

なので、CountDownクラスの内部で新たに宣言されたgameVC:GameViewControllerクラスのプロパティtimerLabel: UILabel!は、その実体がないまま(値がnilのまま)timerLabel.text = timerCountStringと値が代入されているため、実行時エラーが出てしまいます。

絵を描いてみたので末尾に載せておきます。
是非ご自身で一度図示してみることをお勧めします。

このような処理は、delegate という実装方法をやるんだと思います。
たとえば、ここのページあたりは説明がわかりやすいです。

このページでは、「Abcというクラスを定義し、このAbcクラスの中でなにかのイベントが発生したときに、ViewControllerに通知する仕組みを作りたい」と説明してありますが、質問者さんのケースに置き換えれば、「CountDownというクラスを定義し、このCountDownクラスの中でなにかのイベント(=時間の更新)が発生したときに、GameViewControllerに通知する仕組みを作りたい」と置き換えることができます。

やり方としては、このページの通りなのですが、具体的にはこんな感じで書き換えます。
書き換えた場所にはコメントを入れてありますので参考にしてください。

CountDown

1import Foundation 2 3// ** CountDownDelegate プロトコルを定義 4protocol CountDownDelegate : class { 5 // *** 別のクラスで具体的に行わせたい処理 6 func updateTimerLabel(_: String) 7} 8 9class CountDown { 10 11 var t = Double() 12 var timer = Timer() 13 var startTime: TimeInterval? = nil 14 var timerCountString: String = "" 15 16 // ** delegate先のクラス(この時点では未定なのでオプショナル型で定義) 17 // ** 循環参照を防ぐために weak で定義 18 weak var delegate: CountDownDelegate? 19 20 func startTimer() { 21 startTime = Date.timeIntervalSinceReferenceDate 22 timer = Timer.scheduledTimer( 23 timeInterval: 0.01, 24 target: self, 25 selector: #selector(update), 26 userInfo: nil, 27 repeats: true) 28 } 29 30 @objc func update() { 31 if let startTime = startTime { 32 t = 10.00 - (Date.timeIntervalSinceReferenceDate - startTime) 33 } 34 if t <= 0 { 35 t = 0 36 timer.invalidate() 37 } 38 timerCountString = String(format: "%.2f", t) 39 40 // *** delegate先のクラスで定義された updateTImerLabel(_) を実行 41 // *** 仮に delegate == nil の場合は次の行は実行されない(optional chainingの性質) 42 delegate?.updateTimerLabel(timerCountString) 43 } 44}

GameViewController

1import UIKit 2 3// *** CountDownDelegate プロトコルを採用させる 4class GameViewController: UIViewController, CountDownDelegate { 5 6 @IBOutlet weak var timerLabel: UILabel! 7 var countDown = CountDown() 8 var timerCountString: String = "" 9 10 11 override func viewWillAppear(_ animated: Bool) { 12 super.viewWillAppear(animated) 13 14 // *** delegate を設定(自分自身のインスタンスにする) 15 countDown.delegate = self 16 17 countDown.startTimer() 18 } 19 20 // *** 引数を追加 21 func updateTimerLabel(_ timerCountString: String) { 22 print(timerCountString) 23 timerLabel.text = timerCountString 24 } 25}

GameViewControllerの生成
countDownの生成
gameVCの生成
updateTimerLabelの呼び出し

こんな時間になったのでかなり簡潔に書きましたが、疑問があればコメントしてください。

投稿2020/02/23 17:21

編集2020/02/24 03:00
TsukubaDepot

総合スコア5086

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Orihalcon

2020/02/24 03:16

tsukubadepotさん 迅速かつ丁寧なご回答を有難うございます! 実際に頂いた内容でコードを書き換えると問題なく動作しました。 初心者の私にとっては文章だけではモヤモヤしていた内容に関して、実際に図まで提供していただいたお陰で、今回のエラーが起きた原因をとても分かりやすく理解することが出来ました。 (まさか実体がない別のviewを作っていたとは思いもしませんでした・・・) そして今までは何となくで使用していた(教材を見ながら思考停止状態で記述していた)delegateに関しても 「そうか、delegateはこのためにあったのか!」 「delegateが無いと、こういう場合に困るのか」 という新しい発見が出来て、自分の頭の中で何かと何かが繋がったような気がします。 ただまだ自分の中にdelegateの理解を落とし切れていない部分があるので、頂いたリンク先の説明をもう少し理解できるように勉強を重ねてみたいと思います。 深夜まで対応して、さらには追加資料まで作成して頂き、本当に有難うございました!
TsukubaDepot

2020/02/24 12:02

実は私がdelegateについて詳しく考えたのは初めてです。 仕組みや実際のプログラミングでどのように使うのかは理解し始めていたのですが、それを自分のコードにどのように落とすのかについて考える良い機会になりました。こちらこそありがとうございます。 機会があってちょっときちんと整理しようと思い、図を作ってみました。 本当はdelegateの役割まで図示したかったのですが、そちらはまだきちんと図示できるほど整理できていません。 この機会に私も理解を深めたいと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問