fix: merging filters_api

This commit is contained in:
Nikita Semenov 2022-08-30 14:13:11 +03:00
parent d9cdfdec0f
commit ec1fe892ad
14 changed files with 826 additions and 0 deletions

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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)
}
}

View File

@ -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<UILabel>, 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)
}
}

View File

@ -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 })
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}

View File

@ -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 }
}

View File

@ -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
}
}

View File

@ -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<CellViewModelType: FilterCellViewModelProtocol & Hashable,
PropertyValue: FilterPropertyValueRepresenter & Hashable>: 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
}
}

View File

@ -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<DefaultFilterCellViewModel, DefaultFilterPropertyValue> {
public init(filterPropertyValues: [DefaultFilterPropertyValue]) {
let cellsViewModel = filterPropertyValues.compactMap {
DefaultFilterCellViewModel(id: $0.id,
title: $0.title,
isSelected: $0.isSelected)
}
super.init(filterPropertyValues: filterPropertyValues, cellsViewModels: cellsViewModel)
}
}

View File

@ -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<CellType: UICollectionViewCell & ConfigurableView,
PropertyValue: FilterPropertyValueRepresenter & Hashable>:
UICollectionView,
InitializableViewProtocol,
Updatable,
UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable {
public enum DefaultSection: String {
case main
}
public typealias DataSource = UICollectionViewDiffableDataSource<String, CellType.ViewModelType>
public typealias Snapshot = NSDiffableDataSourceSnapshot<String, CellType.ViewModelType>
public var layout: UICollectionViewLayout
public weak var viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>?
public lazy var collectionViewDataSource = createDataSource()
// MARK: - Init
public init(layout: UICollectionViewLayout, viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>? = 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<CellType.ViewModelType, PropertyValue>.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)
}
}
}

View File

@ -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<DefaultFilterCollectionCell, DefaultFilterPropertyValue>