Home > other >  How to perform `ForEach` on a computed property in SwiftUI
How to perform `ForEach` on a computed property in SwiftUI

Time:02-02

Intro

Imagine we have an infinite range like the range of all possible integers and we can NOT just store them in memory. So we need to calculate them chunk by chunk like:

func numbers(around number: Int, distance: Int) -> [Int] {
    ((number - distance)...(number   distance))
    .map { $0 } // Using `map` ONLY for making the question clear that we do NOT want to use a range
}

so now we can build our view like:

struct ContentView: View {

    @State var lastVisibleNumber: Int = 0

    var body: some View {

        ScrollView(.horizontal) {
            LazyHStack {
                ForEach(numbers(around: lastVisibleNumber, distance: 10), id:\.self) { number in
                    Text("\(number)")
                        .padding()
                        .foregroundColor(.white)
                        .background(Circle())
                        // .onAppear { lastVisibleNumber = number }
                }
            }
        }
    }
}

enter image description here

Describing the issue and what tried

In order to load more numbers when needed, I have tried to update the lastVisibleNumber on the appearance of last visible Number by adding the following modifier on the Text:

.onAppear { lastVisibleNumber = number }

Although there is a safe range that is visible and should prevent the infinite loop (theoretically), adding this modifier will freeze the view for calculating all numbers!

So how can we achieve a scroll view with some kind of infinite data source?

Considerations

  1. The range of Int is just a sample and the question can be about an infinite range of anything. (In a form of an Array!)

  2. we don't want unexpected stops (like showing a dummy spinner at the leading or trailing of the list) in the middle of the scrolling.

CodePudding user response:

For this, you should try using table view and data source. Let's assume you have an array of integers. You may create a buffer with an arbitrary number of instances. Let say 100. In that case, with a similar logic, your distance would become a 50. When you scroll, and close enough to the limits, you create another array and reload the table view data source so that you can pretend like you have an infinite array. Be aware that reusable cell and table view implementation is very consistent and optimized.

CodePudding user response:

Don’t refresh view when lastVisibleNumber changes - it just gets into endless loop. You can create Observableobject, store it and update it only on-demand (I provided a button but you can obviously load it onAppear eith some criteria - e.g. last of the array was shown):


class NumberViewModel: ObservableObject {
    var lastVisibleNumber = 0
    @Published var lastRefreshedNumber = 0
    
    func numbers(distance: Int) -> [Int] {
        ((lastRefreshedNumber - distance)...(lastRefreshedNumber   distance))
            .map { $0 } // Using `map` ONLY for making the question clear that we do NOT want to use a range
    }
}

struct ContentView: View {
    
    @StateObject var viewModel = NumberViewModel()
    
    var body: some View {
        VStack {
            Button("Refresh view", action: {
                viewModel.lastRefreshedNumber = viewModel.lastVisibleNumber
            })
            
            ScrollView(.horizontal) {
                
                LazyHStack {
                    ForEach(viewModel.numbers(distance: 10), id:\.self) { number in
                        Text("\(number)")
                            .padding()
                            .foregroundColor(.white)
                            .background(Circle())
                            .onAppear { viewModel.lastVisibleNumber = number }
                    }
                }
            }
        }
    }
}
  •  Tags:  
  • Related