Skip to content

Instantly share code, notes, and snippets.

@simme
Created March 21, 2020 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simme/e9254cf8e93c6f8371c899c787dda00e to your computer and use it in GitHub Desktop.
Save simme/e9254cf8e93c6f8371c899c787dda00e to your computer and use it in GitHub Desktop.
import UIKit
public protocol CellConfinable: UIView {
associatedtype Item
var isSelected: Bool { get set }
var isHighlighted: Bool { get set }
func prepareForReuse()
func configure(for item: Item)
}
import Combine
import Foundation
import UIKit
public protocol DefaultSection: Hashable {
static var defaultSection: Self { get }
}
extension String: DefaultSection {
public static var defaultSection: String { "main" }
}
extension Int: DefaultSection {
public static var defaultSection: Int { 0 }
}
public protocol CollectionViewControllerDelegate: AnyObject {
func move(items: [UICollectionViewDropItem], to indexPath: IndexPath)
}
open class CollectionViewController<Item: Hashable, View: CellConfinable, Section: DefaultSection>: UIViewController, UICollectionViewDragDelegate, UICollectionViewDropDelegate where View.Item == Item {
private var subscriptions: Set<AnyCancellable> = []
private(set) var publisher: AnyPublisher<[Item], Never>
var groupingKeyPath: KeyPath<Item, Section>?
lazy var dataSource: CollectionDiffableDataSource<Section, Item> = {
CollectionDiffableDataSource(collectionView: self.collectionView, cellProvider: { [unowned self] in
ViewHostingCollectionViewCell<View, Item>.dequeue(from: $0, at: $1).configure(for: $2)
})
}()
public var collectionView: UICollectionView {
view as! UICollectionView
}
open override func loadView() {
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .systemBackground
}
private(set) var layout: UICollectionViewLayout
public init(
layout: UICollectionViewLayout,
publisher: AnyPublisher<[Item], Never>,
groupingKeyPath: KeyPath<Item, Section>? = nil
) {
self.layout = layout
self.publisher = publisher
self.groupingKeyPath = groupingKeyPath
super.init(nibName: nil, bundle: nil)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func viewDidLoad() {
super.viewDidLoad()
collectionView.dropDelegate = self
collectionView.dragDelegate = self
collectionView.dragInteractionEnabled = true
collectionView.alwaysBounceVertical = true
ViewHostingCollectionViewCell<View, Item>.register(with: collectionView)
publisher.throttle(for: 0.2, scheduler: RunLoop.main, latest: true).sink(receiveValue: apply).store(in: &subscriptions)
}
private func apply(_ items: [Item]) {
var addedSections: Set<Section> = []
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
if let groupingKeyPath = groupingKeyPath {
for item in items {
let sectionIdentifier = item[keyPath: groupingKeyPath]
if !addedSections.contains(sectionIdentifier) {
snapshot.appendSections([sectionIdentifier])
addedSections.insert(sectionIdentifier)
}
snapshot.appendItems([item], toSection: sectionIdentifier)
}
} else {
snapshot.appendSections([Section.defaultSection])
snapshot.appendItems(items)
}
dataSource.apply(snapshot, animatingDifferences: true)
}
// MARK: - Drag Delegate
public func collectionView(
_ collectionView: UICollectionView,
itemsForBeginning session: UIDragSession,
at indexPath: IndexPath
) -> [UIDragItem] {
let provider = NSItemProvider()
let item = UIDragItem(itemProvider: provider)
item.localObject = dataSource.itemIdentifier(for: indexPath)
return [item]
}
public func collectionView(
_ collectionView: UICollectionView,
itemsForAddingTo session: UIDragSession,
at indexPath: IndexPath,
point: CGPoint
) -> [UIDragItem] {
let provider = NSItemProvider()
let item = UIDragItem(itemProvider: provider)
item.localObject = dataSource.itemIdentifier(for: indexPath)
return [item]
}
// MARK: - Drop Delegate
public func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
return true
}
public func collectionView(
_ collectionView: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath destinationIndexPath: IndexPath?
) -> UICollectionViewDropProposal {
UICollectionViewDropProposal(operation: .move, intent: .unspecified)
}
public func collectionView(
_ collectionView: UICollectionView,
performDropWith coordinator: UICollectionViewDropCoordinator
) {
// let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)
// let items = coordinator.items
}
}
import Foundation
import UIKit
// MARK: - Reusable View
public protocol ReusableCollectionReusableView: UICollectionReusableView {
static var elementKind: String { get }
static var reuseIdentifier: String { get }
}
public extension ReusableCollectionReusableView {
static var elementKind: String { String(describing: Self.self) + "-element-kind" }
static var reuseIdentifier: String { String(describing: Self.self) }
static func register(with collectionView: UICollectionView) {
collectionView.register(Self.self,
forSupplementaryViewOfKind: Self.elementKind,
withReuseIdentifier: Self.reuseIdentifier)
}
static func dequeue(from collectionView: UICollectionView, at indexPath: IndexPath) -> Self {
collectionView.dequeueReusableSupplementaryView(
ofKind: Self.elementKind,
withReuseIdentifier: Self.reuseIdentifier,
for: indexPath) as! Self
}
}
public protocol ConfigurableCollectionReusableView: ReusableCollectionReusableView {
associatedtype Item
func configure(for item: Item)
}
// MARK: - Cell
public protocol ReusableCell: UICollectionViewCell {
static var reuseIdentifier: String { get }
}
public extension ReusableCell {
static var reuseIdentifier: String { String(describing: Self.self) }
static func register(with collectionView: UICollectionView) {
collectionView.register(Self.self, forCellWithReuseIdentifier: Self.reuseIdentifier)
}
static func dequeue(from collectionView: UICollectionView, at indexPath: IndexPath) -> Self {
collectionView.dequeueReusableCell(
withReuseIdentifier: Self.reuseIdentifier,
for: indexPath) as! Self
}
}
public protocol ConfigurableCell: ReusableCell {
associatedtype Item
@discardableResult func configure(for item: Item) -> Self
}
// MARK: - Collection View
public extension UICollectionView {
func isIndexPathLastInSection(_ indexPath: IndexPath) -> Bool {
guard let count = dataSource?.collectionView(self, numberOfItemsInSection: indexPath.section) else {
return false
}
return indexPath.item == count - 1
}
}
public final class ViewHostingCollectionViewCell<View: CellConfinable, Item>: UICollectionViewCell, ConfigurableCell where Item == View.Item {
// MARK: Properties
/// The reuse identifier, made unique by using the type of the wrapped view.
public static var reuseIdentifier: String { return "hosted-\(String(describing: View.self))"}
/// The hosted view.
public let hostedView: View
/// The selection state of the cell.
public override var isSelected: Bool {
didSet {
hostedView.isSelected = isSelected
}
}
/// The highlight state of the cell.
public override var isHighlighted: Bool {
didSet {
hostedView.isHighlighted = isHighlighted
}
}
// MARK: Initialization
public override init(frame: CGRect) {
hostedView = View(frame: frame)
super.init(frame: frame)
contentView.addSubview(hostedView)
hostedView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostedView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
hostedView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
hostedView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.topAnchor.constraint(equalTo: hostedView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: hostedView.bottomAnchor)
])
}
public required init?(coder: NSCoder) { fatalError() }
// MARK: Cell Configuration
public override func prepareForReuse() {
super.prepareForReuse()
hostedView.prepareForReuse()
}
@discardableResult public func configure(for item: Item) -> Self {
hostedView.configure(for: item)
return self
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment