Merge pull request #322 from TouchInstinct/feature/list_filters

Фильтры в виде списка
This commit is contained in:
Nikita Semenov 2022-09-06 18:48:26 +03:00 committed by GitHub
commit dde0eba7a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 476 additions and 9 deletions

View File

@ -3,6 +3,7 @@
### 1.27.0
- **Add**: Tag like filter collection view
- **ADD**: List like filter table view
### 1.26.0

View File

@ -20,6 +20,7 @@
// THE SOFTWARE.
//
import TIUIKitCore
import UIKit
public struct FilterCellStateAppearance {
@ -32,12 +33,15 @@ public struct FilterCellStateAppearance {
public let contentInsets: UIEdgeInsets
public let cornerRadius: CGFloat
public let stateImages: UIControl.StateImages?
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,
stateImages: UIControl.StateImages? = nil) {
self.borderColor = borderColor
self.backgroundColor = backgroundColor
@ -45,5 +49,6 @@ public struct FilterCellStateAppearance {
self.borderWidth = borderWidth
self.contentInsets = contentInsets
self.cornerRadius = cornerRadius
self.stateImages = stateImages
}
}

View File

@ -42,6 +42,7 @@ open class BaseFilterViewModel<CellViewModelType: FilterCellViewModelProtocol &
}
public weak var filtersCollection: Updatable?
public weak var pickerDelegate: FiltersPickerDelegate?
public private(set) var cellsViewModels: [CellViewModelType]
@ -70,6 +71,8 @@ open class BaseFilterViewModel<CellViewModelType: FilterCellViewModelProtocol &
viewModel: cellsViewModels[$0.offset])
}
pickerDelegate?.filters(didSelected: selected)
return changedItems
}

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 TIUIElements
import TIUIKitCore
import UIKit
@available(iOS 13.0, *)
open class BaseFiltersTableView<CellType: UITableViewCell & ConfigurableView,
PropertyValue: FilterPropertyValueRepresenter & Hashable>:
UITableView,
InitializableViewProtocol,
Updatable,
UITableViewDelegate where CellType.ViewModelType: FilterCellViewModelProtocol & Hashable {
public enum DefaultSection: String {
case main
}
public typealias DataSource = UITableViewDiffableDataSource<String, CellType.ViewModelType>
public typealias Snapshot = NSDiffableDataSourceSnapshot<String, CellType.ViewModelType>
public let viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>
public lazy var tableViewDataSource = createDataSource()
// MARK: - Init
public init(viewModel: BaseFilterViewModel<CellType.ViewModelType, PropertyValue>,
allowsMultipleSelection: Bool = true,
style: UITableView.Style = .plain) {
self.viewModel = viewModel
super.init(frame: .zero, style: style)
self.allowsMultipleSelection = allowsMultipleSelection
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 configureLayout() {
// override in subclass
}
open func bindViews() {
delegate = self
}
open func configureAppearance() {
alwaysBounceVertical = false
}
open func localize() {
// override in subclass
}
open func viewDidLoad() {
registerCell()
viewModel.filtersCollection = self
}
open func viewDidAppear() {
applySnapshot()
}
// MARK: - UITableViewDelegate
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
filterDidTapped(atIndexPath: indexPath)
}
public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
filterDidTapped(atIndexPath: indexPath)
}
// MARK: - Updatable
open func update() {
applySnapshot()
}
// MARK: - Open methods
open func registerCell() {
register(CellType.self, forCellReuseIdentifier: CellType.reuseIdentifier)
}
open func filterDidTapped(atIndexPath indexPath: IndexPath) {
let changes = viewModel.filterDidSelected(atIndexPath: indexPath)
applyChanges(changes)
}
open func applySnapshot() {
var snapshot = Snapshot()
snapshot.appendSections([DefaultSection.main.rawValue])
snapshot.appendItems(viewModel.cellsViewModels, toSection: DefaultSection.main.rawValue)
tableViewDataSource.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 .init(tableView: self, cellProvider: cellProvider)
}
open func applyChanges(_ 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)
if let selectableCell = cell as? Selectable {
selectableCell.setSelected(change.viewModel.isSelected)
}
change.viewModel.isSelected
? selectRow(at: change.indexPath, animated: false, scrollPosition: ScrollPosition.none)
: deselectRow(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 DefaultFiltersTableView = BaseFiltersTableView<DefaultFilterTableViewCell, DefaultFilterPropertyValue>

View File

@ -0,0 +1,112 @@
//
// 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 TIUIElements
import TIUIKitCore
import UIKit
open class DefaultFilterTableViewCell: ContainerTableViewCell<DefaultPickerView>, ConfigurableView, Selectable {
open var selectedStateAppearance: FilterCellStateAppearance {
.defaultSelectedRowAppearance
}
open var normalStateAppearance: FilterCellStateAppearance {
.defaultRowAppearance
}
open override var isSelected: Bool {
didSet {
let appearance = isSelected ? selectedStateAppearance : normalStateAppearance
updateAppearance(with: appearance)
wrappedView.setSelected(isSelected)
}
}
// MARK: Life cycle
open override func configureAppearance() {
super.configureAppearance()
updateAppearance(with: normalStateAppearance)
}
// MARK: - ConfigurableView
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
wrappedView.images = appearance.stateImages ?? [:]
backgroundColor = appearance.backgroundColor
layer.borderColor = appearance.borderColor.cgColor
layer.borderWidth = appearance.borderWidth
layer.round(corners: .allCorners, radius: appearance.cornerRadius)
}
}
extension FilterCellStateAppearance {
@available(iOS 13, *)
private static let defaultStateImages: UIControl.StateImages = [.normal: nil,
.selected: UIImage(systemName: "checkmark")]
private static let defaultContentInsets = UIEdgeInsets(top: 16, left: 8, bottom: 16, right: 8)
static var defaultSelectedRowAppearance: FilterCellStateAppearance {
var stateImages: UIControl.StateImages?
if #available(iOS 13, *) {
stateImages = defaultStateImages
}
return .init(borderColor: .clear,
backgroundColor: .white,
fontColor: .black,
borderWidth: .zero,
contentInsets: defaultContentInsets,
cornerRadius: .zero,
stateImages: stateImages)
}
static var defaultRowAppearance: FilterCellStateAppearance {
var stateImages: UIControl.StateImages?
if #available(iOS 13, *) {
stateImages = defaultStateImages
}
return .init(borderColor: .clear,
backgroundColor: .white,
fontColor: .black,
borderWidth: .zero,
contentInsets: defaultContentInsets,
cornerRadius: .zero,
stateImages: stateImages)
}
}

