🐷
SwiftUIでスクロール位置をキープ(維持)する方法
SwiftUIでスクロール位置がずれる
SwiftUIのList, ScrollViewでデータを表示している際に、一番下にデータが追加される分にはスクロール位置がキープされるのですが、上にデータが追加されると一番上まで自動でスクロールされてしまいます。これを解決していきます。
実装の考え方
ScrollViewReaderを使えば、特定のViewのid
にスクロール位置を移動させることができるのでそれを使用します。
そして、表示されているViewのid
を保持しておいて、データ追加後にScrollViewReaderを使って移動させます。
実装
struct ContentView: View {
@State var items: [Item] = (1...100).map { .init(id: $0) }
@State var displayItemIDs: Set<Int> = []
@State var scrollPosition: ScrollPosition<Int>?
var displayItems: [Item] {
items.sorted { $0.id > $1.id }
}
var title: String {
guard let max = displayItemIDs.max().map({ "\($0)" }) else {
return "Nothing"
}
let min = displayItemIDs.min().map { "\($0)" } ?? ""
return "\(max) ~ \(min)"
}
var body: some View {
NavigationStack {
ScrollViewReader { proxy in
List {
ForEach(displayItems) { item in
Label("\(item.id)", systemImage: "person")
.id(item.id)
.onAppear { displayItemIDs.insert(item.id) }
.onDisappear { displayItemIDs.remove(item.id) }
}
}
.onChange(of: scrollPosition) { scrollPosition in
guard let scrollPosition else { return }
proxy.scrollTo(scrollPosition.itemID, anchor: scrollPosition.anchor)
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("ADD") {
let topDisplayItemID = displayItemIDs.max()
let first = items.map(\.id).max() ?? 0
let newItems: [Item] = ((first + 1)...(first + 10)).map { .init(id: $0) }
items.append(contentsOf: newItems)
if let topDisplayItemID {
scrollPosition = .init(itemID: topDisplayItemID, anchor: .top)
}
}
}
}
}
}
}
struct Item: Identifiable {
let id: Int
}
struct ScrollPosition<ContentID: Hashable>: Equatable {
let id = UUID()
let itemID: ContentID
let anchor: UnitPoint
}
問題点(Viewのidが順序不可能)
メモなどのIDがUUIDなどの場合ViewのIDが順序不可能であり、let topDisplayItemID = displayItemIDs.max()
を使用しているため上のやり方は不可能です。
その場合は、displayItemIDsの中央値を使用して、proxy.scrollTo(itemID, anchor: .center)
などで対処するなどが良いです。ただ理想とはかけ離れた動きになります。
もっとうまくやりたい場合は、メモ作成日付
などを使用することでうまくいくと思います。
Discussion