diff --git a/TIEcommerce/Sources/Filters/Models/DefaultFilterPropertyValue.swift b/TIEcommerce/Sources/Filters/Models/DefaultFilterPropertyValue.swift index 7f48ee24..3990634d 100644 --- a/TIEcommerce/Sources/Filters/Models/DefaultFilterPropertyValue.swift +++ b/TIEcommerce/Sources/Filters/Models/DefaultFilterPropertyValue.swift @@ -27,7 +27,6 @@ public struct DefaultFilterPropertyValue: FilterPropertyValueRepresenter, Identi public let id: String public let title: String public let excludingProperties: [String]? - public let cellAppearance: FilterCellAppearanceProtocol public var isSelected: Bool } @@ -37,7 +36,6 @@ public extension DefaultFilterPropertyValue { self.id = id self.title = title self.excludingProperties = excludingProperties - self.cellAppearance = BaseFilterCellAppearance.defaultFilterCellAppearance self.isSelected = false } } diff --git a/TIEcommerce/Sources/Filters/Models/FilterCellViewModelProtocol.swift b/TIEcommerce/Sources/Filters/Models/FilterCellViewModelProtocol.swift index 1ed80da5..b68516d6 100644 --- a/TIEcommerce/Sources/Filters/Models/FilterCellViewModelProtocol.swift +++ b/TIEcommerce/Sources/Filters/Models/FilterCellViewModelProtocol.swift @@ -23,11 +23,6 @@ public protocol FilterCellViewModelProtocol { var id: String { get set } var title: String { get set } - var appearance: FilterCellAppearanceProtocol { get set } var isSelected: Bool { get set } - - init(id: String, - title: String, - appearance: FilterCellAppearanceProtocol, - isSelected: Bool) + var appearance: FilterCellAppearanceProtocol { get } } diff --git a/TIEcommerce/Sources/Filters/Protocols/FilterPropertyValueRepresenter.swift b/TIEcommerce/Sources/Filters/Protocols/FilterPropertyValueRepresenter.swift index 96715e7f..18aff49b 100644 --- a/TIEcommerce/Sources/Filters/Protocols/FilterPropertyValueRepresenter.swift +++ b/TIEcommerce/Sources/Filters/Protocols/FilterPropertyValueRepresenter.swift @@ -23,6 +23,5 @@ public protocol FilterPropertyValueRepresenter { var id: String { get } var excludingProperties: [String]? { get } - var cellAppearance: FilterCellAppearanceProtocol { get } var isSelected: Bool { get set } } diff --git a/TIEcommerce/Sources/Filters/Protocols/FilterViewModelProtocol.swift b/TIEcommerce/Sources/Filters/Protocols/FilterViewModelProtocol.swift new file mode 100644 index 00000000..57b4a40e --- /dev/null +++ b/TIEcommerce/Sources/Filters/Protocols/FilterViewModelProtocol.swift @@ -0,0 +1,102 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public protocol FilterViewModelProtocol: AnyObject { + + associatedtype Property: FilterPropertyValueRepresenter, Hashable + associatedtype CellViewModel: FilterCellViewModelProtocol & Hashable + + typealias Change = (indexPath: IndexPath, viewModel: CellViewModel) + + + var properties: [Property] { get set } + var selectedProperties: Set { get set } + + func filterDidSelected(atIndexPath indexPath: IndexPath) -> [Change] + func toggleProperty(atIndexPath indexPath: IndexPath) -> (selected: [Property], deselected: [Property]) + func getCellsViewModels() -> [CellViewModel] +} + +public extension FilterViewModelProtocol { + + func toggleProperty(atIndexPath indexPath: IndexPath) -> (selected: [Property], deselected: [Property]) { + guard let item = getPropertySafely(indexPath.item) else { return ([], []) } + + return toggleProperty(item) + } + + @discardableResult + private func toggleProperty(_ property: Property) -> (selected: [Property], deselected: [Property]) { + var propertiesToDeselect = [Property]() + var propertiesToSelect = [Property]() + let selectedProperty = selectedProperties.first { selectedProperty in + selectedProperty.id == property.id + } + + if let selectedProperty = selectedProperty { + // Removes previously selected filter + selectedProperties.remove(selectedProperty) + propertiesToDeselect.append(property) + } else { + // Selectes unselected filter + selectedProperties.insert(property) + propertiesToSelect.append(property) + + // If the filter has filters to exclude, these filters marks as deselected + let excludedProperties = excludeProperties(property) + propertiesToDeselect.append(contentsOf: excludedProperties) + } + + return (propertiesToSelect, propertiesToDeselect) + } + + private func excludeProperties(_ filter: Property) -> [Property] { + guard let propertiesToExclude = filter.excludingProperties, !propertiesToExclude.isEmpty else { + return [] + } + + var excludedProperties = [Property]() + + for propertiesIdToExclude in propertiesToExclude { + let propertyToExclude = selectedProperties.first { property in + property.id == propertiesIdToExclude + } + + if let propertyToExclude = propertyToExclude { + let (_, deselected) = toggleProperty(propertyToExclude) + excludedProperties.append(contentsOf: deselected) + } + } + + return excludedProperties + } + + private func getPropertySafely(_ index: Int) -> Property? { + guard index >= 0 && index < properties.count else { + return nil + } + + return properties[index] + } +} diff --git a/TIEcommerce/Sources/Filters/Protocols/FiltersViewModelProtocol.swift b/TIEcommerce/Sources/Filters/Protocols/FiltersViewModelProtocol.swift deleted file mode 100644 index d9f7c28a..00000000 --- a/TIEcommerce/Sources/Filters/Protocols/FiltersViewModelProtocol.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) 2022 Touch Instinct -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the Software), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -public protocol FiltersViewModelProtocol: AnyObject { - - associatedtype Filter: FilterPropertyValueRepresenter, Hashable - associatedtype CellViewModel: FilterCellViewModelProtocol & Hashable - - typealias Change = (indexPath: IndexPath, viewModel: CellViewModel) - - var filters: [Filter] { get set } - var selectedFilters: Set { get set } - - func filterDidSelected(atIndexPath indexPath: IndexPath) -> [Change] - func toggleFilter(atIndexPath indexPath: IndexPath) -> (selected: [Filter], deselected: [Filter]) -} - -public extension FiltersViewModelProtocol { - - func toggleFilter(atIndexPath indexPath: IndexPath) -> (selected: [Filter], deselected: [Filter]) { - guard let item = getFilterSafely(indexPath.item) else { return ([], []) } - - return toggleFilter(item) - } - - @discardableResult - private func toggleFilter(_ filter: Filter) -> (selected: [Filter], deselected: [Filter]) { - var filtersToDeselect = [Filter]() - var filtersToSelect = [Filter]() - let selectedFilter = selectedFilters.first { selectedFilter in - selectedFilter.id == filter.id - } - - if let selectedFilter = selectedFilter { - // Removes previously selected filter - selectedFilters.remove(selectedFilter) - filtersToDeselect.append(filter) - } else { - // Selectes unselected filter - selectedFilters.insert(filter) - filtersToSelect.append(filter) - - // If the filter has filters to exclude, these filters marks as deselected - let excludedFilters = excludeFilters(filter) - filtersToDeselect.append(contentsOf: excludedFilters) - } - - return (filtersToSelect, filtersToDeselect) - } - - private func excludeFilters(_ filter: Filter) -> [Filter] { - guard let filtersToExclude = filter.excludingProperties, !filtersToExclude.isEmpty else { - return [] - } - - var excludedFilters = [Filter]() - - for filtersIdToExclude in filtersToExclude { - let filterToExclude = selectedFilters.first { filter in - filter.id == filtersIdToExclude - } - - if let itemToExclude = filterToExclude { - let (_, deselected) = toggleFilter(itemToExclude) - excludedFilters.append(contentsOf: deselected) - } - } - - return excludedFilters - } - - private func getFilterSafely(_ index: Int) -> Filter? { - guard index >= 0 && index < filters.count else { - return nil - } - - return filters[index] - } -} diff --git a/TIEcommerce/Sources/Filters/ViewModels/DefaultFiltersViewModel.swift b/TIEcommerce/Sources/Filters/ViewModels/BaseFilterViewModel.swift similarity index 52% rename from TIEcommerce/Sources/Filters/ViewModels/DefaultFiltersViewModel.swift rename to TIEcommerce/Sources/Filters/ViewModels/BaseFilterViewModel.swift index eb2fb2f9..a4f7d797 100644 --- a/TIEcommerce/Sources/Filters/ViewModels/DefaultFiltersViewModel.swift +++ b/TIEcommerce/Sources/Filters/ViewModels/BaseFilterViewModel.swift @@ -23,52 +23,43 @@ import TIUIKitCore import UIKit -open class DefaultFiltersViewModel: NSObject, - FiltersViewModelProtocol { +open class BaseFilterViewModel: FilterViewModelProtocol { + // MARK: - FilterViewModelProtocol + + public typealias Property = PropertyValue public typealias CellViewModel = CellViewModelType - private var cellsViewModels: [CellViewModelType] + private var cellsViewModels: [CellViewModelType] = [] - public var filters: [DefaultFilterPropertyValue] { + public var properties: [PropertyValue] = [] { didSet { - rebuildCellsViewModels() filtersCollection?.updateView() } } - - public var selectedFilters: Set = [] { + public var selectedProperties: Set = [] { didSet { - reselectFilters() - rebuildCellsViewModels() filtersCollection?.updateView() } } public weak var filtersCollection: UpdatableView? - public init(filters: [DefaultFilterPropertyValue]) { - self.filters = filters - self.cellsViewModels = filters.compactMap { - CellViewModelType(id: $0.id, - title: $0.title, - appearance: $0.cellAppearance, - isSelected: $0.isSelected) - } + public init(filters: [PropertyValue]) { + self.properties = filters } - // MARK: - Open methods - open func filterDidSelected(atIndexPath indexPath: IndexPath) -> [Change] { - let (selected, deselected) = toggleFilter(atIndexPath: indexPath) + let (selected, deselected) = toggleProperty(atIndexPath: indexPath) - let changedFilters = filters + let changedFilters = properties .enumerated() .filter { isFilterInArray($0.element, filters: selected) || isFilterInArray($0.element, filters: deselected) } for (offset, element) in changedFilters { - cellsViewModels[offset].isSelected = selectedFilters.contains(element) - filters[offset].isSelected = selectedFilters.contains(element) + cellsViewModels[offset].isSelected = selectedProperties.contains(element) + properties[offset].isSelected = selectedProperties.contains(element) } let changedItems = changedFilters @@ -80,30 +71,13 @@ open class DefaultFiltersViewModel [CellViewModelType] { + cellsViewModels } // MARK: - Public methods - public func getCellsViewModels() -> [FilterCellViewModelProtocol] { - cellsViewModels - } - - public func isFilterInArray(_ filter: DefaultFilterPropertyValue, filters: [DefaultFilterPropertyValue]) -> Bool { + public func isFilterInArray(_ filter: PropertyValue, filters: [PropertyValue]) -> Bool { filters.contains(where: { $0.id == filter.id }) } - - public func reselectFilters() { - let selectedFilters = Array(selectedFilters) - - filters.enumerated().forEach { - filters[$0.offset].isSelected = isFilterInArray($0.element, filters: selectedFilters) - } - } } diff --git a/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterCellViewModel.swift b/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterCellViewModel.swift index 00ad855e..689a1752 100644 --- a/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterCellViewModel.swift +++ b/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterCellViewModel.swift @@ -24,17 +24,18 @@ public struct DefaultFilterCellViewModel: FilterCellViewModelProtocol, Hashable public var id: String public var title: String - public var appearance: FilterCellAppearanceProtocol public var isSelected: Bool + public var appearance: FilterCellAppearanceProtocol { + BaseFilterCellAppearance.defaultFilterCellAppearance + } + public init(id: String, title: String, - appearance: FilterCellAppearanceProtocol, isSelected: Bool) { self.id = id self.title = title - self.appearance = appearance self.isSelected = isSelected } diff --git a/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterViewModel.swift b/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterViewModel.swift new file mode 100644 index 00000000..558f3f36 --- /dev/null +++ b/TIEcommerce/Sources/Filters/ViewModels/DefaultFilterViewModel.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +open class DefaultFilterViewModel: BaseFilterViewModel { + + private var cellsViewModels: [DefaultFilterCellViewModel] = [] + + public override init(filters: [DefaultFilterPropertyValue]) { + super.init(filters: filters) + self.cellsViewModels = filters.compactMap { + DefaultFilterCellViewModel(id: $0.id, + title: $0.title, + isSelected: $0.isSelected) + } + } +} diff --git a/TIEcommerce/Sources/Filters/Views/BaseFiltersCollectionView.swift b/TIEcommerce/Sources/Filters/Views/BaseFiltersCollectionView.swift index 1ca3f890..a8c2d78e 100644 --- a/TIEcommerce/Sources/Filters/Views/BaseFiltersCollectionView.swift +++ b/TIEcommerce/Sources/Filters/Views/BaseFiltersCollectionView.swift @@ -24,40 +24,37 @@ import TIUIKitCore import UIKit @available(iOS 13.0, *) -open class BaseFiltersCollectionView: UICollectionView, - InitializableViewProtocol, - UpdatableView, - UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable { - - public enum Section { +open class BaseFiltersCollectionView: UICollectionView, + InitializableViewProtocol, + UpdatableView, + UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable { + public enum DefaultSection: String { case main } - public typealias DataSource = UICollectionViewDiffableDataSource - public typealias Snapshot = NSDiffableDataSourceSnapshot + public typealias DataSource = UICollectionViewDiffableDataSource + public typealias Snapshot = NSDiffableDataSourceSnapshot public var layout: UICollectionViewLayout - public weak var viewModel: DefaultFiltersViewModel? + public weak var viewModel: BaseFilterViewModel? public lazy var collectionViewDataSource = createDataSource() - open var cellsReusedIdentifier: String { - "filter-cells-identifier" - } - // MARK: - Init - public init(layout: UICollectionViewLayout, viewModel: DefaultFiltersViewModel? = nil) { + public init(layout: UICollectionViewLayout, viewModel: BaseFilterViewModel? = nil) { self.layout = layout self.viewModel = viewModel - + super.init(frame: .zero, collectionViewLayout: layout) initializeView() viewDidLoad() } + @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -85,7 +82,8 @@ open class BaseFiltersCollectionView DataSource { - let cellProvider: DataSource.CellProvider = { [weak self] collectionView, indexPath, itemIdentifier in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self?.cellsReusedIdentifier ?? "", + let cellProvider: DataSource.CellProvider = {collectionView, indexPath, itemIdentifier in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellType().reuseIdentifier ?? "", for: indexPath) as? CellType cell?.configure(with: itemIdentifier) @@ -135,7 +137,7 @@ open class BaseFiltersCollectionView.Change]) { + open func applyChange(_ changes: [BaseFilterViewModel.Change]) { for change in changes { guard let cell = cellForItem(at: change.indexPath) as? CellType else { continue diff --git a/TIEcommerce/Sources/Filters/Views/DefaultFilterCollectionCell.swift b/TIEcommerce/Sources/Filters/Views/DefaultFilterCollectionCell.swift index 8ad344cc..06bfc516 100644 --- a/TIEcommerce/Sources/Filters/Views/DefaultFilterCollectionCell.swift +++ b/TIEcommerce/Sources/Filters/Views/DefaultFilterCollectionCell.swift @@ -29,6 +29,10 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell, public var viewModel: DefaultFilterCellViewModel? + open override var reuseIdentifier: String? { + "default-filter-cell" + } + open override func configureAppearance() { super.configureAppearance() diff --git a/TIEcommerce/Sources/Filters/Views/DefaultFiltersCollectionView.swift b/TIEcommerce/Sources/Filters/Views/DefaultFiltersCollectionView.swift new file mode 100644 index 00000000..fd83ddaa --- /dev/null +++ b/TIEcommerce/Sources/Filters/Views/DefaultFiltersCollectionView.swift @@ -0,0 +1,26 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@available(iOS 13.0, *) +open class DefaultFiltersCollectionView: BaseFiltersCollectionView { + +} \ No newline at end of file diff --git a/TIUIKitCore/Sources/Extensions/UICollectionView/Layout Helpers/NSDirectionalEdgeInsets+Init.swift b/TIUIKitCore/Sources/Extensions/UIKit/NSDirectionalEdgeInsets+Init.swift similarity index 100% rename from TIUIKitCore/Sources/Extensions/UICollectionView/Layout Helpers/NSDirectionalEdgeInsets+Init.swift rename to TIUIKitCore/Sources/Extensions/UIKit/NSDirectionalEdgeInsets+Init.swift