swiftuimatchedgeometryeffect

No matchedGeometryEffect in Switch grid style in SwiftUI


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
                }
                 
            }
             
        }
    }
    
}

Solution

  • 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()
    }
    

    Animation