From ec1fe892ad283377cefacf970f3837bb943ea113 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Tue, 30 Aug 2022 14:13:11 +0300 Subject: [PATCH] fix: merging filters_api --- .../Models/FilterCellStateAppearance.swift | 49 ++++++ .../Models/FilterCellViewModelProtocol.swift | 27 +++ .../DefaultFilterCellViewModel.swift | 45 +++++ .../Views/DefaultFilterCollectionCell.swift | 78 ++++++++ ...Array+FilterPropertyValueRepresenter.swift | 30 ++++ ...UICollectionViewLayout+DefaultLayout.swift | 58 ++++++ .../Models/DefaultFilterPropertyValue.swift | 52 ++++++ .../Models/FiltersLayoutConfiguration.swift | 60 +++++++ .../FilterPropertyValueRepresenter.swift | 27 +++ .../Protocols/FilterViewModelProtocol.swift | 93 ++++++++++ .../ViewModels/BaseFilterViewModel.swift | 83 +++++++++ .../ViewModels/DefaultFilterViewModel.swift | 34 ++++ .../Views/BaseFiltersCollectionView.swift | 166 ++++++++++++++++++ .../Views/DefaultFiltersCollectionView.swift | 24 +++ 14 files changed, 826 insertions(+) create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellStateAppearance.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellViewModelProtocol.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/ViewModels/DefaultFilterCellViewModel.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Views/DefaultFilterCollectionCell.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/Array+FilterPropertyValueRepresenter.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/UICollectionViewLayout+DefaultLayout.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/DefaultFilterPropertyValue.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/FiltersLayoutConfiguration.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterPropertyValueRepresenter.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterViewModelProtocol.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/BaseFilterViewModel.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/DefaultFilterViewModel.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift create mode 100644 TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/DefaultFiltersCollectionView.swift diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellStateAppearance.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellStateAppearance.swift new file mode 100644 index 00000000..785fbced --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellStateAppearance.swift @@ -0,0 +1,49 @@ +// +// 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 UIKit + +public struct FilterCellStateAppearance { + + public let borderColor: UIColor + public let backgroundColor: UIColor + public let fontColor: UIColor + + public let borderWidth: CGFloat + public let contentInsets: UIEdgeInsets + public let cornerRadius: CGFloat + + public init(borderColor: UIColor, + backgroundColor: UIColor, + fontColor: UIColor, + borderWidth: CGFloat, + contentInsets: UIEdgeInsets = .init(top: 4, left: 8, bottom: 4, right: 8), + cornerRadius: CGFloat = 6) { + + self.borderColor = borderColor + self.backgroundColor = backgroundColor + self.fontColor = fontColor + self.borderWidth = borderWidth + self.contentInsets = contentInsets + self.cornerRadius = cornerRadius + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellViewModelProtocol.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellViewModelProtocol.swift new file mode 100644 index 00000000..282416d2 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Models/FilterCellViewModelProtocol.swift @@ -0,0 +1,27 @@ +// +// 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. +// + +public protocol FilterCellViewModelProtocol { + var id: String { get } + var title: String { get } + var isSelected: Bool { get set } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/ViewModels/DefaultFilterCellViewModel.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/ViewModels/DefaultFilterCellViewModel.swift new file mode 100644 index 00000000..e01cae45 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/ViewModels/DefaultFilterCellViewModel.swift @@ -0,0 +1,45 @@ +// +// 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. +// + +public struct DefaultFilterCellViewModel: FilterCellViewModelProtocol, Hashable { + + public let id: String + public let title: String + public var isSelected: Bool + + public init(id: String, + title: String, + isSelected: Bool) { + + self.id = id + self.title = title + self.isSelected = isSelected + } + + public static func == (lhs: DefaultFilterCellViewModel, rhs: DefaultFilterCellViewModel) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Views/DefaultFilterCollectionCell.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Views/DefaultFilterCollectionCell.swift new file mode 100644 index 00000000..19895540 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionCell/Views/DefaultFilterCollectionCell.swift @@ -0,0 +1,78 @@ +// +// 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 TIUIKitCore +import TIUIElements +import UIKit + +open class DefaultFilterCollectionCell: ContainerCollectionViewCell, ConfigurableView { + + open var selectedStateAppearance: FilterCellStateAppearance { + .defaultSelectedAppearance + } + + open var normalStateAppearance: FilterCellStateAppearance { + .defaultNormalAppearance + } + + open override var isSelected: Bool { + didSet { + let appearance = isSelected ? selectedStateAppearance : normalStateAppearance + updateAppearance(with: appearance) + } + } + + open override func configureAppearance() { + super.configureAppearance() + + updateAppearance(with: normalStateAppearance) + } + + // MARK: - ConfigurableView + + open func configure(with viewModel: DefaultFilterCellViewModel) { + wrappedView.text = viewModel.title + } + + // MARK: - Open methdos + + open func updateAppearance(with appearance: FilterCellStateAppearance) { + contentInsets = appearance.contentInsets + wrappedView.textColor = appearance.fontColor + + backgroundColor = appearance.backgroundColor + layer.borderColor = appearance.borderColor.cgColor + layer.borderWidth = appearance.borderWidth + layer.round(corners: .allCorners, radius: appearance.cornerRadius) + } +} + +extension FilterCellStateAppearance { + static var defaultSelectedAppearance: FilterCellStateAppearance { + .init(borderColor: .systemGreen, backgroundColor: .white, fontColor: .systemGreen, borderWidth: 1) + } + + static var defaultNormalAppearance: FilterCellStateAppearance { + + .init(borderColor: .lightGray, backgroundColor: .lightGray, fontColor: .black, borderWidth: 0) + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/Array+FilterPropertyValueRepresenter.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/Array+FilterPropertyValueRepresenter.swift new file mode 100644 index 00000000..1a28fb70 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/Array+FilterPropertyValueRepresenter.swift @@ -0,0 +1,30 @@ +// +// 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 UIKit + +public extension Array where Element: FilterPropertyValueRepresenter { + + func contains(_ property: Element) -> Bool { + contains(where: { $0.id == property.id }) + } +} \ No newline at end of file diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/UICollectionViewLayout+DefaultLayout.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/UICollectionViewLayout+DefaultLayout.swift new file mode 100644 index 00000000..48ca34de --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Helpers/UICollectionViewLayout+DefaultLayout.swift @@ -0,0 +1,58 @@ +// +// 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 UIKit +import TIUIKitCore + +@available(iOS 13, *) +public extension UICollectionViewLayout { + + static func gridLayout(_ configuration: FiltersLayoutConfiguration) -> UICollectionViewLayout { + let item = NSCollectionLayoutItem(layoutSize: configuration.itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), + heightDimension: configuration.itemSize.heightDimension) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + group.interItemSpacing = .fixed(configuration.horizontalItemSpacing) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(insets: configuration.contentInsets) + section.interGroupSpacing = configuration.verticalItemSpacing + + return UICollectionViewCompositionalLayout(section: section) + } + + static func horizontalScrollLayout(_ configuration: FiltersLayoutConfiguration) -> UICollectionViewLayout { + let item = NSCollectionLayoutItem(layoutSize: configuration.itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: configuration.itemSize.widthDimension, + heightDimension: .fractionalHeight(1)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(insets: configuration.contentInsets) + section.interGroupSpacing = configuration.horizontalItemSpacing + section.orthogonalScrollingBehavior = .continuous + + return UICollectionViewCompositionalLayout(section: section) + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/DefaultFilterPropertyValue.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/DefaultFilterPropertyValue.swift new file mode 100644 index 00000000..b4bcb5ef --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/DefaultFilterPropertyValue.swift @@ -0,0 +1,52 @@ +// +// 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 UIKit + +public struct DefaultFilterPropertyValue: FilterPropertyValueRepresenter { + + public let id: String + public let title: String + public let excludingPropertiesIds: [String] + + public var isSelected: Bool + + public init(id: String, title: String, excludingPropertiesIds: [String] = [], isSelected: Bool = false) { + self.id = id + self.title = title + self.excludingPropertiesIds = excludingPropertiesIds + self.isSelected = isSelected + } +} + +@available(iOS 13, *) +extension DefaultFilterPropertyValue: Identifiable { } + +extension DefaultFilterPropertyValue: Hashable { + public static func == (lhs: DefaultFilterPropertyValue, rhs: DefaultFilterPropertyValue) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/FiltersLayoutConfiguration.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/FiltersLayoutConfiguration.swift new file mode 100644 index 00000000..06258f52 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Models/FiltersLayoutConfiguration.swift @@ -0,0 +1,60 @@ +// +// 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 UIKit + +@available(iOS 13, *) +public struct FiltersLayoutConfiguration { + + public var itemSize: NSCollectionLayoutSize + public var horizontalItemSpacing: CGFloat + public var verticalItemSpacing: CGFloat + public var contentInsets: UIEdgeInsets + + public init(itemSize: NSCollectionLayoutSize = .init(widthDimension: .estimated(36), + heightDimension: .estimated(36)), + horizontalItemSpacing: CGFloat = .zero, + verticalItemSpacing: CGFloat = .zero, + contentInsets: UIEdgeInsets) { + + self.itemSize = itemSize + self.horizontalItemSpacing = horizontalItemSpacing + self.verticalItemSpacing = verticalItemSpacing + self.contentInsets = contentInsets + } +} + +@available(iOS 13, *) +public extension FiltersLayoutConfiguration { + static let horizontalScrollConfiguration = FiltersLayoutConfiguration(horizontalItemSpacing: 16, + contentInsets: .init(top: .zero, + left: 8, + bottom: .zero, + right: 8)) + + static let gridConfiguration = FiltersLayoutConfiguration(horizontalItemSpacing: 16, + verticalItemSpacing: 16, + contentInsets: .init(top: .zero, + left: 8, + bottom: .zero, + right: 8)) +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterPropertyValueRepresenter.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterPropertyValueRepresenter.swift new file mode 100644 index 00000000..27c9df50 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterPropertyValueRepresenter.swift @@ -0,0 +1,27 @@ +// +// 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. +// + +public protocol FilterPropertyValueRepresenter { + var id: String { get } + var excludingPropertiesIds: [String] { get } + var isSelected: Bool { get set } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterViewModelProtocol.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterViewModelProtocol.swift new file mode 100644 index 00000000..afc77da0 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Protocols/FilterViewModelProtocol.swift @@ -0,0 +1,93 @@ +// +// 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 +import TISwiftUtils + +public protocol FilterViewModelProtocol: AnyObject { + + associatedtype PropertyValue: FilterPropertyValueRepresenter & Hashable + associatedtype CellViewModelType: FilterCellViewModelProtocol & Hashable + + var values: [PropertyValue] { get set } + var selectedValues: [PropertyValue] { get set } + + func filterDidSelected(atIndexPath indexPath: IndexPath) -> [(indexPath: IndexPath, viewModel: CellViewModelType)] + func toggleProperty(atIndexPath indexPath: IndexPath) -> (selected: [PropertyValue], deselected: [PropertyValue]) +} + +public extension FilterViewModelProtocol { + + func toggleProperty(atIndexPath indexPath: IndexPath) -> (selected: [PropertyValue], deselected: [PropertyValue]) { + guard let item = values[safe: indexPath.item] else { return ([], []) } + + return toggleProperty(item) + } + + @discardableResult + private func toggleProperty(_ value: PropertyValue) -> (selected: [PropertyValue], deselected: [PropertyValue]) { + var valuesToDeselect = [PropertyValue]() + var valuesToSelect = [PropertyValue]() + let selectedValueId = selectedValues.firstIndex { selectedValue in + selectedValue.id == value.id + } + + if let selectedValueId = selectedValueId { + // Removes previously selected filter + selectedValues.remove(at: selectedValueId) + valuesToDeselect.append(value) + } else { + // Selectes unselected filter + selectedValues.append(value) + valuesToSelect.append(value) + + // If the filter has filters to exclude, these filters marks as deselected + let excludedValues = excludeValues(value) + valuesToDeselect.append(contentsOf: excludedValues) + } + + return (valuesToSelect, valuesToDeselect) + } + + private func excludeValues(_ values: PropertyValue) -> [PropertyValue] { + let valuesIdsToExclude = values.excludingPropertiesIds + + guard !valuesIdsToExclude.isEmpty else { + return [] + } + + var excludedValues = [PropertyValue]() + + for valuesIdToExclude in valuesIdsToExclude { + let propertyToExclude = selectedValues.first { property in + property.id == valuesIdToExclude + } + + if let propertyToExclude = propertyToExclude { + let (_, deselected) = toggleProperty(propertyToExclude) + excludedValues.append(contentsOf: deselected) + } + } + + return excludedValues + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/BaseFilterViewModel.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/BaseFilterViewModel.swift new file mode 100644 index 00000000..a41f13fd --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/BaseFilterViewModel.swift @@ -0,0 +1,83 @@ +// +// 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 TISwiftUtils +import UIKit + +open class BaseFilterViewModel: FilterViewModelProtocol { + + // MARK: - FilterViewModelProtocol + + public typealias Change = (indexPath: IndexPath, viewModel: CellViewModelType) + + public var values: [PropertyValue] = [] { + didSet { + filtersCollection?.update() + } + } + public var selectedValues: [PropertyValue] = [] { + didSet { + filtersCollection?.update() + } + } + + public weak var filtersCollection: Updatable? + + public private(set) var cellsViewModels: [CellViewModelType] + + public init(filterPropertyValues: [PropertyValue], cellsViewModels: [CellViewModelType] = []) { + self.values = filterPropertyValues + self.cellsViewModels = cellsViewModels + } + + open func filterDidSelected(atIndexPath indexPath: IndexPath) -> [Change] { + let (selected, deselected) = toggleProperty(atIndexPath: indexPath) + + let changedValues = values + .enumerated() + .filter { selected.contains($0.element) || deselected.contains($0.element) } + + changedValues.forEach { index, element in + let isSelected = selectedValues.contains(element) + + setSelectedCell(atIndex: index, isSelected: isSelected) + setSelectedProperty(atIndex: index, isSelected: isSelected) + } + + let changedItems = changedValues + .map { + Change(indexPath: IndexPath(item: $0.offset, section: .zero), + viewModel: cellsViewModels[$0.offset]) + } + + return changedItems + } + + open func setSelectedCell(atIndex index: Int, isSelected: Bool) { + cellsViewModels[index].isSelected = isSelected + } + + open func setSelectedProperty(atIndex index: Int, isSelected: Bool) { + values[index].isSelected = isSelected + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/DefaultFilterViewModel.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/DefaultFilterViewModel.swift new file mode 100644 index 00000000..5a668148 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/ViewModels/DefaultFilterViewModel.swift @@ -0,0 +1,34 @@ +// +// 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 { + + public init(filterPropertyValues: [DefaultFilterPropertyValue]) { + let cellsViewModel = filterPropertyValues.compactMap { + DefaultFilterCellViewModel(id: $0.id, + title: $0.title, + isSelected: $0.isSelected) + } + + super.init(filterPropertyValues: filterPropertyValues, cellsViewModels: cellsViewModel) + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift new file mode 100644 index 00000000..832ffbd1 --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/BaseFiltersCollectionView.swift @@ -0,0 +1,166 @@ +// +// 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 TISwiftUtils +import TIUIKitCore +import UIKit + +@available(iOS 13.0, *) +open class BaseFiltersCollectionView: + UICollectionView, + InitializableViewProtocol, + Updatable, + UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable { + + public enum DefaultSection: String { + case main + } + + public typealias DataSource = UICollectionViewDiffableDataSource + public typealias Snapshot = NSDiffableDataSourceSnapshot + + public var layout: UICollectionViewLayout + + public weak var viewModel: BaseFilterViewModel? + + public lazy var collectionViewDataSource = createDataSource() + + // MARK: - Init + + 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") + } + + // MARK: - Life cycle + + open func addViews() { + // override in subclass + } + + open func bindViews() { + delegate = self + } + + open func configureLayout() { + // override in subclass + } + + open func configureAppearance() { + backgroundColor = .white + } + + open func localize() { + // override in subclass + } + + open func viewDidLoad() { + registerCell() + + viewModel?.filtersCollection = self + } + + open func viewDidAppear() { + applySnapshot() + } + + // MARK: - UICollectionViewDelegate + + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + filterDidTapped(atIndexPath: indexPath) + } + + open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + filterDidTapped(atIndexPath: indexPath) + } + + // MARK: - UpdatableView + + open func update() { + applySnapshot() + } + + // MARK: - Open methods + + open func registerCell() { + register(CellType.self, forCellWithReuseIdentifier: CellType.reuseIdentifier) + } + + open func filterDidTapped(atIndexPath indexPath: IndexPath) { + guard let viewModel = viewModel else { return } + + let changes = viewModel.filterDidSelected(atIndexPath: indexPath) + + applyChange(changes) + } + + open func applySnapshot() { + guard let viewModel = viewModel else { + return + } + + var snapshot = Snapshot() + + snapshot.appendSections([DefaultSection.main.rawValue]) + snapshot.appendItems(viewModel.cellsViewModels, toSection: DefaultSection.main.rawValue) + + collectionViewDataSource.apply(snapshot, animatingDifferences: true) + } + + open func createDataSource() -> DataSource { + let cellProvider: DataSource.CellProvider = { collectionView, indexPath, itemIdentifier in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellType.reuseIdentifier, + for: indexPath) as? CellType + + cell?.configure(with: itemIdentifier) + + return cell + } + + return DataSource(collectionView: self, cellProvider: cellProvider) + } + + open func applyChange(_ changes: [BaseFilterViewModel.Change]) { + changes.forEach { change in + guard let cell = cellForItem(at: change.indexPath) as? CellType else { + return + } + + cell.configure(with: change.viewModel) + + change.viewModel.isSelected + ? selectItem(at: change.indexPath, animated: false, scrollPosition: []) + : deselectItem(at: change.indexPath, animated: false) + } + } +} diff --git a/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/DefaultFiltersCollectionView.swift b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/DefaultFiltersCollectionView.swift new file mode 100644 index 00000000..8caef21e --- /dev/null +++ b/TIEcommerce/Sources/Filters/TagsFilters/FiltersCollectionView/Views/DefaultFiltersCollectionView.swift @@ -0,0 +1,24 @@ +// +// 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, *) +public typealias DefaultFiltersCollectionView = BaseFiltersCollectionView