The SwiftUI ‘searchable’ modifier

From IOS 15 swiftUI has been enriched of new (wished) functionalities. One of the most useful in my opinion is the new modifier searchable that allow to achieve in pretty straightforward way a full working search bar.

Form first announcement I couldn’t wait to use it and as soon as I’ve updated my XCode I started to refactor one of the my iOS app from UIKit couple UITableViewController + UISearchController to swiftUI List + Searchable modifier.

Problem with ‘@FetchRequest’ dynamic filtering

I’ve applied it on top of a List and it worked as expected but the problem was that I would want to use @FetchRequest property wrapper but it seemed not thought for use it in a dynamic filtering scenario like when we want to use with a search bar.

So the problem was: is it possible to use use @FetchRequest to perform dynamic filtering ?.

Solution using a ‘DynamicFetchRequestView’

Luckily I land on this article from amazing Paul Hudson that explains how is possible use @FetchRequest property wrapper for dynamic filtering so i decided to apply the provided solution in my project and develop a more generic SwiftUI View DynamicFetchRequestView allowing to apply the dynamic filtering in different scenarios.

‘DynamicFetchRequestView’ implementation

Essentially the solution proposed by Paul Hudson was to set @FetchRequest property wrapper in custom initializer to which we can pass argument allowing to prepare a FetchRequest on-the-fly. Below there is the DynamicFetchRequestView implementation

struct DynamicFetchRequestView<T: NSManagedObject, Content: View>: View {

    // That will store our fetch request, so that we can loop over it inside the body.
    // However, we don’t create the fetch request here, because we still don’t know what we’re searching for.
    // Instead, we’re going to create custom initializer(s) that accepts filtering information to set the fetchRequest property.
    @FetchRequest var fetchRequest: FetchedResults<T>

    // this is our content closure; we'll call this once the fetch results is available
    let content: (FetchedResults<T>) -> Content

    var body: some View {
        self.content(fetchRequest)
    }

    // This is a generic initializer that allow to provide all filtering information
    init( withPredicate predicate: NSPredicate, andSortDescriptor sortDescriptors: [NSSortDescriptor] = [],  @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {
        _fetchRequest = FetchRequest<T>(sortDescriptors: sortDescriptors, predicate: predicate)
        self.content = content
    }

    // This initializer allows to provide a complete custom NSFetchRequest
    init( withFetchRequest request:NSFetchRequest<T>,  @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {
        _fetchRequest = FetchRequest<T>(fetchRequest: request)
        self.content = content
    }

}

As you can see the View is pretty simple, as said the trick is within initializer where we are able to instantiate a custom FetchRequest providing the required arguments. After that, the request will be automatically performed by the View when its render is required and the result will be passed to the custom content that is a @ViewBuilder provided in initializer itself

‘DynamicFetchRequestView’ usage sample

Define the NSManagedObject

Let assume having a Data Object named Account with two attributes

class Account : NSManagedObject {

  @NSManaged public var name: String
  @NSManaged public var age: NSNumber
}

Define the ‘DynamicFetchRequestView’ extension by Entity

Now I want filtering Accounts in my View by name, to do this I’ve to make a DynamicFetchRequestView extension for Account entity as shown in the code snippet below:

// Initializer for Account filtering
extension DynamicFetchRequestView where T : Account {

    init( withSearchText searchText: String, @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {

        let search_criteria = "name CONTAINS[c] %@"
        let predicate = NSPredicate(format: search_criteria, searchText )

        self.init( withPredicate: predicate, content: content)
    }
}

Put ‘searchable’ modifier and ‘DynamicFetchRequestView’ together

In code above we stated that we want perform a search on name attribute using a search criteria string. Now we can put all together and develop our final View

struct AccountList: View {
    @Environment(\.managedObjectContext) var managedObjectContext

    @State private var searchText = ""

    var body: some View {

        NavigationView {

            DynamicFetchRequestView( withSearchText: searchText ) { results in

                List( results, id: \.self ) { account in

                    HStack {
                      Text( account.name )
                      Text( "\(account.age)" )
                    }
                }

            }
            .searchable(text: $searchText, placement: .automatic, prompt: "search keys")
            .navigationBarTitle( Text("Account List"), displayMode: .inline )

        }
    }
}

Bonus: make a section for each alphabetic character

In code below we update AccountList implementation to include a section for each letter of alphabet

struct AccountList: View {
    @Environment(\.managedObjectContext) var managedObjectContext

    @State private var searchText = ""

    var body: some View {

        NavigationView {

            DynamicFetchRequestView( withSearchText: searchText ) { results in

                let groupByFirstCharacter = Dictionary( grouping: results, by: { $0.name.first! })

                List {
                    ForEach( groupByFirstCharacter.keys.sorted(), id: \.self ) { section in
                        Section( header: Text( String(section) ) ) {

                            ForEach( groupByFirstCharacter[section]!, id: \.self ) { account in

                                HStack {
                                  Text( account.name )
                                  Text( "\(account.age)" )
                                }
                            }
                        }
                    }
                }
            }
            .searchable(text: $searchText, placement: .automatic, prompt: "search keys")
            .navigationBarTitle( Text("Account List"), displayMode: .inline )

        }
    }
}

Conclusion

In this article I shared my experience during the discovery and deepening of SwiftUI knowledge.

Hope this help, in the meanwhile happy coding and … enjoy SwiftUI! 👋