├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── CollectionView │ ├── Cell.swift │ ├── CollectionView.swift │ ├── CollectionViewController.swift │ ├── Extensions.swift │ ├── Grid.swift │ ├── Section.swift │ ├── Source.swift │ └── ViewModel.swift └── Tests └── CollectionViewTests └── CollectionViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | .swiftpm 3 | *.xcuserstate 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2015-2021 Tibor Bödecs 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "CollectionView", 6 | platforms: [ 7 | .iOS(.v12), 8 | ], 9 | products: [ 10 | .library(name: "CollectionView", targets: ["CollectionView"]), 11 | ], 12 | targets: [ 13 | .target(name: "CollectionView"), 14 | .testTarget(name: "CollectionViewTests", dependencies: ["CollectionView"]), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CollectionView 2 | 3 | CollectionViews with ease. 4 | 5 | 6 | ## Installation 7 | 8 | ### Swift Package Manager 9 | 10 | ``` 11 | .package(url: "https://github.com/CoreKit/CollectionView", from: "2.0.0"), 12 | ``` 13 | -------------------------------------------------------------------------------- /Sources/CollectionView/Cell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // CVVM 4 | // 5 | // Created by Tibor Bödecs on 2018. 04. 11.. 6 | // Copyright © 2018. Tibor Bödecs. All rights reserved. 7 | // 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | open class Cell: UICollectionViewCell { 12 | 13 | public override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | self.initialize() 17 | } 18 | 19 | public required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | 22 | self.initialize() 23 | } 24 | 25 | open override func awakeFromNib() { 26 | super.awakeFromNib() 27 | 28 | self.reset() 29 | } 30 | 31 | open override func prepareForReuse() { 32 | super.prepareForReuse() 33 | 34 | self.reset() 35 | } 36 | 37 | // MARK: - API 38 | 39 | open func initialize() { 40 | 41 | } 42 | 43 | open func reset() { 44 | 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/CollectionView/CollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionView.swift 3 | // CollectionView 4 | // 5 | // Created by Tibor Bödecs on 2019. 04. 25.. 6 | // 7 | #if canImport(UIKit) 8 | import Foundation 9 | import UIKit 10 | 11 | open class CollectionView: UICollectionView { 12 | 13 | open var source: Source? = nil { 14 | didSet { 15 | self.source?.register(itemsFor: self) 16 | 17 | self.dataSource = self.source 18 | self.delegate = self.source 19 | } 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/CollectionView/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewController.swift 3 | // CVVM 4 | // 5 | // Created by Tibor Bödecs on 2018. 04. 11.. 6 | // Copyright © 2018. Tibor Bödecs. All rights reserved. 7 | // 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | open class CollectionViewController: UIViewController { 12 | 13 | @IBOutlet open weak var collectionView: CollectionView! 14 | 15 | // MARK: - init 16 | 17 | public init() { 18 | super.init(nibName: nil, bundle: nil) 19 | 20 | self.initialize() 21 | } 22 | 23 | required public init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | 26 | self.initialize() 27 | } 28 | 29 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 30 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 31 | 32 | self.initialize() 33 | } 34 | 35 | open func initialize() { 36 | // do nothing... 37 | } 38 | 39 | open func layoutConstraints() -> [NSLayoutConstraint] { 40 | return [ 41 | self.collectionView.topAnchor.constraint(equalTo: self.view.topAnchor), 42 | self.collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), 43 | self.collectionView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor), 44 | self.collectionView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor), 45 | ] 46 | } 47 | 48 | // MARK: - view controller 49 | 50 | open override func loadView() { 51 | super.loadView() 52 | 53 | let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 54 | collectionView.translatesAutoresizingMaskIntoConstraints = false 55 | self.collectionView = collectionView 56 | self.view.addSubview(self.collectionView) 57 | NSLayoutConstraint.activate(self.layoutConstraints()) 58 | } 59 | 60 | open override func viewDidLoad() { 61 | super.viewDidLoad() 62 | 63 | //self.view.backgroundColor = .white 64 | 65 | self.collectionView.backgroundColor = .clear 66 | self.collectionView.alwaysBounceVertical = true 67 | self.collectionView.showsVerticalScrollIndicator = true 68 | } 69 | 70 | open override func didReceiveMemoryWarning() { 71 | super.didReceiveMemoryWarning() 72 | 73 | if self.isViewLoaded && self.view.window == nil { 74 | //self.collectionView = nil 75 | } 76 | } 77 | 78 | // MARK: - handle autorotation 79 | 80 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 81 | super.traitCollectionDidChange(previousTraitCollection) 82 | 83 | guard 84 | self.collectionView != nil, 85 | let previousTraitCollection = previousTraitCollection, 86 | self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass || 87 | self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass 88 | else { 89 | return 90 | } 91 | 92 | self.collectionView.collectionViewLayout.invalidateLayout() 93 | self.collectionView.reloadData() 94 | } 95 | 96 | open override func viewWillTransition(to size: CGSize, 97 | with coordinator: UIViewControllerTransitionCoordinator) { 98 | 99 | super.viewWillTransition(to: size, with: coordinator) 100 | 101 | guard self.collectionView != nil else { 102 | return 103 | } 104 | 105 | self.collectionView.collectionViewLayout.invalidateLayout() 106 | self.collectionView.bounds.size = size 107 | 108 | coordinator.animate(alongsideTransition: { context in 109 | context.viewController(forKey: UITransitionContextViewControllerKey.from) 110 | 111 | }, completion: { [weak self] _ in 112 | self?.collectionView.collectionViewLayout.invalidateLayout() 113 | }) 114 | } 115 | } 116 | #endif 117 | -------------------------------------------------------------------------------- /Sources/CollectionView/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Id.swift 3 | // CollectionView 4 | // 5 | // Created by Tibor Bödecs on 2019. 04. 25.. 6 | // 7 | #if canImport(UIKit) 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | var id: String? { 14 | get { 15 | return self.accessibilityIdentifier 16 | } 17 | set { 18 | self.accessibilityIdentifier = newValue 19 | } 20 | } 21 | 22 | func view(withId id: String) -> UIView? { 23 | if self.id == id { 24 | return self 25 | } 26 | for view in self.subviews { 27 | if let view = view.view(withId: id) { 28 | return view 29 | } 30 | } 31 | return nil 32 | } 33 | } 34 | 35 | extension Array { 36 | 37 | func element(at index: Int) -> Element? { 38 | return index < self.count && index >= 0 ? self[index] : nil 39 | } 40 | } 41 | 42 | extension CGFloat { 43 | 44 | var evenRounded: CGFloat { 45 | guard self > 1 else { 46 | return self 47 | } 48 | var newValue = self.rounded(.towardZero) 49 | if newValue.truncatingRemainder(dividingBy: 2) == 1 { 50 | newValue -= 1 51 | } 52 | return newValue 53 | } 54 | } 55 | 56 | extension UICollectionViewCell { 57 | 58 | static var uniqueIdentifier: String { 59 | return String(describing: self) 60 | } 61 | 62 | var uniqueIdentifier: String { 63 | return type(of: self).uniqueIdentifier 64 | } 65 | 66 | static var hasNib: Bool { 67 | return Bundle.main.path(forResource: self.uniqueIdentifier, ofType: "nib") != nil 68 | } 69 | 70 | static var nib: UINib { 71 | return UINib(nibName: self.uniqueIdentifier, bundle: nil) 72 | } 73 | 74 | // MARK: - cells 75 | 76 | static func register(nibFor collectionView: UICollectionView) { 77 | collectionView.register(self.nib, forCellWithReuseIdentifier: self.uniqueIdentifier) 78 | } 79 | 80 | static func register(classFor collectionView: UICollectionView) { 81 | collectionView.register(self, forCellWithReuseIdentifier: self.uniqueIdentifier) 82 | } 83 | 84 | static func register(itemFor collectionView: UICollectionView) { 85 | if self.hasNib { 86 | return self.register(nibFor: collectionView) 87 | } 88 | self.register(classFor: collectionView) 89 | } 90 | 91 | static func reuse(_ collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { 92 | return collectionView.dequeueReusableCell(withReuseIdentifier: self.uniqueIdentifier, for: indexPath) 93 | } 94 | 95 | // MARK: - supplementary views 96 | 97 | static func register(nibFor collectionView: UICollectionView, kind: String) { 98 | collectionView.register(self.nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: self.uniqueIdentifier) 99 | } 100 | 101 | static func register(classFor collectionView: UICollectionView, kind: String) { 102 | collectionView.register(self, forSupplementaryViewOfKind: kind, withReuseIdentifier: self.uniqueIdentifier) 103 | } 104 | 105 | static func register(itemFor collectionView: UICollectionView, kind: String) { 106 | if self.hasNib { 107 | return self.register(nibFor: collectionView, kind: kind) 108 | } 109 | self.register(classFor: collectionView, kind: kind) 110 | } 111 | 112 | static func reuse(_ collectionView: UICollectionView, indexPath: IndexPath, kind: String) -> UICollectionReusableView { 113 | return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: self.uniqueIdentifier, for: indexPath) 114 | } 115 | } 116 | #endif 117 | -------------------------------------------------------------------------------- /Sources/CollectionView/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // CVVM 4 | // 5 | // Created by Tibor Bödecs on 2018. 04. 11.. 6 | // Copyright © 2018. Tibor Bödecs. All rights reserved. 7 | // 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public extension UIEdgeInsets { 12 | 13 | init(all value: CGFloat) { 14 | self.init(top: value, left: value, bottom: value, right: value) 15 | } 16 | 17 | init(horizontal: CGFloat, vertical: CGFloat) { 18 | self.init(top: vertical, left: horizontal, bottom: vertical, right: horizontal) 19 | } 20 | } 21 | 22 | open class Grid { 23 | 24 | open var columns: CGFloat 25 | open var margin: UIEdgeInsets 26 | open var padding: UIEdgeInsets 27 | 28 | public var verticalMargin: CGFloat { 29 | return self.margin.top + self.margin.bottom 30 | } 31 | 32 | public var horizontalMargin: CGFloat { 33 | return self.margin.left + self.margin.right 34 | } 35 | 36 | // line spacing 37 | public var verticalPadding: CGFloat { 38 | return self.padding.top + self.padding.bottom 39 | } 40 | 41 | // inter item spacing 42 | public var horizontalPadding: CGFloat { 43 | return self.padding.left + self.padding.right 44 | } 45 | 46 | public init(columns: CGFloat = 1, margin: UIEdgeInsets = .zero, padding: UIEdgeInsets = .zero) { 47 | self.columns = columns 48 | self.margin = margin 49 | self.padding = padding 50 | } 51 | 52 | open func size(for view: UIView, ratio: CGFloat, items: CGFloat = 1, gaps: CGFloat? = nil) -> CGSize { 53 | let size = self.width(for: view, items: items, gaps: gaps) 54 | return CGSize(width: size, height: (size * ratio).evenRounded) 55 | } 56 | 57 | open func size(for view: UIView, height: CGFloat, items: CGFloat = 1, gaps: CGFloat? = nil) -> CGSize { 58 | let size = self.width(for: view, items: items, gaps: gaps) 59 | 60 | var height = height 61 | if height < 0 { 62 | height = view.bounds.size.height - height 63 | } 64 | return CGSize(width: size, height: height.evenRounded) 65 | } 66 | 67 | open func width(for view: UIView, items: CGFloat = 1, gaps: CGFloat? = nil) -> CGFloat { 68 | let gaps = gaps ?? items - 1 69 | 70 | let width = view.bounds.size.width - self.horizontalMargin - self.horizontalPadding * gaps 71 | 72 | return (width / self.columns * items).evenRounded 73 | } 74 | 75 | open func height(for view: UIView, items: CGFloat = 1, gaps: CGFloat? = nil) -> CGFloat { 76 | let gaps = gaps ?? items - 1 77 | 78 | let height = view.bounds.size.height - self.verticalMargin - self.verticalPadding * gaps 79 | 80 | return (height / self.columns * items).evenRounded 81 | } 82 | } 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/CollectionView/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewSection.swift 3 | // CVVM 4 | // 5 | // Created by Tibor Bödecs on 2018. 04. 11.. 6 | // Copyright © 2018. Tibor Bödecs. All rights reserved. 7 | // 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | open class Section { 12 | 13 | public var grid: Grid? 14 | public var header: ViewModelProtocol? 15 | public var footer: ViewModelProtocol? 16 | public var items: [ViewModelProtocol] 17 | 18 | public init(grid: Grid? = nil, 19 | header: ViewModelProtocol? = nil, 20 | footer: ViewModelProtocol? = nil, 21 | items: [ViewModelProtocol] = []) { 22 | self.grid = grid 23 | self.header = header 24 | self.footer = footer 25 | self.items = items 26 | } 27 | 28 | // MARK: - helpers 29 | 30 | public func add(_ item: ViewModelProtocol) { 31 | self.items.append(item) 32 | } 33 | 34 | public func by(id: String) -> ViewModel? { 35 | for item in self.items { 36 | if item.id == id { 37 | return item as? ViewModel 38 | } 39 | } 40 | return nil 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/CollectionView/Source.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewSource.swift 3 | // CVVM 4 | // 5 | // Created by Tibor Bödecs on 2018. 04. 11.. 6 | // Copyright © 2018. Tibor Bödecs. All rights reserved. 7 | // 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | open class Source: NSObject { 12 | 13 | private var indexPathSelected = false 14 | 15 | open var grid: Grid 16 | open var sections: [Section] 17 | 18 | public init(grid: Grid = Grid(), sections: [Section] = []) { 19 | self.grid = grid 20 | self.sections = sections 21 | 22 | super.init() 23 | } 24 | 25 | public convenience init(grid: Grid = Grid(), _ sections: [[ViewModelProtocol]]) { 26 | let sections = sections.map { items in 27 | return Section(grid: grid, header: nil, footer: nil, items: items) 28 | } 29 | self.init(grid: grid, sections: sections) 30 | } 31 | 32 | // MARK: - helpers 33 | 34 | public func add(_ section: Section) { 35 | self.sections.append(section) 36 | } 37 | 38 | public func by(id: String) -> ViewModel? { 39 | for section in self.sections { 40 | if let viewModel: ViewModel = section.by(id: id) { 41 | return viewModel 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | // MARK: - section indexes 48 | 49 | public var sectionIndexes: IndexSet? { 50 | if self.sections.isEmpty { 51 | return nil 52 | } 53 | if self.sections.count == 1 { 54 | return IndexSet(integer: 0) 55 | } 56 | return IndexSet(integersIn: 0.. Section? { 62 | return self.sections.element(at: section) 63 | } 64 | 65 | public func itemAt(_ indexPath: IndexPath) -> ViewModelProtocol? { 66 | return self.itemAt(indexPath.section)?.items.element(at: indexPath.item) 67 | } 68 | 69 | // MARK: - view registration 70 | 71 | public func register(itemsFor collectionView: UICollectionView) { 72 | 73 | for section in self.sections { 74 | section.header?.cell.register(itemFor: collectionView, kind: UICollectionView.elementKindSectionHeader) 75 | section.footer?.cell.register(itemFor: collectionView, kind: UICollectionView.elementKindSectionFooter) 76 | 77 | for cell in section.items.map({ $0.cell }) { 78 | cell.register(itemFor: collectionView) 79 | } 80 | } 81 | } 82 | } 83 | 84 | extension Source: UICollectionViewDataSource { 85 | 86 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 87 | return self.sections.count 88 | } 89 | 90 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 91 | return self.itemAt(section)?.items.count ?? 0 92 | } 93 | 94 | private func collectionView(_ collectionView: UICollectionView, 95 | itemForIndexPath indexPath: IndexPath) -> UICollectionViewCell { 96 | guard 97 | let view = collectionView as? CollectionView, 98 | let viewModel = self.itemAt(indexPath), 99 | let cell = viewModel.cell.reuse(collectionView, indexPath: indexPath) as? Cell 100 | else { 101 | return Cell.reuse(collectionView, indexPath: indexPath) 102 | } 103 | viewModel.config(cell: cell, collectionView: view, indexPath: indexPath, grid: self.grid(indexPath.section)) 104 | return cell 105 | } 106 | 107 | public func collectionView(_ collectionView: UICollectionView, 108 | cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 109 | return self.collectionView(collectionView, itemForIndexPath: indexPath) 110 | } 111 | 112 | 113 | private func _collectionView(_ collectionView: UICollectionView, 114 | viewForSupplementaryElementOfKind kind: String, 115 | at indexPath: IndexPath) -> UICollectionReusableView 116 | { 117 | let section = self.itemAt(indexPath.section) 118 | var optionalViewModel: ViewModelProtocol? 119 | if kind == UICollectionView.elementKindSectionHeader { 120 | optionalViewModel = section?.header 121 | } 122 | if kind == UICollectionView.elementKindSectionFooter { 123 | optionalViewModel = section?.footer 124 | } 125 | 126 | guard 127 | let viewModel = optionalViewModel, 128 | let view = collectionView as? CollectionView, 129 | let cell = viewModel.cell.reuse(collectionView, 130 | indexPath: indexPath, 131 | kind: kind) as? Cell 132 | else { 133 | return Cell.reuse(collectionView, indexPath: indexPath) 134 | } 135 | viewModel.config(cell: cell, collectionView: view, indexPath: indexPath, grid: self.grid(indexPath.section)) 136 | return cell 137 | } 138 | 139 | public func collectionView(_ collectionView: UICollectionView, 140 | viewForSupplementaryElementOfKind kind: String, 141 | at indexPath: IndexPath) -> UICollectionReusableView { 142 | return self._collectionView(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) 143 | } 144 | } 145 | 146 | extension Source: UICollectionViewDelegate { 147 | 148 | func selectItem(at indexPath: IndexPath) { 149 | guard !self.indexPathSelected else { 150 | return 151 | } 152 | self.itemAt(indexPath)?.callback(indexPath: indexPath) 153 | } 154 | 155 | public func collectionView(_ collectionView: UICollectionView, 156 | didSelectItemAt indexPath: IndexPath) { 157 | self.selectItem(at: indexPath) 158 | } 159 | } 160 | 161 | extension Source: UICollectionViewDelegateFlowLayout { 162 | 163 | func grid(_ section: Int) -> Grid { 164 | return self.itemAt(section)?.grid ?? self.grid 165 | } 166 | 167 | public func collectionView(_ collectionView: UICollectionView, 168 | layout collectionViewLayout: UICollectionViewLayout, 169 | sizeForItemAt indexPath: IndexPath) -> CGSize { 170 | guard let viewModel = self.itemAt(indexPath) else { 171 | return .zero 172 | } 173 | return viewModel.size(collectionView: collectionView as! CollectionView, grid: self.grid(indexPath.section)) 174 | } 175 | 176 | public func collectionView(_ collectionView: UICollectionView, 177 | layout collectionViewLayout: UICollectionViewLayout, 178 | insetForSectionAt section: Int) -> UIEdgeInsets { 179 | return self.grid(section).margin 180 | } 181 | 182 | public func collectionView(_ collectionView: UICollectionView, 183 | layout collectionViewLayout: UICollectionViewLayout, 184 | minimumLineSpacingForSectionAt section: Int) -> CGFloat { 185 | return self.grid(section).verticalPadding 186 | } 187 | 188 | public func collectionView(_ collectionView: UICollectionView, 189 | layout collectionViewLayout: UICollectionViewLayout, 190 | minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 191 | return self.grid(section).horizontalPadding 192 | } 193 | 194 | public func collectionView(_ collectionView: UICollectionView, 195 | layout collectionViewLayout: UICollectionViewLayout, 196 | referenceSizeForHeaderInSection section: Int) -> CGSize { 197 | guard let viewModel = self.itemAt(section)?.header else { 198 | return .zero 199 | } 200 | return viewModel.size(collectionView: collectionView as! CollectionView, grid: self.grid(section)) 201 | } 202 | 203 | public func collectionView(_ collectionView: UICollectionView, 204 | layout collectionViewLayout: UICollectionViewLayout, 205 | referenceSizeForFooterInSection section: Int) -> CGSize { 206 | guard let viewModel = self.itemAt(section)?.footer else { 207 | return .zero 208 | } 209 | return viewModel.size(collectionView: collectionView as! CollectionView, grid: self.grid(section)) 210 | } 211 | } 212 | #endif 213 | -------------------------------------------------------------------------------- /Sources/CollectionView/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewViewModel.swift 3 | // CVVM 4 | // 5 | // Created by Tibor Bödecs on 2018. 04. 11.. 6 | // Copyright © 2018. Tibor Bödecs. All rights reserved. 7 | // 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public protocol ViewModelProtocol { 12 | 13 | var id: String { get } 14 | var cell: Cell.Type { get } 15 | 16 | func config(cell: Cell, collectionView: CollectionView, indexPath: IndexPath, grid: Grid) 17 | func size(collectionView: CollectionView, grid: Grid) -> CGSize 18 | func callback(indexPath: IndexPath) 19 | } 20 | 21 | open class ViewModel: ViewModelProtocol where View: Cell, Model: Any { 22 | 23 | public typealias ViewModelHandler = ((ViewModel) -> Void) 24 | 25 | public var explicitId: String? 26 | public var model: Model 27 | public var cell: Cell.Type { return View.self } 28 | public var value: Any { return self.model } 29 | 30 | public weak var view: View? 31 | public weak var collectionView: CollectionView! 32 | public var indexPath: IndexPath! 33 | 34 | public var selectionHandler: ViewModelHandler? 35 | 36 | open var height: CGFloat { 37 | return 44 38 | } 39 | 40 | public var id: String { 41 | if let id = self.explicitId { 42 | return id 43 | } 44 | return "\(self.indexPath.section)/\(self.indexPath.item)" 45 | } 46 | 47 | // MARK: - init 48 | 49 | public init(id explicitId: String? = nil, _ data: Model) { 50 | self.explicitId = explicitId 51 | self.model = data 52 | self.initialize() 53 | } 54 | 55 | open func initialize() { 56 | 57 | } 58 | 59 | // MARK: - CollectionViewViewModelProtocol 60 | 61 | public func config(cell: Cell, collectionView: CollectionView, indexPath: IndexPath, grid: Grid) { 62 | self.collectionView = collectionView 63 | self.indexPath = indexPath 64 | self.view = (cell as! View) 65 | 66 | self.updateView() 67 | } 68 | 69 | public func size(collectionView: CollectionView, grid: Grid) -> CGSize { 70 | self.collectionView = collectionView 71 | return self.size(grid: grid) 72 | } 73 | 74 | public func callback(indexPath: IndexPath) { 75 | self.selectionHandler?(self) 76 | } 77 | 78 | // MARK: - API methods 79 | 80 | open func updateView() { 81 | 82 | } 83 | 84 | open func size(grid: Grid) -> CGSize { 85 | return grid.size(for: self.collectionView, height: self.height, items: grid.columns, gaps: grid.columns - 1) 86 | } 87 | 88 | open func onSelect(_ handler: @escaping ViewModelHandler) -> Self { 89 | self.selectionHandler = handler 90 | return self 91 | } 92 | 93 | public func by(id: String) -> ViewModel? { 94 | return self.collectionView?.source?.by(id: id) 95 | } 96 | } 97 | #endif 98 | -------------------------------------------------------------------------------- /Tests/CollectionViewTests/CollectionViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CollectionView 3 | 4 | final class CollectionViewTests: XCTestCase { 5 | 6 | func testExample() { 7 | XCTAssertTrue(true) 8 | } 9 | } 10 | --------------------------------------------------------------------------------