0

I have a list, and I want to use swipeAction to either set or unset a property on the list items. My problem is that the function to decide if the property is set or unset is slow, and it runs not just when the user swipes (as I thought) but for every item as the list loads. Here is a mockup of the problem.

Note: In real life, the hasStar() function only takes 50ms, but the list is thousands of elements long, so the result is the same, a slow-loading list.

Is there a better way to do this?

struct ContentView: View {

let data = (1...10).map { "Item \($0)" }

var body: some View {
    VStack {
        List() {
            ForEach(data, id: \.self) { item in
                Text(item)
                    .swipeActions(edge: .leading) {
                        if hasStar() {
                            Button {
                                print("star removed")
                            } label: {
                                Label("Remove star", systemImage: "star.slash").font(.title)
                            }.tint(.red)
                        } else {
                            Button {
                                print("star added")
                            } label: {
                                Label("Add star", systemImage: "star").font(.title)
                            }.tint(.green)
                        }
                    }
            }
        }
    }
}

func hasStar() -> Bool {
    print("Slow function started")
    sleep(1)
    print("Slow function finished")
    return true
}

}

themadp
  • 23
  • 2

1 Answers1

0

The foreach inside the list prevents it from being lazily loading. Make the data list objects conform to Identifiable protocol. And use the List initializer.

See the official docs for a concert example - https://developer.apple.com/documentation/swiftui/list/

If I’m correct. Currently (Nov 2022), under the hood List is implemented using table view. Which loads and renders just the rows on screen. As such the function would be called only for the relevant row swiped and wouldn’t render the whole list.

Nevertheless, is hasStar is async. I’d suggest adding a loader until it finishes

Just to be clear the code should look like:

struct ContentView: View {
    
    let data = (1...1000).map { "Item \($0)" }
    
    var body: some View {
        VStack {
            List(data, id: \.self) { item in
                    Text(item)
                        .swipeActions(edge: .leading) {
                            if hasStar() {
                                Button {
                                    print("star removed")
                                } label: {
                                    Label("Remove star", systemImage: "star.slash").font(.title)
                                }.tint(.red)
                            } else {
                                Button {
                                    print("star added")
                                } label: {
                                    Label("Add star", systemImage: "star").font(.title)
                                }.tint(.green)
                            }
                        }
            }
        }
    }
}


func hasStar() -> Bool {
    print("Slow function started")
    sleep(1)
    print("Slow function finished")
    return true
}

When using a big number of lines you would be able to see that only the visible part is shown and rendered (even when swipe)

Please note. This specific code example gets the ui stuck as it is sleeps the main thread. It is recommended to use asynchronous calls with some loader

CloudBalancing
  • 1,461
  • 2
  • 11
  • 22
  • The items dont know if they have a star, so where would I call the hasStar function to decide if the swipe action is to add or remove the star, if not in the list? – themadp Nov 30 '22 at 16:25
  • At the same place just change `List() { ForEach(data, id: \.self) { item in...}}` TO `List(data, id: \.self) { item in ...}` – CloudBalancing Nov 30 '22 at 22:59