Lewis Seddon
Lewis Seddon

Reputation: 153

Boolean statement for multiple filtering Swift

I'm currently creating an app in Swift 4.2 which I'd like a filtering feature in place which allows the user to select multiple filters.

I have an array of the currently selected filters, for example ["Low", "Unread"]. I also have an array of the objects being filtered. But I'm struggling to figure out how to apply multiple filters to this array, especially due to the objects have children which in turn have properties which are filtered against. For example object array holds bulletin.importance.name which is the property that "Low" would be checked against.

The following code is a boolean returning function which will get the filters to be used on the array of bulletin objects:

return (bulletin.bulletinVersion?.issued == true) && (scopes.contains("All") || (scopes.contains((bulletin.bulletinVersion?.bulletin?.importance?.name)!) ||
        (!scopes.contains(where: {$0 == "Low" || $0 == "Normal" || $0 == "High"}))) && (scopes.contains(bulletin.read(i: bulletin.firstReadDate)) ||
            (!scopes.contains(where: {$0 == "Unread"}))) &&
            (scopes.contains(bulletin.signed(i: bulletin.signDate)) && bulletin.bulletinVersion?.bulletin?.requiresSignature == true) && scopes.contains(bulletin.favourited(i: bulletin.favourite)))

This is my current attempt of the boolean check. I wish for it to be hard set so if the user selects "High" and "Unread" it will only show objects which match both of those filters.

The function is called here, getting the filters and filtering an array of ALL bulletins into which ones should be shown based upon the filters:

currentBulletinArray = bulletinArray.filter({bulletin -> Bool in
    let doesCategoryMatch = getScopeFilters(issued: true, scopes: scope, bulletin: bulletin, signature: true)

    if searchBarIsEmpty(){
        return doesCategoryMatch
    } else {
        return doesCategoryMatch && (bulletin.bulletinVersion?.bulletin?.name?.lowercased().contains(searchText.lowercased()))!
    }
   })

I want to be able to set any combination of filters, where the returning .filter predicate will show only the bulletins which match ALL the filters. So, Unread + Unsigned + High will show all High Importance bulletins where they are unread and unsigned.

Upvotes: 3

Views: 2362

Answers (2)

Lewis Seddon
Lewis Seddon

Reputation: 153

Instead of trying to shove a huge amount of boolean combinations into one statement, I decided to place each filter into a dictionary:

 public var filters:[String: Any] = ["importance":[],
                             "unread": false,
                             "unsigned": false,
                             "favourite": false]

The importance filter will hold an array of strings, for example ["Low", "Medium", "High"].

When the filter buttons are clicked the filters will be toggled if they are a bool or appended/removed from the array:

if(!importanceToAdd.isEmpty){
            filters["importance"] = importanceToAdd
        }
        if(cell.filterTitleLabel.text == "Unread")
        {
            filters["unread"] = true
        }
        if(cell.filterTitleLabel.text == "Unsigned")
        {
            filters["unsigned"] = true
        }
        if(cell.filterTitleLabel.text == "Favourites")
        {
            filters["favourite"] = true
        }

Then, in a seperate function, I check to see if the filters are set, independently of one another. If so, filter the array fo bulletins by each of these conditions:

 if let importance = filters["importance"] as! [String]?{
        if(importance.count != 0){
            filteredBulletins = filteredBulletins.filter({importance.contains(($0.bulletinVersion?.bulletin?.importance?.name)!)})
        }
    }

    if let unread = filters["unread"] as! Bool?
    {
        if(unread)
        {
            filteredBulletins = filteredBulletins.filter({$0.firstReadDate == nil})
        }

    }

    if let unsigned = filters["unsigned"] as! Bool?
    {
        if(unsigned)
        {
            filteredBulletins = filteredBulletins.filter({$0.bulletinVersion?.bulletin?.requiresSignature == true && $0.signDate == nil})
        }
    }

    if let favourite = filters["favourite"] as! Bool?
    {
        if(favourite)
        {
            filteredBulletins = filteredBulletins.filter({$0.favourite == true})
        }
    }

The inclusion and removal of filters to a dictionary really made my needs more clear. Trying to create a monster of a boolean statement would have been infinitely difficult to be dynamic enough to match each possible combination of filters (I'm not even sure it would have been possible).

But thank you to all who commented to offer alternative solutions, you really helped me think outside of the box! :)

Upvotes: 1

DrPhill
DrPhill

Reputation: 632

I am not sure what you are asking, but here goes. First you could format your code to be more readable.... I know multiple return points were considered bad, but the tide seems to be turning a little on that. I would pick out the cases in order of importance and return any definite booleans you can find. For example (untested code as I do not know what Bulletin and Scopes are):

func foo(bulletin: Bulletin, scopes: Scopes) -> Bool {
    if bulletin.bulletinVersion?.issued == true && scopes.contains("All") {
        return true
    }
    if scopes.contains(bulletin.bulletinVersion?.bulletin?.importance?.name)! {
        return true
    }
    if scopes.contains(bulletin.bulletinVersion?.bulletin?.importance?.name)! {
        return true
    }
    if !scopes.contains(where: {$0 == "Low" || $0 == "Normal" || $0 == "High"})
        && (scopes.contains(bulletin.read(i: bulletin.firstReadDate) {
        return true
    }

    if !scopes.contains(where: {$0 == "Unread"}) 
        && scopes.contains(bulletin.signed(i: bulletin.signDate)
        && bulletin.bulletinVersion?.bulletin?.requiresSignature == true
        && scopes.contains(bulletin.favourited(i: bulletin.favourite)) {
        return true
    }
    return false
}

Note that each of these clauses tests for true and returns it if appropriate, so the function returns as soon as a true case is found. The order of the tsts might have an effect on the logic.

I would consider creating a protocol with a method that took a bulletin, scope and returned true or false.

protocol Filter {
    func test(bulletin: Bulletin, scopes: Scopes) -> Bool
}

Then creating implementors of this protocol:

class AFilter: Filter {
    func test(bulletin: Bulletin, scopes: Scopes) -> Bool {
        if bulletin.bulletinVersion?.issued == true && scopes.contains("All") {
            return true
        }
        return false
    }
}

Once you have built a list of filter instances you could do (modified to match clarified requirements):

for filter in filters {
    if !filter.test(bulletin: bulletin, scopes: scopes) {
        return false
    }
}
return true

(Bonus: note how easy it was to change the logic with this design pattern)

Upvotes: 0

Related Questions