2

Suddenly I have figured out that there is no automatic animation for non of them:

Demo

no animations for inserting, removing, reordering, etc. ☹️ Ids are unique and persistent between transitions

Question 1

How can I make them work as expected?

Question 2

Is there a simple way to have an animation for seeing the transition between lists? Like moving a row between sections of a single list.


Explaining:

Let's say I have three lists that group different states of elements of a single array:

extension Call {
    enum State: Equatable {
        case inProgress
        case accepted
        case rejected
    }
}

The observable:

class CallManager: ObservableObject {
    @Published var calls: [Call]
    init(visits: [Call] = []) { self.calls = visits }
}

And Call is a simple Identifiable:

struct Call: Identifiable & Equatable & Hashable {
    let id = UUID()
    var state: State
}

By making these bindings, I have bind all lists to the core calls array:

extension CallManager {
    func bindingCalls(for state: Call.State) -> Binding<[Call]> {
        Binding<[Call]>(
            get: { self.calls.filter { $0.state == state } },
            set: { // TODO: Find a better way for this
                self.calls.removeAll(where: { $0.state == state })
                self.calls.append(contentsOf: $0)
            }
        )
    }

    var inProgress: Binding<[Call]> { bindingCalls(for: .inProgress) }
    var accepted: Binding<[Call]> { bindingCalls(for: .accepted) }
    var rejected: Binding<[Call]> { bindingCalls(for: .rejected) }
}

And here is the View code:

struct ContentView: View {

    @StateObject var visitManager =  CallManager(visits: [
        Call(state: .inProgress),
        Call(state: .accepted),
        Call(state: .accepted),
        Call(state: .inProgress),
        Call(state: .inProgress),
        Call(state: .rejected)
    ])

    var body: some View {
        HStack {
            List(visitManager.inProgress) { $call in
                CallView(call: $call)
            }

            List(visitManager.accepted) { $call in
                CallView(call: $call)
            }

            List(visitManager.rejected) { $call in
                CallView(call: $call)
            }
        }
    }
}

struct CallView: View & Identifiable {
    @Binding var call: Call
    var id: UUID { call.id }

    var body: some View {
        Text(call.id.uuidString.prefix(15))
            .foregroundColor(call.state.color)
            .onTapGesture(count: 2) { call.state = .rejected }
            .onTapGesture { call.state = .accepted }
    }
}

extension Call.State {
    var color: Color {
        switch self {
        case .inProgress: return .blue
        case .rejected: return .red
        case .accepted: return .green
        }
    }
}

1 Answer 1

3

You can enable the animations on the List view:

List(visitManager.inProgress) { $call in
    CallView(call: $call)
}
.animation(.default)

Or wrap the changes in a withAnimation block:

.onTapGesture { withAnimation { call.state = .accepted } }

As for the animation between the columns, you can get something like that with .matchedGeometryEffect. afaik it will always look a bit crumbly between List, to make it look good you need to use a VStack (but then loose all the comfort of the List view). For example:

import SwiftUI

extension Call {
    enum State: Equatable, CaseIterable {
        case inProgress
        case accepted
        case rejected
    }
}

class CallManager: ObservableObject {
    @Published var calls: [Call]
    init(visits: [Call] = []) { calls = visits }
}

struct Call: Identifiable & Equatable & Hashable {
    let id = UUID()
    var state: State
}

struct ContentView: View {
    @Namespace var items

    @StateObject var visitManager = CallManager(visits: [
        Call(state: .inProgress),
        Call(state: .accepted),
        Call(state: .accepted),
        Call(state: .inProgress),
        Call(state: .inProgress),
        Call(state: .rejected),
    ])

    var body: some View {
        HStack(alignment: .top) {
            ForEach(Call.State.allCases, id: \.self) { state in
                VStack {
                    List(visitManager.calls.filter { $0.state == state }) { call in
                        CallView(call: call)
                            .id(call.id)
                            .matchedGeometryEffect(id: call.id, in: items)
                            .onTapGesture {
                                if let idx = visitManager.calls.firstIndex(where: { $0.id == call.id }) {
                                    withAnimation {
                                        visitManager.calls[idx].state = .rejected
                                    }
                                }
                            }
                    }
                }
            }
        }
    }
}

struct CallView: View & Identifiable {
    var call: Call
    var id: UUID { call.id }

    var body: some View {
        Text(call.id.uuidString.prefix(15))
            .foregroundColor(call.state.color)
    }
}

extension Call.State {
    var color: Color {
        switch self {
        case .inProgress: return .blue
        case .rejected: return .red
        case .accepted: return .green
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, Ralf +1 for answering question 1. I am looking for a convenient animation for moving rows between lists. How can I achieve that?
You can get something like that using matchedGeometryEffect but it will not perfect between Lists. See the answer for an example.
Thanks, Ralf, I would upvote you again if I could. I didn't know about the @Namespace and matchedGeometryEffect. I've learned a lot tonight.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.