I'm trying to implement a SwiftUI view that switches between different grid styles (list, row, grid) using a SwitchMode enum. I want to use matchedGeometryEffect
to animate the transition between these styles, when I apply it by passing its id, it seems that the animation does not play, it changes suddenly as if it will not apply
video https://www.youtube.com/shorts/9YRxAZnonYA
Here is my current implementation:
struct ColorItem: Identifiable {
let id: UUID = .init()
let title: String
let summary: String
let color: Color
static let allItems: [ColorItem] = [
ColorItem(title: "Red", summary: "This is red", color: .red),
ColorItem(title: "Green", summary: "This is green", color: .green),
ColorItem(title: "Blue", summary: "This is blue", color: .blue),
ColorItem(title: "Yellow", summary: "This is yellow", color: .yellow),
ColorItem(title: "Orange", summary: "This is orange", color: .orange),
ColorItem(title: "Pink", summary: "This is pink", color: .pink),
ColorItem(title: "Mint", summary: "This is Mint", color: .mint),
ColorItem(title: "Teal", summary: "This is teal", color: .teal),
ColorItem(title: "Cyan", summary: "This is cyan", color: .cyan),
ColorItem(title: "Indigo", summary: "This is indigo", color: .indigo),
ColorItem(title: "Purple", summary: "This is purple", color: .purple),
ColorItem(title: "Gray", summary: "This is Gray", color: .gray),
ColorItem(title: "Brown", summary: "This is Brown", color: .brown),
]
}
enum SwitchMode: CustomStringConvertible {
case list, row, grid
var iconName: String {
switch self {
case .list: return "list.bullet"
case .row: return "rectangle.grid.1x2"
case .grid: return "rectangle.grid.2x2"
}
}
var description: String {
switch self {
case .list: return "List style"
case .row: return "Row style"
case .grid: return "Grid style"
}
}
}
struct SwitchGridPlayground: View {
@State private var currentStyle: SwitchMode = .list
@Namespace private var animation
let gridItems = [
GridItem(.flexible(minimum: 140), spacing: 0),
GridItem(.flexible(minimum: 140), spacing: 0),
GridItem(.flexible(minimum: 140), spacing: 0)
]
let dataList = ColorItem.allItems
var body: some View {
NavigationStack {
ScrollView {
switch currentStyle {
case .list:
//let _ = print(dataList.first?.id)
LazyVStack {
ForEach(dataList) { item in
HStack {
Color(item.color)
.aspectRatio(1, contentMode: .fit)
Spacer()
Text(item.title)
}
.frame(maxHeight: 72)
.matchedGeometryEffect(id: item.id, in: animation)
}
}
case .row:
//let _ = print(dataList.first?.id)
LazyVStack {
ForEach(dataList) { item in
VStack {
Color(item.color)
.aspectRatio(16 / 9, contentMode: .fit)
Text(item.title)
Text(item.summary)
}
.matchedGeometryEffect(id: item.id, in: animation)
}
}
case .grid:
//let _ = print(dataList.first?.id)
LazyVGrid(columns: gridItems, alignment: .center, spacing: 10) {
ForEach(dataList) { item in
VStack {
Color(item.color)
.aspectRatio(4 / 3, contentMode: .fit)
Text(item.title)
}.matchedGeometryEffect(id: item.id, in: animation)
}
}
}
}
.navigationTitle("Grid switch mode")
.toolbar {
myToolBarContent()
}
}
}
//Button for switch grid
@ToolbarContentBuilder
func myToolBarContent() -> some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
let nextStyle: SwitchMode = {
return switch currentStyle {
case .list: .row
case .row: .grid
case .grid: .list
}
}()
Menu {
Picker(selection: $currentStyle, label: Text("Content style")) {
Label(SwitchMode.list.description, systemImage: SwitchMode.list.iconName)
.tag(SwitchMode.list)
Label(SwitchMode.row.description, systemImage: SwitchMode.row.iconName)
.tag(SwitchMode.row)
Label(SwitchMode.grid.description, systemImage: SwitchMode.grid.iconName)
.tag(SwitchMode.grid)
}
} label: {
Image(systemName: nextStyle.iconName)
.resizable()
.scaledToFit()
.frame(width: 22)
} primaryAction: {
switch currentStyle {
case .list: currentStyle = .row
case .row: currentStyle = .grid
case .grid: currentStyle = .list
}
}
}
}
}
You get some animation if you use withAnimation
to perform the change inside the primaryAction
block of the menu, or if you just add an .animation
modifier to the ScrollView
:
ScrollView {
// ...
}
.animation(.easeInOut, value: currentStyle) // 👈 HERE
.navigationTitle("Grid switch mode")
.toolbar {
myToolBarContent()
}