Open3
[SwiftUI] iOS16.4以上でListのアニメーションが動作しない+解決策
記事を表すArticle
を List
で一覧表示し、ボタンを押すとアニメーション付きでRowを削除する画面
import SwiftUI
struct Article: Identifiable, Hashable {
enum Category {
case news
case pr
}
var id: Int
var title: String
var category: Category
}
struct SampleView: View {
@State var articles: [Article] = [
Article(id: 1, title: "title1", category: .news),
Article(id: 2, title: "title2", category: .news),
Article(id: 3, title: "title3", category: .news),
Article(id: 4, title: "title4", category: .news),
Article(id: 5, title: "title5", category: .pr),
Article(id: 6, title: "title6", category: .news),
Article(id: 7, title: "title7", category: .news),
Article(id: 8, title: "title8", category: .news),
Article(id: 9, title: "title9", category: .news),
Article(id: 10, title: "title10", category: .pr),
]
var body: some View {
VStack {
List(articles) { article in
Group {
switch article.category {
case .news:
HStack {
Image(systemName: "newspaper")
Text(article.title)
}
case .pr:
HStack {
Image(systemName: "star")
Text(article.title)
Text("PR")
}
}
}
}
Button {
_ = withAnimation {
articles.removeFirst()
}
} label: {
Text("remove article")
}
}
}
}
このコードはiOS16.4以上と未満で挙動が異なる
(a) はRowが上部にスライドしながら消えていくアニメーションが出ている
(b) は最下部がフェードアウトしていくようなアニメーションが出ている(アニメーションがない、ではない)
(a) が正しい挙動だと思われる
(a) iOS16.2 | (b) iOS16.4 |
---|---|
こういうケースは大抵 List
の Rowのidに問題があり
スナップショット間でidが上手く追跡できてないため
アニメーションが正常に動作していない
のだと思われる(?)
色々デバッグ
分岐をなくす
(a)の正しい挙動
List
+ 分岐が良くないっぽい
コード
struct SampleViewNoCondition: View {
@State var articles: [Article] = (0...10).map { Article(id: $0, title: "title\($0)", category: $0.isMultiple(of: 5) ? .pr : .news) }
var body: some View {
VStack {
List(articles) { article in
HStack {
Image(systemName: "newspaper")
Text(article.title)
}
}
Button {
_ = withAnimation {
articles.removeFirst()
}
} label: {
Text("remove article")
}
}
}
}
idを手動でつける
(b) の間違った挙動
コード
struct SampleViewAddId: View {
@State var articles: [Article] = (0...10).map { Article(id: $0, title: "title\($0)", category: $0.isMultiple(of: 5) ? .pr : .news) }
var body: some View {
VStack {
List {
ForEach(articles) { article in
Group {
switch article.category {
case .news:
HStack {
Image(systemName: "newspaper")
Text(article.title)
}
case .pr:
HStack {
Image(systemName: "star")
Text(article.title)
Text("PR")
}
}
}
.id(article.id)
}
}
Button {
_ = withAnimation {
articles.removeFirst()
}
} label: {
Text("remove article")
}
}
}
}
ScrollViewReaderを利用してidを確かめる
(わかりにくいが)title20
までスクロールする
スクロール位置は毎回同じなので
「1回removeしたらidがおかしくなる」とかではなさそう
コード
struct SampleViewScroll: View {
@State var articles: [Article] = (0...100).map { Article(id: $0, title: "title\($0)", category: $0.isMultiple(of: 5) ? .pr : .news) }
var body: some View {
VStack {
ScrollViewReader { proxy in
List(articles) { article in
Group {
switch article.category {
case .news:
HStack {
Image(systemName: "newspaper")
Text(article.title)
}
case .pr:
HStack {
Image(systemName: "star")
Text(article.title)
Text("PR")
}
}
}
}
HStack {
Button {
_ = withAnimation {
articles.removeFirst()
}
} label: {
Text("remove article")
}
Button {
withAnimation {
proxy.scrollTo(20, anchor: .top)
}
} label: {
Text("scroll to 20")
}
}
}
}
}
}
解決策
Sectionで囲う
これでiOS16.4以上も未満も (a) の正しい挙動が出た
struct SampleViewSolved: View {
@State var articles: [Article] = (0...10).map { Article(id: $0, title: "title\($0)", category: $0.isMultiple(of: 5) ? .pr : .news) }
var body: some View {
VStack {
List {
Section {
ForEach(articles) { article in
Group {
switch article.category {
case .news:
HStack {
Image(systemName: "newspaper")
Text(article.title)
}
case .pr:
HStack {
Image(systemName: "star")
Text(article.title)
Text("PR")
}
}
}
}
}
}
Button {
_ = withAnimation {
articles.removeFirst()
}
} label: {
Text("remove article")
}
}
}
}
なぜ?
わからん、有識者の方教えてください
そもそもSection
なしで List
を使うのが間違ってるのか?とも思ったが
List
単体で使ってる例が公式ドキュメントにあったのでそういうわけでもなさそう
まとめ
SwiftUIはiOSのメジャーアップデートは言わずもがな、マイナーアップデートでも挙動が変わるので
「マイナーだから大丈夫っしょw」と思わずちゃんとデバッグしておいた方が良さそうです