fix: review notes

This commit is contained in:
Nikita Semenov 2022-08-04 18:20:08 +03:00
parent c583b8a98a
commit 47280c7cd7
10 changed files with 223 additions and 55 deletions

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
open class BaseFilterCellAppearance: FilterCellAppearanceProtocol {
public var selectedColor: UIColor
public var selectedBgColor: UIColor
public var deselectedBgColor: UIColor
public var contentInsets: UIEdgeInsets
init(selectedColor: UIColor,
selectedBgColor: UIColor,
deselectedBgColor: UIColor,
contentInsets: UIEdgeInsets) {
self.selectedColor = selectedColor
self.selectedBgColor = selectedBgColor
self.deselectedBgColor = deselectedBgColor
self.contentInsets = contentInsets
}
}
// MARK: - Default appearance
public extension BaseFilterCellAppearance {
static var defaultFilterCellAppearance: BaseFilterCellAppearance {
.init(selectedColor: .systemGreen,
selectedBgColor: .white,
deselectedBgColor: .lightGray,
contentInsets: .init(top: 4, left: 8, bottom: 4, right: 8))
}
}

View File

@ -27,18 +27,9 @@ 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
public func convertToViewModel() -> FilterCellViewModelRepresentable {
DefaultFilterCellViewModel(id: id,
title: title,
selectedColor: .green,
selectedBgColor: .white,
deselectedBgColor: .lightGray,
insets: .init(top: 4, left: 8, bottom: 4, right: 8),
isSelected: isSelected)
}
}
public extension DefaultFilterPropertyValue {
@ -46,11 +37,16 @@ public extension DefaultFilterPropertyValue {
self.id = id
self.title = title
self.excludingProperties = excludingProperties
self.cellAppearance = BaseFilterCellAppearance.defaultFilterCellAppearance
self.isSelected = false
}
}
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,28 @@
//
// 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 set }
var title: String { get set }
var appearance: FilterCellAppearanceProtocol { get set }
var isSelected: Bool { get set }
}

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 protocol FilterCellAppearanceProtocol {
var selectedColor: UIColor { get set }
var selectedBgColor: UIColor { get set }
var deselectedBgColor: UIColor { get set }
var contentInsets: UIEdgeInsets { get set }
}

View File

