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

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

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

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

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Q&A

解決済

1回答

1462閲覧

Swift UI 画面遷移時にタイマーを停止したい

jun9

総合スコア23

iOS

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

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

0グッド

0クリップ

投稿2023/02/12 14:09

編集2023/02/14 09:31

イメージ説明### 実現したいこと

Swift UIで画面遷移時にタイマーを停止したい。

前提

ソースコード TobusDataViewModelクラス内にTimer.scheduledTimer()で15秒おきに処理を繰り返すタイマーを設置しておりますが、メモリ解放のため、ContentViewが画面遷移で非表示になった際にタイマー(myTimer)の破棄をするプログラムを実装しております。

ContentView内のVStack()直下の.onDisappear()でTobusDataViewModelクラス内のmyTimerプロパティを呼び出し、invalidate()でタイマーの破棄を試みました。

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

コンソールでタイマーが停止しているか、確認を行いましたが、タイマーが停止していない(処理が続いている)ことがわかりました。

該当のソースコード

Swift

1 2 3struct TopView: View{ 4 var routes: [Route] = routeData 5 var body: some View { 6 NavigationStack { 7 ScrollView { 8 LazyVGrid(columns: [GridItem()], spacing: 0.0) { 9 ForEach(0 ..< routes.count ,id: \.self) { index in 10 NavigationLink(value: index){ 11 Text(routes[index].title) 12 } 13 } 14 } 15 } 16 .navigationBarTitle(Text("ホーム"),displayMode: .inline) 17 .navigationDestination(for: Int.self) { value in 18 ContentView(id: routes[value].note, busroute: routes[value].busroute, buspole: routes[value].title, startpole1: routes[value].startpole1, startpole2: routes[value].startpole2) 19 } 20 } 21 } 22} 23 24struct ContentView: View { 25 @ObservedObject private var tobusDataVM = TobusDataViewModel() 26 var body: some View { 27 VStack() { 28 Text("text") 29 } 30 .onDisappear(perform:{ 31 self.tobusDataVM.myTimer.invalidate() 32 print("タイマー停止") 33 }) 34 .toolbar { 35 ToolbarItem(placement: .navigationBarTrailing) { 36 Button(action: { 37 self.tobusDataVM.myTimer.invalidate() 38 }, label: { 39 Text("タイマー停止") 40 }) 41 } 42 } 43 } 44 } 45 46class TobusDataViewModel: ObservableObject { 47 var myTimer: Timer! 48 init() { 49 myTimer = Timer.scheduledTimer(withTimeInterval:15, repeats: true, block: { (time:Timer) in 50 //処理 51 }) 52 } 53 //処理 54}

試したこと

onDisappear()内に print("タイマー停止") を記述し、画面遷移時にアクションが実行可能か、確認を行いましたところ、アクションは実行可能である事が分かりました。
しかしながら、該当のソースコードでは、タイマーの破棄が実行されません。
原因が分かる方がいらっしゃいましたら、ご教示いただけますと幸いです。

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

ここにより詳細な情報を記載してください。

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

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

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

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

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

TakeOne

2023/02/14 08:24 編集

これは、どこか別のViewからContentViewに画面遷移してきていて、ContentViewから元の画面に戻る時にタイマーを止めたいということですか? そうであれば、今のコードで問題なさそうに思います。それとも、ContentViewから別の画面に遷移する処理があって、その別画面への遷移時にタイマーを止めたいということですか? そうであれば、 `.onDisappear` は呼び出されないので`invalidate()`も`print("タイマー停止")`も実行されず、タイマーが停止しないまま画面遷移するように思います。いずれにしても、画面遷移時にタイマーを停止したいということであれば、問題が発生する状況が再現できるコード(画面遷移元と画面遷移先の両方の画面遷移処理を含めたコード)を提示する必要があると思います。
jun9

2023/02/14 09:36

TakeOneさんコメントありがとうございます。ソースコードにTopViewを追加致しました。当方がやりたいのは、TopViewからContentViewに画面遷移してきていて、ContentViewからTopViewに戻る時にタイマーを止めたいということです。今回新たに`.onDisappear`の下に`.toolbar`を設置し、タイマー停止ボタンを作成致しました(トップの画像参照)。ボタンをクリックしたところ、View上はデータの更新が停止している事がわかったのですが、コンソール上ではデータの更新が続いています。やはりこれはタイマーが停止ていないと考えるのが正しいでしょうか。
TakeOne

2023/02/14 10:48

画面遷移元のコードが追加されましたが、提示されたコードだけで正常にビルド/実行できますか? `Route` 型や `routeData` が存在していないし、ContentViewの引数にidやbusroute等を指定していますが、それを受け取る変数がContentViewに見当たりません。問題と関係ないと思われる部分は省略してかまいませんが、省略したコードでアプリをビルド/実行して、問題が再現することを確認してから質問していますか? あと、タイマーを起動する処理の直前に `print("タイマー開始")` を入れると、何が起きているか見えてくると思います。
guest

回答1

0

ベストアンサー

提示された画面遷移元のコードは、Route型が定義されていなかったり、ContentViewを呼び出す時の引数が合っていなかったりするので、このままコピペしてアプリを実行して再現確認することはできませんでしたが、エラーを適当に修正してアプリを起動したところ、質問の動作が発生することを確認しました。

画面遷移元から単純に1つのContentViewに画面遷移するだけなら元のコードでも問題ないと思いますが、提示されたコードは、ForEachでNavigationLinkを複数生成しており、それぞれのNavigationLinkからContentViewに画面遷移するようになっています。この場合、画面遷移時に複数回ContentViewが再生成されることがあります。

TobusDataViewModelのinitでタイマーを起動する箇所に print("タイマー開始") を入れて実行するとわかると思いますが、画面遷移した時に、複数個の「タイマー開始」がprint表示されることがわかると思います。つまり、画面遷移した時に、ContentViewが複数回生成されていて、TobusDataViewModelもそれに応じて複数生成されているため、そのinit処理でタイマーが複数個生成されているということです。そして、 onDisappear でタイマーを止めても、1個のタイマーしか止められていないため、他のタイマーは動作したままになります。

画面遷移時に複数のContentViewが生成されるのは正常な動作です。ForEachの中に指定したViewは、必要に応じて何度も再描画されることがあり、再描画する時にContentViewを新しく作り直すことがあります。つまり、ForEachの中で表示するViewは、何回再描画され、何回作り直されても良いように作る必要があります。

再描画される度にタイマーを起動し直して良いのであれば、 onDisappear でタイマーを止めるのではなく、次のようにして、TobusDataViewModelのdeinit処理でタイマーを止めれば、同時に起動されるタイマーは1つになるのでうまくいくと思います。

swift

1struct ContentView: View { 2 @ObservedObject private var tobusDataVM = TobusDataViewModel() 3 var body: some View { 4 VStack() { 5 Text("text") 6 } 7 .toolbar { 8 ToolbarItem(placement: .navigationBarTrailing) { 9 Button(action: { 10 self.tobusDataVM.myTimer.invalidate() 11 }, label: { 12 Text("タイマー停止") 13 }) 14 } 15 } 16 } 17 } 18 19class TobusDataViewModel: ObservableObject { 20 var myTimer: Timer! 21 init() { 22 print("タイマー開始") 23 myTimer = Timer.scheduledTimer(withTimeInterval:1, repeats: true, block: { (time:Timer) in 24 print("タイマー処理") 25 }) 26 } 27 deinit { 28 print("タイマー停止") 29 myTimer.invalidate() 30 } 31}

再描画される度にタイマーを再起動するのが問題な場合は、次のようにして TobusDataViewModel@StateObject で宣言し、 onAppear でタイマーを開始、 onDisAppear でタイマーを停止するように処理すれば、ContentViewが表示された時にタイマーが開始され、ContentViewを閉じた時にタイマーが停止されるようになると思います。

swift

1struct ContentView: View { 2 @StateObject private var tobusDataVM = TobusDataViewModel() 3 var body: some View { 4 VStack() { 5 Text("text") 6 } 7 .onAppear { 8 tobusDataVM.startTimer() 9 } 10 .onDisappear(perform:{ 11 tobusDataVM.stopTimer() 12 }) 13 .toolbar { 14 ToolbarItem(placement: .navigationBarTrailing) { 15 Button(action: { 16 tobusDataVM.stopTimer() 17 }, label: { 18 Text("タイマー停止") 19 }) 20 } 21 } 22 } 23 } 24 25class TobusDataViewModel: ObservableObject { 26 var myTimer: Timer! 27 func startTimer() { 28 print("タイマー開始") 29 myTimer = Timer.scheduledTimer(withTimeInterval:1, repeats: true, block: { (time:Timer) in 30 print("タイマー処理") 31 }) 32 } 33 func stopTimer() { 34 print("タイマー停止") 35 myTimer.invalidate() 36 } 37}

投稿2023/02/16 12:23

編集2023/02/16 13:18
TakeOne

総合スコア6299

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

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

jun9

2023/02/18 15:12

TakeOneさん ご回答ありがとうございました。当方もタイマーを起動する処理の直前にprint("タイマー開始")を入れたところ、タイマーが複数個生成されている事が確認できました。画面遷移時に複数のContentViewが生成されていたのですね。ご回答いただいた後者の方法を参考に、コードを書き直しましたところ、問題が解決いたしました。ご丁寧にご解説頂き、問題が発生した原因を理解する事ができました。重ねてお礼申し上げます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問