🐨
SwiftでonChange内の処理がうまく実行されない時の解決方法
発生した事象
keyboardを表示したらtodo一覧の一番下まで自動でスクロールさせたいです。
キーボードが開閉するタイミングを検知して、onChangeイベント内でscrollTo
関数を使って処理を実行させているのに何故かスクロールしてくれません。
コードはこんな感じ
TodoScreenView.swift
//
// TodoScreenVIEW.swift
// ai_chat_app
//
// Created by 池内隆人 on 2025/02/09.
//
import SwiftUI
import SwiftData
struct TodoScreenView: View {
@Environment(\.modelContext) private var context
@Query private var todoItemList: [TodoItem]
@State private var titleTextInput = ""
@State private var isKeyboardOpen = false
var body: some View {
ZStack {
VStack {
ScrollViewReader { proxy in
List {
ForEach(todoItemList) { todoItem in TodoItemView(todoItem: todoItem)}.onDelete(perform: {
indexList in for index in indexList {
delete(todoItem: todoItemList[index])
}
})
Color.clear.id(999)
}.padding().onChange(of: isKeyboardOpen) {
if(isKeyboardOpen) {
withAnimation(.smooth(duration: 10)) {
proxy.scrollTo(999, anchor: .top)
}
}
}
HStack {
TextField("Title", text: $titleTextInput, axis: .vertical).padding(16).font(.title3).overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
).onSubmit {
add(title: titleTextInput)
}
Button("追加") {
add(title: titleTextInput)
}
}.padding(20)
}
}
}.ignoresSafeArea(.container, edges: .bottom).onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in isKeyboardOpen = true}.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in isKeyboardOpen = false}
}
private func add(title: String) {
let data = TodoItem(id: UUID(), title: title, isDone: false)
context.insert(data)
do {
try context.save()
} catch {
print(error)
}
}
private func delete(todoItem: TodoItem) {
context.delete(todoItem)
do {
try context.save()
} catch {
print(error)
}
}
}
struct TodoItemView: View {
var todoItem: TodoItem
var body: some View {
HStack {
Circle().fill(todoItem.isDone ? Color.red : Color.green).frame(width: 30, height: 30)
VStack(alignment: .leading) {
Text(todoItem.title).font(.title).padding(20)
}
}
}
}
原因
onChangeイベントが発生後にすぐscrollTo
関数を実行しているのが原因です。
UI(今回だとキーボードが開かれたり閉じたりしたこと)の反映後ScrollTo関数がほぼ同時に行われていることでうまく機能していないようです。
解決方法
メインスレッドでのランタイムを待たせる(処理を少し遅延させる)ことで解決します。
TodoScreenView.swift
.onChange(of: isKeyboardOpen) { newValue in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation {
proxy.scrollTo(999, anchor: .top)
}
}
}
DispatchQueue.main.asyncAfterとは
見慣れないメソッドが出てきたので調べました。
メインスレッド上で一定の遅延時間を置いてから処理を実行してくれるメソッドとのこと。
UIの更新やユーザーの操作系は基本メインスレッドで実行されるので、「UIが更新されたら~」とか「特定の操作が完了したら~」の後に後続処理をさせたい場合に使用します。
つまりこのメソッドを使うことで、ただTimerメソッドみたいに秒数を待つのではなくて、UIの更新が終わった後みたいなイベントの後に続けて処理をしたい時に有用です。
例)
- いいねボタンを押した0.5秒後にアニメーションでハートを一瞬大きくしたい
- 投稿内容がUIに完全に反映された後に「投稿しました!」というトーストを出したい
検証
うまくできました!
DispatchQueue.main.asyncAfter
メソッドは多用も良くないと思うのでアプリの要件に合わせて使い方はしっかり考えた方が良さそうです。
Discussion