fix: review notes

This commit is contained in:
Nikita Semenov 2022-08-05 14:29:18 +03:00
parent 7bef631668
commit 95a0045582
12 changed files with 212 additions and 176 deletions

View File

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

View File

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

View File

@ -23,6 +23,5 @@
public protocol FilterPropertyValueRepresenter {
var id: String { get }
var excludingProperties: [String]? { get }
var cellAppearance: FilterCellAppearanceProtocol { get }
var isSelected: Bool { get set }
}

View File

@ -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<Property> { 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]
}
}

View File

@ -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<Filter> { 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]
}
}

View File

@ -23,52 +23,43 @@
import TIUIKitCore
import UIKit
open class DefaultFiltersViewModel<CellViewModelType: FilterCellViewModelProtocol & Hashable>: NSObject,
FiltersViewModelProtocol {
open class BaseFilterViewModel<CellViewModelType: FilterCellViewModelProtocol & Hashable,
PropertyValue: FilterPropertyValueRepresenter & Hashable>: 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<DefaultFilterPropertyValue> = [] {
public var selectedProperties: Set<PropertyValue> = [] {
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: FilterCellViewModelProtoco
return changedItems
}
open func rebuildCellsViewModels() {
cellsViewModels = filters.compactMap {
CellViewModelType(id: $0.id,
title: $0.title,
appearance: $0.cellAppearance,
isSelected: $0.isSelected)
}
open func getCellsViewModels() -> [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)
}
}
}

View File

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

View File

@ -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<DefaultFilterCellViewModel, DefaultFilterPropertyValue> {
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)
}
}
}

View File

@ -24,40 +24,37 @@ import TIUIKitCore
import UIKit
@available(iOS 13.0, *)
open class BaseFiltersCollectionView<CellType: UICollectionViewCell & ConfigurableView>: UICollectionView,
InitializableViewProtocol,
UpdatableView,
UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable {
public enum Section {
open class BaseFiltersCollectionView<CellType: UICollectionViewCell & ConfigurableView,
PropertyValue: FilterPropertyValueRepresenter & Hashable>: UICollectionView,
InitializableViewProtocol,
UpdatableView,
UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable {
public enum DefaultSection: String {
case main
}
public typealias DataSource = UICollectionViewDiffableDataSource<Section, CellType.ViewModelType>
public typealias Snapshot = NSDiffableDataSourceSnapshot<Section, CellType.ViewModelType>
public typealias DataSource = UICollectionViewDiffableDataSource<String, CellType.ViewModelType>
public typealias Snapshot = NSDiffableDataSourceSnapshot<String, CellType.ViewModelType>
public var layout: UICollectionViewLayout
public weak var viewModel: DefaultFiltersViewModel<CellType.ViewModelType>?
public weak var viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>?
public lazy var collectionViewDataSource = createDataSource()
open var cellsReusedIdentifier: String {
"filter-cells-identifier"
}
// MARK: - Init
public init(layout: UICollectionViewLayout, viewModel: DefaultFiltersViewModel<CellType.ViewModelType>? = nil) {
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")
}
@ -85,7 +82,8 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
}
open func viewDidLoad() {
register(CellType.self, forCellWithReuseIdentifier: cellsReusedIdentifier)
registerCell()
viewModel?.filtersCollection = self
applySnapshot()
@ -109,6 +107,10 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
// MARK: - Open methods
open func registerCell() {
register(CellType.self, forCellWithReuseIdentifier: CellType().reuseIdentifier ?? "")
}
open func applySnapshot() {
guard let viewModel = viewModel else {
return
@ -116,15 +118,15 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(viewModel.getCellsViewModels() as! [CellType.ViewModelType], toSection: .main)
snapshot.appendSections([DefaultSection.main.rawValue])
snapshot.appendItems(viewModel.getCellsViewModels(), toSection: DefaultSection.main.rawValue)
collectionViewDataSource.apply(snapshot, animatingDifferences: true)
}
open func createDataSource() -> 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<CellType: UICollectionViewCell & Configurab
return DataSource(collectionView: self, cellProvider: cellProvider)
}
open func applyChange(_ changes: [DefaultFiltersViewModel<CellType.ViewModelType>.Change]) {
open func applyChange(_ changes: [BaseFilterViewModel<CellType.ViewModelType, PropertyValue>.Change]) {
for change in changes {
guard let cell = cellForItem(at: change.indexPath) as? CellType else {
continue

View File

@ -29,6 +29,10 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell<UILabel>,
public var viewModel: DefaultFilterCellViewModel?
open override var reuseIdentifier: String? {
"default-filter-cell"
}
open override func configureAppearance() {
super.configureAppearance()

View File

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