3

I have a List in my SwiftUI app, with on top buttons to see the previous/next category of items to be loaded into the list. When you press the buttons, the source of the List changes, and the List updates with its default animation (where rows are folding away).

Code for a simplified reproducible version (see below for a screenshot):

import SwiftUI

struct ContentView: View {
  private let models = [
    ["a", "b", "c", "d", "e", "f"],
    ["g", "h"],
    ["i", "j", "k", "l"],
  ]

  @State private var selectedCategory = 1

  private var viewingModels: [String] {
    models[selectedCategory]
  }

  var body: some View {
    VStack(spacing: 0.0) {
      HStack {
        Button(action: previous) {
          Text("<")
        }

        Text("\(selectedCategory)")

        Button(action: next) {
          Text(">")
        }
      }

      List(viewingModels, id: \.self) { model in
        Text(model)
      }
    }
  }

  private func previous() {
    if selectedCategory > 0 {
      selectedCategory -= 1
    }
  }

  private func next() {
    if selectedCategory < (models.count - 1) {
      selectedCategory += 1
    }
  }
}

enter image description here

Now, I don't want to use the default List animation here. I want the list items to slide horizontally. So when you press the > arrow to view the next category of items, the existing items on screen should move to the left, and the new items should come in from the right. And the reverse when you press the < button. Basically it should feel like a collection view with multiple pages that you scroll between.

I already found that wrapping the contents of the previous and next functions with withAnimation changes the default List animation to something else. I then tried adding .transition(.slide) to the List (and to the Text within it) to change the new animation, but that doesn't have an effect. Not sure how to change the animation of the List, especially with a different one/direction for the 2 different buttons.

Using a ScrollView with a List per category is not going to scale in the real app, even though yea that might be a solution for this simple example with very few rows :)

1 Answer 1

6
+50

If your button is wrapped as you suggested and I added a simple direction boolean:

Button(action: {
            withAnimation {
                slideRight = true
                self.previous()
            }
        }) {
          Text("<")
        }

And opposite for the other direction:

Button(action: {
            withAnimation {
                slideRight = false
                self.next()
            }
        }) {
          Text(">")
        }

Then you can transition your view like this:

List(viewingModels, id: \.self) { model in
            Text(model)
        }
        .id(UUID())
        .transition(.asymmetric(insertion: .move(edge: slideRight ? .leading : .trailing),
                                removal: .move(edge: slideRight ? .trailing : .leading)))

Note that in order for the list to not animate, we need to give the list a new unique ID each time, see this article: https://swiftui-lab.com/swiftui-id/

UPDATE:

I wanted to provide the full shortened code that works, also removed the UUID() usage based on comments below.

import SwiftUI

struct ContentView: View {
    private let models = [
        ["a", "b", "c", "d", "e", "f"],
        ["g", "h"],
        ["i", "j", "k", "l"],
    ]

    @State private var selectedCategory = 0
    @State private var slideRight = true

    private var viewingModels: [String] {
        models[selectedCategory]
    }

    var body: some View {
        VStack(spacing: 0.0) {
            HStack {
                Button(action: {
                    withAnimation {
                        if(self.selectedCategory - 1 < 0) { self.selectedCategory = self.models.count - 1 }
                            else {  self.selectedCategory -= 1 }
                        self.slideRight = true
                    }
                }) {
                    Image(systemName: "arrow.left")
                }

                Text("\(selectedCategory + 1)")

                Button(action: {
                    withAnimation {

                        if(self.selectedCategory + 1 > self.models.count - 1) { self.selectedCategory = 0 }
                            else { self.selectedCategory += 1 }
                        self.slideRight = false
                    }
                }) {
                    Image(systemName: "arrow.right")
                }
            }.font(.title)

            List(viewingModels, id: \.self) { model in
                Text(model)
            }
            .id(selectedCategory)
            .transition(.asymmetric(insertion: .move(edge: slideRight ? .leading : .trailing),
                                    removal: .move(edge: slideRight ? .trailing : .leading)))
        }.padding(10)
    }
}

animated list change

Sign up to request clarification or add additional context in comments.

6 Comments

Sadly the need for .id(UUID()) on the List is causing a bunch of different problems. For example when tapping to one of the list rows to open that detail view, this is now no longer a "live updating" model. So changes made on the server to this model, no longer update in realtime. Or when you present a modal sheet on top of this list, it scrolls to the top since the UUID changed. But, easily fixed by using a constant ID per list.
So with the constant ID per updated list, it is working, correct?
Correct! Luckily :)
@KevinRenskers More accurate for this example would be to use '.id(selectedCategory)', which is what I did and added the full example.
Yep that is exactly what I ended up using in my code as well. Nice that you updated the example though for other people running into the same issue!
|

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.