View File

@ -0,0 +1,92 @@
//
// 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 TIUIElements
import TIUIKitCore
import TISwiftUtils
import UIKit
open class DefaultPickerView: BaseInitializableView, Selectable {
private let titleLabel = UILabel()
private let selectionStateImageView = UIImageView()
open var images: UIControl.StateImages = [:] {
didSet {
if images.contains(where: { $0.key == .highlighted }) {
selectionStateImageView.highlightedImage = images[.highlighted] ?? nil
}
}
}
open var text: String? {
get {
titleLabel.text
}
set {
titleLabel.text = newValue
}
}
open var textColor: UIColor {
get {
titleLabel.textColor
}
set {
titleLabel.textColor = newValue
}
}
open var isSelected: Bool = false {
didSet {
selectionStateImageView.image = images[isSelected ? .selected : .normal] ?? nil
}
}
open var defaultImageSize: CGFloat {
20
}
open override func addViews() {
super.addViews()
addSubviews(titleLabel, selectionStateImageView)
}
open override func configureLayout() {
super.configureLayout()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
selectionStateImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
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(equalToConstant: defaultImageSize),
selectionStateImageView.widthAnchor.constraint(equalToConstant: defaultImageSize)
])
}
}

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
}
@ -121,7 +121,7 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
let changes = viewModel.filterDidSelected(atIndexPath: indexPath)
applyChange(changes)
applyChanges(changes)
}
open func applySnapshot() {
@ -147,10 +147,10 @@ open class BaseFiltersCollectionView<CellType: UICollectionViewCell & Configurab
return cell
}
return DataSource(collectionView: self, cellProvider: cellProvider)
return .init(collectionView: self, cellProvider: cellProvider)
}
open func applyChange(_ changes: [BaseFilterViewModel<CellType.ViewModelType, PropertyValue>.Change]) {
open func applyChanges(_ changes: [BaseFilterViewModel<CellType.ViewModelType, PropertyValue>.Change]) {
changes.forEach { change in
guard let cell = cellForItem(at: change.indexPath) as? CellType else {
return

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 FiltersPickerDelegate: AnyObject {
func filters(didSelected filters: [FilterPropertyValueRepresenter])
}

View File

@ -0,0 +1,33 @@
//
// 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 Selectable: AnyObject {
var isSelected: Bool { get set }
func setSelected(_ isSelected: Bool)
}
public extension Selectable {
func setSelected(_ isSelected: Bool) {
self.isSelected = isSelected
}
}

View File

@ -41,7 +41,7 @@ open class ContainerTableViewCell<View: UIView>: BaseInitializableCell, WrappedV
override open func addViews() {
super.addViews()
addSubview(wrappedView)
contentView.addSubview(wrappedView)
}
override open func configureLayout() {

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