Home > Mobile >  Moving LazyFilterSequence out of ForEach into a separate variable results in requires conform to �
Moving LazyFilterSequence out of ForEach into a separate variable results in requires conform to �

Time:09-07

This code works fine

ForEach(items.filter({$0.matchesSearch(text: searchText) }), id: \.id) { item in
            

but moving the collection out of the ForEach

let takenOut = items.filter({$0.matchesSearch(text: searchText) })
ForEach(takenOut, id: \.id) { item in

results in this error

Generic struct 'ForEach' requires that 'LazyFilterSequence<Results<T>>.SubSequence' 
(aka 'LazyFilterSequence<Slice<Results<T>>>') conform to 'RandomAccessCollection'

any ideas why?

i need to take the sequence out so i can use it's count

whole code for the context

struct SearchSectionView<T: DatabaseItem & Identifiable & SearchMatchingSupport>: View {
    
    @Binding var searchText: String
    
    @ObservedResults(
        T.self,
        where: { $0.recordState == .published }
    ) var items
    
    var body: some View {
         Section("Results") {
             let o = items.sorted(byKeyPath:"name", ascending: true).filter({$0.matchesSearch(text: searchText)})
             ForEach(o, id: \.id) { item in
                 Label(item.suggestionText(), systemImage: "circle").searchCompletion("\(item.id)")
             }
         }
    }
    
}

CodePudding user response:

Why does assigning the result of filter to a let constant cause it to no longer work?

Short Answer:

filter returns different results based upon what the receiver expects.


Long Answer

Consider this example:

let arr = ["apple", "apricot", "banana", "pear"]

let filtered = arr.lazy.filter { $0.hasPrefix("a") }

func count(arr: [String]) -> Int {
    arr.count
}

// This works
count(arr: arr.lazy.filter { $0.hasPrefix("a") })
      
// This doesn't work
count(arr: filtered)

Here it is running in a Playground. When filter is passed directly to count, it works. When filter is assigned to the constant filtered, it doesn't work when passed to count.

playground showing the error


So, what is going on here?

In the first call to filter, it returns a LazyFilterSequence<String>.

playground showing filter returning a LazyFilterSequence


When passed to count which is expecting a [String], filter returns a [String].

playground showing filter returning an array of String


To make this example more similar to your case, let's make count generic:

func count<T: RandomAccessCollection>(arr: T) -> Int {
    arr.count
}

Playground showing RandomAccessCollection error

Now, like ForEach, count is expecting something that adopts RandomAccessCollection. In this case, Swift still chooses the version of filter that returns an Array:

Playground example with RandomAccessCollection


Conclusion

The conclusion is, there are different versions of filter, and the one you get is based upon the expectations of the receiver. In this example, when the result was assigned to filtered, Swift returned a version that required the least up front work. That way, if only the first item in the result is needed, a lot of work is saved.

In the second case where we are passing the filtered result to count(), Swift does the extra work to turn the result back into an Array which is what count expects.

In your case, ForEach expects something that adopts RandomAccessCollection. Swift chooses a version of filter that returns a value that adopts that protocol.

Note:

You can use let to store the search result, you just need to give your constant the explicit type that is passed to ForEach. For example, in my case:

let filtered: [String] = arr.lazy.filter { $0.hasPrefix("a") }

count(arr: filtered)  // this now works

[Option]-click on your call to filter to find the type that filter is returning and then use that type in your let statement.

CodePudding user response:

This filtering shouldn't be in the view, move it to your model, so that the view's model becomes sortedAndFilteredStuff and not stuff that the view has to then process itself. You'll get errors, sometimes hard-to-understand errors, if you put regular swift code inside a ViewBuilder block.

  • Related