Search code examples
swiftoopsearchencapsulationobject-oriented-analysis

Swift OOP: How to encapsulate Search behavior


Currently I have the following code:

import UIKit

struct ToBeSearched {
    var value1 = "1"
    var value2 = "3"
    var value3 = "3"
    var boolean = true
}

var data = [ToBeSearched]()
var completeData = [ToBeSearched]()

public func updateSearchResults(for searchController: UISearchController) {
    if let text = searchController.searchBar.text,
        !text.isEmpty {
        data = completeData.filter{
            // How to encapsulate this behavior, i.e. to extend it to use new values (value2, value2...)
            $0.value1.lowercased().contains(text.lowercased())
        }
    } else {
        data = completeData
    }
    reloadResults()
}

It's a simple search code that finds all the values where value1 contain search text.

What if I'd like to match also value2 and value3? How could I extract the search logic, so that it could be altered separately, without touching the main code.

Currently, I'd have to use the binary OR operator to go through all the cases:

let searchText = text.lowercased()
$0.value1.lowercased().contains(searchText) ||
$0.value2.lowercased().contains(searchText) ||
$0.value3.lowercased().contains(searchText)
...

Is there a more elegant way of achieving the same result?


Solution

  • Method 1: Specifiy the properties to search using [KeyPath]:

    If you just want to flexibly specify which fields to search of the ToBeSearched struct, you can pass in an array [KeyPath] of the properties to search, and use contains with a closure inside of filter to check if any of the properties identified by the keyPaths contain the text you are searching for:

    public func updateSearchResults(for searchController: UISearchController, using keyPaths: [KeyPath<ToBeSearched, String>]) {
        if let text = searchController.searchBar.text,
            !text.isEmpty {
            data = completeData.filter { element in
                keyPaths.contains { keyPath in element[keyPath: keyPath].lowercased().contains(text.lowercased()) }
            }
        } else {
            data = completeData
        }
        reloadResults()
    }
    

    Example:

    To search value1 and value2:

    updateSearchResults(for: searchController, using: [\.value1, \.value2])
    

    Method 2: Pass in a closure for the filter method:

    public func updateSearchResults(for searchController: UISearchController, using filterProc: (ToBeSearched) -> Bool)  {
        if let text = searchController.searchBar.text,
            !text.isEmpty {
            data = completeData.filter(filterProc)
            }
        } else {
            data = completeData
        }
        reloadResults()
    }
    

    Example:

    let filterProc: (ToBeSearched) -> Bool = {
        $0.value1.lowercased().contains(searchText) ||
        $0.value2.lowercased().contains(searchText)
    }
    
    updateSearchResults(for: searchController, using: filterProc)