@ -20,14 +20,9 @@
// THE SOFTWARE.
//
public protocol FilterPropertyValueRepresenter: FilterCellViewModelConvertable {
public protocol FilterPropertyValueRepresenter {
var id: String { get }
var excludingProperties: [String]? { get }
var cellAppearance: FilterCellAppearanceProtocol { get }
var isSelected: Bool { get set }
}
public protocol FilterCellViewModelRepresentable { }
public protocol FilterCellViewModelConvertable {
func convertToViewModel() -> FilterCellViewModelRepresentable
}

View File

@ -20,22 +20,18 @@
// THE SOFTWARE.
//
import UIKit
public struct DefaultFilterCellViewModel: FilterCellViewModelProtocol, Hashable {
public protocol FilterCellViewModelProtocol: FilterCellViewModelRepresentable {
var id: String { get set }
var title: String { get set }
var selectedColor: UIColor { get set }
var insets: UIEdgeInsets { get set }
var isSelected: Bool { get set }
}
public struct DefaultFilterCellViewModel: FilterCellViewModelProtocol, FilterCellViewModelRepresentable {
public var id: String
public var title: String
public var selectedColor: UIColor
public var selectedBgColor: UIColor
public var deselectedBgColor: UIColor
public var insets: UIEdgeInsets
public var appearance: FilterCellAppearanceProtocol
public var isSelected: Bool
public static func == (lhs: DefaultFilterCellViewModel, rhs: DefaultFilterCellViewModel) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@ -20,28 +20,49 @@
// THE SOFTWARE.
//
import TIUIKitCore
import UIKit
open class DefaultFiltersViewModel: NSObject,
FiltersViewModelProtocol {
public var filters: [DefaultFilterPropertyValue]
public var selectedFilters: Set<DefaultFilterPropertyValue> = []
public var cellsViewModels: [FilterCellViewModelProtocol]
private var cellsViewModels: [FilterCellViewModelProtocol]
public var filters: [DefaultFilterPropertyValue] {
didSet {
rebuildCellsViewModels()
filtersCollection?.updateView()
}
}
public var selectedFilters: Set<DefaultFilterPropertyValue> = [] {
didSet {
reselectFilters()
rebuildCellsViewModels()
filtersCollection?.updateView()
}
}
public weak var filtersCollection: UpdatableView?
public init(filters: [DefaultFilterPropertyValue]) {
self.filters = filters
self.cellsViewModels = filters.compactMap { $0.convertToViewModel() as? FilterCellViewModelProtocol }
self.cellsViewModels = filters.compactMap {
DefaultFilterCellViewModel(id: $0.id,
title: $0.title,
appearance: $0.cellAppearance,
isSelected: $0.isSelected)
}
}
// MARK: - Public methods
// MARK: - Open methods
open func filterDidSelected(atIndexPath indexPath: IndexPath) -> [Change] {
let (selected, deselected) = toggleFilter(atIndexPath: indexPath)
let changedFilters = filters
.enumerated()
.filter { isFilterChanged($0.element, filters: selected) || isFilterChanged($0.element, filters: deselected) }
.filter { isFilterInArray($0.element, filters: selected) || isFilterInArray($0.element, filters: deselected) }
for (offset, element) in changedFilters {
cellsViewModels[offset].isSelected = selectedFilters.contains(element)
@ -57,7 +78,30 @@ open class DefaultFiltersViewModel: NSObject,
return changedItems
}
public func isFilterChanged(_ filter: DefaultFilterPropertyValue, filters: [DefaultFilterPropertyValue]) -> Bool {
open func rebuildCellsViewModels() {
cellsViewModels = filters.compactMap {
DefaultFilterCellViewModel(id: $0.id,
title: $0.title,
appearance: $0.cellAppearance,
isSelected: $0.isSelected)
}
}
// MARK: - Public methods
public func getCellsViewModels() -> [FilterCellViewModelProtocol] {
cellsViewModels
}
public func isFilterInArray(_ filter: DefaultFilterPropertyValue, filters: [DefaultFilterPropertyValue]) -> 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

@ -26,7 +26,8 @@ import UIKit
@available(iOS 13.0, *)
open class BaseFiltersCollectionView<CellType: UICollectionViewCell & ConfigurableView>: UICollectionView,
InitializableViewProtocol,
UICollectionViewDelegate where CellType.ViewModelType: Hashable {
UpdatableView,
UICollectionViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable {
public enum Section {
case main
@ -85,6 +86,7 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
open func viewDidLoad() {
register(CellType.self, forCellWithReuseIdentifier: cellsReusedIdentifier)
viewModel?.filtersCollection = self
applySnapshot()
}
@ -99,6 +101,12 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
applyChange(changes)
}
// MARK: - UpdatableView
open func updateView() {
applySnapshot()
}
// MARK: - Open methods
open func applySnapshot() {
@ -109,7 +117,7 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(viewModel.cellsViewModels as! [CellType.ViewModelType], toSection: .main)
snapshot.appendItems(viewModel.getCellsViewModels() as! [CellType.ViewModelType], toSection: .main)
collectionViewDataSource.apply(snapshot, animatingDifferences: true)
}
@ -129,21 +137,13 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
open func applyChange(_ changes: [DefaultFiltersViewModel.Change]) {
for change in changes {
guard let cell = cellForItem(at: change.indexPath) else {
guard let cell = cellForItem(at: change.indexPath) as? CellType else {
continue
}
configure(filterCell: cell, cellViewModel: change.viewModel)
// cell.configure(with: change.viewModel)
}
applySnapshot()
}
open func configure(filterCell: UICollectionViewCell, cellViewModel: FilterCellViewModelProtocol) {
guard let cellViewModel = cellViewModel as? CellType.ViewModelType else { return }
guard let configurableCell = filterCell as? CellType else { return }
configurableCell.configure(with: cellViewModel)
}
}

View File

@ -20,9 +20,9 @@
// THE SOFTWARE.
//
import UIKit
import TIUIKitCore
import TIUIElements
import UIKit
open class DefaultFilterCollectionCell: ContainerCollectionViewCell<UILabel>,
ConfigurableView {
@ -41,7 +41,7 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell<UILabel>,
self.viewModel = viewModel
wrappedView.text = viewModel.title
contentInsets = viewModel.insets
contentInsets = viewModel.appearance.contentInsets
setSelected(isSelected: viewModel.isSelected)
}
@ -49,11 +49,13 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell<UILabel>,
// MARK: - Public methods
open func setSelected(isSelected: Bool) {
let selectedColor = viewModel?.selectedColor ?? .green
let appearance = viewModel?.appearance
let selectedColor = appearance?.selectedColor ?? .green
wrappedView.textColor = isSelected ? selectedColor : .black
if isSelected {
backgroundColor = viewModel?.selectedBgColor ?? .white
backgroundColor = appearance?.selectedBgColor ?? .white
layer.borderColor = selectedColor.cgColor
layer.borderWidth = 1
} else {
@ -63,6 +65,6 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell<UILabel>,
open func setDeselectAppearance() {
layer.borderWidth = 0
backgroundColor = viewModel?.deselectedBgColor ?? .lightGray
backgroundColor = viewModel?.appearance.deselectedBgColor ?? .lightGray
}
}

View File

@ -0,0 +1,25 @@
//
// 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 UpdatableView: AnyObject {
func updateView()
}