fix: update table director with diffable data source

This commit is contained in:
Nikita Semenov 2022-08-30 21:34:29 +03:00
parent ec1fe892ad
commit 0bc8574a32
13 changed files with 339 additions and 248 deletions

View File

@ -1,43 +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.
//
public struct DefaultFilterRowViewModel: FilterRowRepresentable, Equatable {
public let id: String
public let title: String
public var appearance: FilterCellAppearanceProtocol
public var isSelected: Bool
public init(id: String,
title: String,
appearance: FilterCellAppearanceProtocol,
isSelected: Bool) {
self.id = id
self.title = title
self.appearance = appearance
self.isSelected = isSelected
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}

View File

@ -20,13 +20,13 @@
// THE SOFTWARE.
//
public protocol FilterListPickerConfigurator: AnyObject {
associatedtype RowViewModel: FilterRowRepresentable & Equatable
var visibleValues: [RowViewModel] { get }
var isMultiselectionEnabled: Bool { get set }
var isFinishWithSeparator: Bool { get }
func setSelected(model: RowViewModel, isSelected: Bool)
}
//public protocol FilterListPickerConfigurator: AnyObject {
//
// associatedtype RowViewModel: FilterRowRepresentable & Equatable
//
// var visibleValues: [RowViewModel] { get }
// var isMultiselectionEnabled: Bool { get set }
// var isFinishWithSeparator: Bool { get }
//
// func setSelected(model: RowViewModel, isSelected: Bool)
//}

View File

@ -23,8 +23,8 @@
import TableKit
import TIUIElements
public protocol FilterTableSectionBuilder {
associatedtype CellType: BaseSeparatorCell & ConfigurableCell
func makeSection<ViewModel: FilterListPickerConfigurator>(with viewModel: ViewModel) -> TableSection where ViewModel.RowViewModel == CellType.CellData
}
//public protocol FilterTableSectionBuilder {
// associatedtype CellType: BaseSeparatorCell & ConfigurableCell
//
// func makeSection<ViewModel: FilterListPickerConfigurator>(with viewModel: ViewModel) -> TableSection where ViewModel.RowViewModel == CellType.CellData
//}

View File

@ -21,5 +21,5 @@
//
public protocol FiltersPickerDelegate: AnyObject {
func filters(didSelect filters: [FilterRowRepresentable])
func filters(didSelect filters: [FilterPropertyValueRepresenter])
}

View File

@ -20,74 +20,74 @@
// THE SOFTWARE.
//
open class BaseListFilterViewModel<RowViewModelType: FilterRowRepresentable & Equatable>: FilterListPickerConfigurator {
public typealias RowViewModel = RowViewModelType
public var rowViewModels: [RowViewModelType]
public weak var delegate: FiltersPickerDelegate?
public var initiallySelectedValues: [RowViewModelType]?
open var isFinishWithSeparator: Bool {
true
}
open var isMultiselectionEnabled: Bool = false
open var areInitiallySelectedValuesTheSame: Bool {
guard let initiallySelectedValues = initiallySelectedValues else {
return false
}
return initiallySelectedValues == selectedValues
}
open var areThereAnyValuesSelected: Bool {
if let _ = rowViewModels.first(where: { $0.isSelected }) {
return false
}
return true
}
open var selectedValues: [RowViewModelType] {
rowViewModels.filter { $0.isSelected }
}
open var visibleValues: [RowViewModelType] {
rowViewModels
}
open var visibleSelectedIndexes: [Int] {
visibleValues.enumerated().compactMap {
$0.element.isSelected ? $0.offset : nil
}
}
public init(rowViewModels: [RowViewModelType], isMultiselectionEnabled: Bool) {
self.rowViewModels = rowViewModels
self.isMultiselectionEnabled = isMultiselectionEnabled
}
open func viewDidLoad() {
initiallySelectedValues = selectedValues
}
open func setSelected(model: RowViewModelType, isSelected: Bool) {
if !isMultiselectionEnabled {
rowViewModels.enumerated().forEach {
rowViewModels[$0.offset].isSelected = false
}
}
if let index = rowViewModels.firstIndex(of: model) {
rowViewModels[index].isSelected = isSelected
}
if !selectedValues.isEmpty {
delegate?.filters(didSelect: selectedValues)
}
}
}
//open class BaseListFilterViewModel<RowViewModelType: FilterRowRepresentable & Equatable>: FilterListPickerConfigurator {
//
// public typealias RowViewModel = RowViewModelType
//
// public var rowViewModels: [RowViewModelType]
//
// public weak var delegate: FiltersPickerDelegate?
//
// public var initiallySelectedValues: [RowViewModelType]?
//
// open var isFinishWithSeparator: Bool {
// true
// }
//
// open var isMultiselectionEnabled: Bool = false
//
// open var areInitiallySelectedValuesTheSame: Bool {
// guard let initiallySelectedValues = initiallySelectedValues else {
// return false
// }
//
// return initiallySelectedValues == selectedValues
// }
//
// open var areThereAnyValuesSelected: Bool {
// if let _ = rowViewModels.first(where: { $0.isSelected }) {
// return false
// }
//
// return true
// }
//
// open var selectedValues: [RowViewModelType] {
// rowViewModels.filter { $0.isSelected }
// }
//
// open var visibleValues: [RowViewModelType] {
// rowViewModels
// }
//
// open var visibleSelectedIndexes: [Int] {
// visibleValues.enumerated().compactMap {
// $0.element.isSelected ? $0.offset : nil
// }
// }
//
// public init(rowViewModels: [RowViewModelType], isMultiselectionEnabled: Bool) {
// self.rowViewModels = rowViewModels
// self.isMultiselectionEnabled = isMultiselectionEnabled
// }
//
// open func viewDidLoad() {
// initiallySelectedValues = selectedValues
// }
//
// open func setSelected(model: RowViewModelType, isSelected: Bool) {
// if !isMultiselectionEnabled {
// rowViewModels.enumerated().forEach {
// rowViewModels[$0.offset].isSelected = false
// }
// }
//
// if let index = rowViewModels.firstIndex(of: model) {
// rowViewModels[index].isSelected = isSelected
// }
//
// if !selectedValues.isEmpty {
// delegate?.filters(didSelect: selectedValues)
// }
// }
//}

View File

@ -24,26 +24,26 @@ import TableKit
import TITableKitUtils
import TIUIElements
open class DefaultFilterListSectionBuilder<CellType: BaseSeparatorCell & ConfigurableCell>: FilterTableSectionBuilder {
open func makeSection<ViewModel: FilterListPickerConfigurator>(with viewModel: ViewModel) -> TableSection where ViewModel.RowViewModel == CellType.CellData {
let rows = viewModel.visibleValues.map { item in
TableRow<CellType>(item: item)
.on(.select) { [weak viewModel] _ in
viewModel?.setSelected(model: item, isSelected: !item.isSelected)
}
.on(.deselect) { [weak viewModel] _ in
viewModel?.setSelected(model: item, isSelected: false)
}
}
let separatedRows: [SeparatorRowBox] = rows.map { $0.separatorRowBox }
let separator = SeparatorConfiguration(color: .gray)
separatedRows.configureSeparators(first: separator,
middle: separator,
last: viewModel.isFinishWithSeparator ? separator : SeparatorConfiguration(color: .clear))
return .init(rows: separatedRows.rows)
}
}
//open class DefaultFilterListSectionBuilder<CellType: BaseSeparatorCell & ConfigurableCell>: FilterTableSectionBuilder {
//
// open func makeSection<ViewModel: FilterListPickerConfigurator>(with viewModel: ViewModel) -> TableSection where ViewModel.RowViewModel == CellType.CellData {
// let rows = viewModel.visibleValues.map { item in
// TableRow<CellType>(item: item)
// .on(.select) { [weak viewModel] _ in
// viewModel?.setSelected(model: item, isSelected: !item.isSelected)
// }
// .on(.deselect) { [weak viewModel] _ in
// viewModel?.setSelected(model: item, isSelected: false)
// }
// }
//
// let separatedRows: [SeparatorRowBox] = rows.map { $0.separatorRowBox }
// let separator = SeparatorConfiguration(color: .gray)
//
// separatedRows.configureSeparators(first: separator,
// middle: separator,
// last: viewModel.isFinishWithSeparator ? separator : SeparatorConfiguration(color: .clear))
//
// return .init(rows: separatedRows.rows)
// }
//}

View File

@ -20,46 +20,147 @@
// THE SOFTWARE.
//
import TableKit
import TISwiftUtils
import TIUIElements
import TIUIKitCore
import UIKit
open class BaseListFilterPickerView<CellType: BaseSeparatorCell & ConfigurableCell>: BaseCustomTableView where CellType.CellData: FilterRowRepresentable & Equatable {
@available(iOS 13.0, *)
open class BaseFiltersTableView<CellType: UITableViewCell & ConfigurableView,
PropertyValue: FilterPropertyValueRepresenter & Hashable>:
UITableView,
InitializableViewProtocol,
Updatable,
UITableViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable {
public let builder = DefaultFilterListSectionBuilder<CellType>()
public weak var viewModel: BaseListFilterViewModel<CellType.CellData>?
public init(viewModel: BaseListFilterViewModel<CellType.CellData>) {
self.viewModel = viewModel
super.init(frame: .zero)
tableView.allowsMultipleSelection = viewModel.isMultiselectionEnabled
public enum DefaultSection: String {
case main
}
public typealias DataSource = UITableViewDiffableDataSource<String, CellType.ViewModelType>
public typealias Snapshot = NSDiffableDataSourceSnapshot<String, CellType.ViewModelType>
public weak var viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>?
public lazy var collectionViewDataSource = createDataSource()
// MARK: - Init
public init(viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>, allowsMultipleSelection: Bool = true) {
self.viewModel = viewModel
super.init(frame: .zero, style: .plain)
self.allowsMultipleSelection = allowsMultipleSelection
}
@available(*, unavailable)
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func configureAppearance() {
super.configureAppearance()
// MARK: - Life cycle
tableView.alwaysBounceVertical = false
viewModel?.viewDidLoad()
// MARK: - Life cycle
reloadData(with: viewModel?.visibleSelectedIndexes ?? [])
open func addViews() {
// override in subclass
}
open func reloadData(with selectedIndexes: [Int]) {
open func configureLayout() {
// override in subclass
}
open func bindViews() {
delegate = self
}
open func configureAppearance() {
alwaysBounceVertical = false
// reloadData(with: viewModel?.visibleSelectedIndexes ?? [])
}
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, forCellReuseIdentifier: CellType.reuseIdentifier)
}
open func filterDidTapped(atIndexPath indexPath: IndexPath) {
guard let viewModel = viewModel else { return }
let elements = builder.makeSection(with: viewModel)
updateTableView(elements)
let changes = viewModel.filterDidSelected(atIndexPath: indexPath)
selectedIndexes.forEach {
tableView.selectRow(at: IndexPath(row: $0, section: .zero),
animated: false,
scrollPosition: .none)
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 = { tableView, indexPath, itemIdentifier in
let cell = tableView.dequeueReusableCell(withIdentifier: CellType.reuseIdentifier, for: indexPath) as? CellType
cell?.configure(with: itemIdentifier)
return cell
}
return DataSource(tableView: self, cellProvider: cellProvider)
}
open func applyChange(_ changes: [BaseFilterViewModel<CellType.ViewModelType, PropertyValue>.Change]) {
changes.forEach { change in
guard let cell = cellForRow(at: change.indexPath) as? CellType else {
return
}
cell.configure(with: change.viewModel)
change.viewModel.isSelected
? selectRow(at: change.indexPath, animated: false, scrollPosition: ScrollPosition.none)
: deselectRow(at: change.indexPath, animated: false)
}
}
}

View File

@ -20,91 +20,123 @@
// THE SOFTWARE.
//
import TableKit
import TIUIElements
import TIUIKitCore
import UIKit
open class DefaultFilterListCell: BaseSeparatorCell, ConfigurableCell {
open class DefaultPickerView: BaseInitializableView {
private var titleLeadingConstraint: NSLayoutConstraint!
private var titleTopConstraint: NSLayoutConstraint!
private var titleBottomConstraint: NSLayoutConstraint!
private let titleLabel = UILabel()
private let selectionStateImageView = UIImageView()
private var imageTrailingConstraint: NSLayoutConstraint!
private var imageTopConstraint: NSLayoutConstraint!
private var imageBottomConstraint: NSLayoutConstraint!
public lazy var titleLabel = UILabel()
public lazy var selectionCheckmarkImageView = UIImageView()
open var selectedImage: UIImage? {
if #available(iOS 13.0, *) {
return UIImage(systemName: "checkmark")!
open var image: UIImage? {
get {
selectionStateImageView.image
}
set {
selectionStateImageView.image = newValue
}
return nil
}
open var deselectedImage: UIImage? {
nil
open var text: String? {
get {
titleLabel.text
}
set {
titleLabel.text = newValue
}
}
open var textColor: UIColor {
get {
titleLabel.textColor
}
set {
titleLabel.textColor = newValue
}
}
open override func addViews() {
super.addViews()
contentView.addSubviews(titleLabel, selectionCheckmarkImageView)
addSubviews(titleLabel, selectionStateImageView)
}
open override func configureLayout() {
super.configureLayout()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
selectionCheckmarkImageView.translatesAutoresizingMaskIntoConstraints = false
titleLeadingConstraint = titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
titleTopConstraint = titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor)
titleBottomConstraint = titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
imageTrailingConstraint = selectionCheckmarkImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
imageTopConstraint = selectionCheckmarkImageView.topAnchor.constraint(equalTo: contentView.topAnchor)
imageBottomConstraint = selectionCheckmarkImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
selectionStateImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLeadingConstraint,
titleTopConstraint,
titleBottomConstraint,
imageTrailingConstraint,
imageTopConstraint,
imageBottomConstraint
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
titleLabel.heightAnchor.constraint(equalTo: heightAnchor),
selectionStateImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
selectionStateImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
selectionStateImageView.heightAnchor.constraint(equalTo: heightAnchor),
])
}
}
open func configure(with viewModel: DefaultFilterRowViewModel) {
titleLabel.text = viewModel.title
open class DefaultFilterListCell: ContainerTableViewCell<DefaultPickerView>, ConfigurableView {
setContentInsets(viewModel.appearance.contentInsets)
contentView.backgroundColor = viewModel.appearance.deselectedBgColor
open var selectedStateAppearance: FilterCellStateAppearance {
.defaultSelectedRowAppearance
}
open func setContentInsets(_ insets: UIEdgeInsets) {
titleLeadingConstraint.constant = insets.left
imageTrailingConstraint.constant = -insets.right
titleTopConstraint.constant = insets.top
imageTopConstraint.constant = insets.top
titleBottomConstraint.constant = -insets.bottom
imageBottomConstraint.constant = -insets.bottom
open var normalStateAppearance: FilterCellStateAppearance {
.defaultNormalRowAppearance
}
open override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
selectionCheckmarkImageView.image = selectedImage
} else {
selectionCheckmarkImageView.image = deselectedImage
open override var isSelected: Bool {
didSet {
let appearance = isSelected ? selectedStateAppearance : normalStateAppearance
updateAppearance(with: appearance)
}
}
open override func configureAppearance() {
super.configureAppearance()
updateAppearance(with: normalStateAppearance)
}
open func configure(with viewModel: DefaultFilterCellViewModel) {
wrappedView.text = viewModel.title
}
// MARK: - Open methods
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 defaultSelectedRowAppearance: FilterCellStateAppearance {
var selectionImage: UIImage?
if #available(iOS 13, *) {
selectionImage = UIImage(systemName: "checkmark")
}
return .init(borderColor: .systemGreen,
backgroundColor: .white,
fontColor: .systemGreen,
borderWidth: 1,
selectionImage: selectionImage)
}
static var defaultNormalRowAppearance: FilterCellStateAppearance {
.init(borderColor: .lightGray, backgroundColor: .lightGray, fontColor: .black, borderWidth: 0)
}
}

View File

@ -20,14 +20,5 @@
// THE SOFTWARE.
//
public protocol FilterRowRepresentable {
var id: String { get }
var title: String { get }
var isSelected: Bool { get set }
var appearance: FilterCellAppearanceProtocol { get set }
init(id: String,
title: String,
appearance: FilterCellAppearanceProtocol,
isSelected: Bool)
}
@available(iOS 13.0, *)
public typealias DefaultFiltersTableView = BaseFiltersTableView<DefaultFilterListCell, DefaultFilterPropertyValue>

View File

@ -32,12 +32,15 @@ public struct FilterCellStateAppearance {
public let contentInsets: UIEdgeInsets
public let cornerRadius: CGFloat
public let selectionImage: UIImage?
public init(borderColor: UIColor,
backgroundColor: UIColor,
fontColor: UIColor,
borderWidth: CGFloat,
contentInsets: UIEdgeInsets = .init(top: 4, left: 8, bottom: 4, right: 8),
cornerRadius: CGFloat = 6) {
cornerRadius: CGFloat = 6,
selectionImage: UIImage? = nil) {
self.borderColor = borderColor
self.backgroundColor = backgroundColor
@ -45,5 +48,6 @@ public struct FilterCellStateAppearance {
self.borderWidth = borderWidth
self.contentInsets = contentInsets
self.cornerRadius = cornerRadius
self.selectionImage = selectionImage
}
}

View File

@ -68,14 +68,14 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
// override in subclass
}
open func bindViews() {
delegate = self
}
open func configureLayout() {
// override in subclass
}
open func bindViews() {
delegate = self
}
open func configureAppearance() {
backgroundColor = .white
}

View File

@ -32,3 +32,9 @@ extension UICollectionViewCell: ReuseIdentifierProtocol {
.init(describing: Self.self)
}
}
extension UITableViewCell: ReuseIdentifierProtocol {
public static var reuseIdentifier: String {
.init(describing: Self.self)
}
}