個人開発のiOSアプリで、1,000件ほどのレイアウトデータ(ゲームの配置図)を検索・絞り込みする画面を作りました。最初は TextField の入力ごとに全件フィルタしていたので、文字を打つたびにスクロールがカクつきました。やったことは2つだけです。
1. 入力をdebounceする
@MainActor
final class SearchModel: ObservableObject {
@Published var query = ""
@Published private(set) var results: [Layout] = []
private var task: Task<Void, Never>?
private let all: [Layout]
init(all: [Layout]) { self.all = all; self.results = all }
func queryChanged() {
task?.cancel()
let q = query
task = Task {
try? await Task.sleep(for: .milliseconds(250))
guard !Task.isCancelled else { return }
results = filter(q)
}
}
}
View側は .onChange(of: model.query) { model.queryChanged() } で呼ぶだけです。250msにすると、普通に打っている間はフィルタが走りません。
2. 絞り込み結果をキーごとにキャッシュする
カテゴリ(レベル×用途)の組み合わせは数十通りしかないので、一度計算した結果を辞書に持っておきます。
private var cache: [String: [Layout]] = [:]
func filter(_ q: String) -> [Layout] {
let key = q.lowercased()
if let hit = cache[key] { return hit }
let r = key.isEmpty ? all : all.filter { $0.searchText.contains(key) }
cache[key] = r
return r
}
searchText は起動時にタイトルとタグを小文字で連結しておいた文字列です。毎回 lowercased() しないだけでもかなり違いました。
結果
iPhone 12で、入力中のカクつきはほぼ体感できなくなりました。データが更新されたら cache.removeAll() を忘れずに。もっと良いやり方があればコメントで教えてください。