pmusolino / Wormholy

iOS network debugging, like a wizard 🧙‍♂️
MIT License
2.31k stars 184 forks source link

Enhancement/issue 122 filter for requests #133

Open Intout opened 1 year ago

Intout commented 1 year ago

Hello

I have created filter UI and filter logic for the responses as requested in Issue 122.

I tried to take a dynamic approach without affecting existing logic and models; filter models are created by predetermined categories (code and method in current version) and from observed request model.

I am looking forward to your feedback.

UI

https://user-images.githubusercontent.com/49282941/224537491-c9842ec7-3825-4fa7-9355-86292180dfea.mp4

I did use popover view that anchored to filter button on SearchBar. View height is dynamic and consists of 2 level:

You can access from this Figma link for the design.

Level 1 Categories

Level 2 Types (Values)

Logic

Wormholy Filter Logic

You can access this logic flow with this Figma link

Model

Filters are represented with FilterModel.

On ViewModel array of FilterModel gets stored in the FilterCollectionModel.

Flow

A new FilterModel gets created with a didSet observer for Requests. Created array of filterModel gets saved or updated then filterChange notification gets posted.

On ViewController, filterChange notification being observed. On post, FilterCollectionModel gets created from filter data and existing request data gets updated via filter data and search text.

When user selects a filter type from FilterTypeViewController, cell UI gets updated on didSelectRowAt and after animation update gets requestested for selected filter, filter selectionStatus gets updated with "updateFilterModel" method.

How to add new category and types for category?

While explaining the logic, I will give example with scheme parameter of the requests.

Category

Category is Enum type with String desc for each case. We have to add new case to Enum and case array on FilterViewController. FilterModel.swift

enum FilterCategory: CaseIterable{
    case code, method, scheme

    var description: String{
        switch self {
        case .code:
            return "Code"
        case .method:
            return "Method"
        case .scheme:
            return "Scheme"
        }
    }
}

FilterViewController.swift

class FilterViewController: UIViewController {

    private static var cellHeight: CGFloat = 50

    private var filterModel: [FilterModel] = []
    private var filterCategories: [FilterCategory] = [.code, .method, .scheme]

Final result for adding category:

Scheme Screenshot

Category Types

For category types saving, updating and UI methods are handled dynamically, but we have add conditions and Equatable types for filter model creation and data filtering methods.


 func createFilterModel(from requests: [RequestModel]){

        var codeDict: [Int: Int] = [:]
        var methodDict: [String: Int] = [:]
        var schemeDict: [String: Int] = [:]
        var filterArray: [FilterModel] = []

        for request in requests {

            if request.code == 0{
                continue
            }

            if codeDict[request.code] != nil{
                codeDict[request.code]! += 1
            } else {
                codeDict[request.code] = 1
            }
            if methodDict[request.method] != nil {
                methodDict[request.method]! += 1
            } else {
                methodDict[request.method] = 1
            }
            if let scheme = request.scheme, schemeDict[scheme] != nil{
                schemeDict[scheme]! += 1
            } else if let scheme = request.scheme{
                schemeDict[scheme] = 1
            }
        }

        for codeKey in codeDict.keys{
            filterArray.append(.init(filterCategory: .code, value: codeKey, count: codeDict[codeKey] ?? 1))
        }

        for methodKey in methodDict.keys{
            filterArray.append(.init(filterCategory: .method, value: methodKey, count: methodDict[methodKey] ?? 1))
        }

        for schemeKey in schemeDict.keys{
            filterArray.append(.init(filterCategory: .scheme, value: schemeKey, count: schemeDict[schemeKey] ?? 1))
        }

        Storage.shared.saveFilters(filters: filterArray)
    }

We can see the scheme types in FilterTypeViewController but selecting type doesn't affect the logic yet.

Scheme FilterTypeViewController Screenshot

To effect the logic we have to:

Final result of FilterCollectionModel:


open class FilterCollectionModel{
    var filterCollection: [FilterModel]

    var selectedFilterCollection: [FilterModel]{
        filterCollection.filter{ filterModel -> Bool in
            filterModel.selectionStatus == .selected
        }
    }

    var selectedMethodFilterCollection: [String]{
        getSelectedFilterCollection(by: .method) as! [String]
    }

    var selectedCodeFilterCollection: [Int]{
        getSelectedFilterCollection(by: .code) as! [Int]
    }

    var selectedSchemeFilterCollection: [String]{
        getSelectedFilterCollection(by: .scheme) as! [String]
    }

    init(filterCollection: [FilterModel]) {
        self.filterCollection = filterCollection
    }

    /// Returns collection of any Equatable from current filter collection that matches with given filter category.
    /// - Parameter filterCategory: ``FilterCategory`` type that filter collection element must conform.
    /// - Returns: Filtered filter colelction values as array.
    func getFilterCollection(by filterCategory: FilterCategory) -> [any Equatable]{
        return filterCollection.filter{ filterModel -> Bool in
            filterModel.filterCategory == filterCategory
        }.map{ filterModel -> any Equatable in
            filterModel.value
        }
    }

    /// Returns collection of any Equatable from current filter collection that matches with given filter category and selected status.
    /// - Parameter filterCategory: ``FilterCategory`` type that filter collection element must conform.
    /// - Returns: Filtered filter colelction values as array.
    private func getSelectedFilterCollection(by filterCategory: FilterCategory) -> [any Equatable]{
        return filterCollection.filter{ filterModel -> Bool in
            filterModel.filterCategory == filterCategory && filterModel.selectionStatus == .selected
        }.map{ filterModel -> any Equatable in
            filterModel.value
        }
    }
}

new we can add scheme conditions to filterByFilterModels on RequestsViewController handle filtering of the data.

Final result of "filterByFilterModels"


    func filterByFilterModels(filterCollection: FilterCollectionModel?, requests: [RequestModel]) -> [RequestModel]{

        guard let filterCollection = filterCollection else{
            return requests
        }

        if filterCollection.selectedFilterCollection.isEmpty{
            return requests
        }

        // If no selected filter exists for category, contain all of the category filters.
        let codeArray: [Int] = filterCollection.selectedCodeFilterCollection.isEmpty ? filterCollection.getFilterCollection(by: .code) as! [Int] : filterCollection.selectedCodeFilterCollection

        let methodArray: [String] = filterCollection.selectedMethodFilterCollection.isEmpty ? filterCollection.getFilterCollection(by: .method) as! [String] : filterCollection.selectedMethodFilterCollection

        let schemeArray: [String] = filterCollection.selectedSchemeFilterCollection.isEmpty ? filterCollection.getFilterCollection(by: .scheme) as! [String] : filterCollection.selectedSchemeFilterCollection

        return requests.filter{ request -> Bool in
            methodArray.contains(request.method) && codeArray.contains(request.code) && schemeArray.contains(request.scheme ?? "")
        }
    }

P.S. I have added some lightweight extensions to library classes to ease the development of the UI part.

Intout commented 1 year ago

What do you think @pmusolino @gmoraleda?