Created
March 21, 2020 21:59
-
-
Save simme/e9254cf8e93c6f8371c899c787dda00e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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