I am building an app that relies heavily on collection view so I decided to use IGListKit. Here is what I have to do:
and here is what I have:
... and it is not moving in any direction!
Here is my code:
import UIKit
import IGListKit
class MatchCollectionViewCell: UICollectionViewCell {
var helloWorld = "Hello World"
}
class LabelSectionController: ListSectionController {
override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
return collectionContext!.dequeueReusableCell(of: MatchCollectionViewCell.self, for: self, at: index)
}
}
class MatchViewController: UIViewController {
// MARK: - Outlets
#IBOutlet weak var matchCollectionView: UICollectionView!
// MARK: - Variables
let layout = UICollectionViewFlowLayout()
lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
}()
var users: [User] = []
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
matchCollectionView.dataSource = self
matchCollectionView.delegate = self
matchCollectionView.collectionViewLayout = CenterCellCollectionViewFlowLayout()
matchCollectionView.isPagingEnabled = true
layout.minimumLineSpacing = 8
layout.scrollDirection = .horizontal
for i in 1...10 {
let newMatchingPreferences = MatchingPreferences(preferedAge: (23, 33))
let newUser = User(id: i, name: "Some Name \(i)", email: "some_name#gmail.com", age: 27, location: "New York", isOnboarded: true, isPremium: true, matchingPreferences: newMatchingPreferences)
users.append(newUser)
}
}
// MARK: - Actions
// MARK: - Methods
}
extension MatchViewController: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return self.users
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return LabelSectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
let view = UIView()
view.backgroundColor = .lightGray
return view
}
}
extension MatchViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = matchCollectionView.dequeueReusableCell(withReuseIdentifier: "MatchCell", for: indexPath) as! MatchCollectionViewCell
print(cell.helloWorld)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.bounds.width - 32, height: view.bounds.height - 40)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return .zero
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout {
var mostRecentOffset: CGPoint = CGPoint.zero
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if velocity.x == 0 {
return self.mostRecentOffset
}
guard let cv = self.collectionView,
let attributesForVisibleCells = self.layoutAttributesForElements(in: cv.bounds) else {
// Fallback
self.mostRecentOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
return self.mostRecentOffset
}
let halfWidth = cv.bounds.size.width * 0.5
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// Skip comparison with non-cell items (headers and footers)
if attributes.representedElementCategory != UICollectionElementCategory.cell {
continue
}
if (attributes.center.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) {
continue
}
candidateAttributes = attributes
}
// Beautification step , I don't know why it works!
if proposedContentOffset.x == -(cv.contentInset.left) {
return proposedContentOffset
}
guard let attributes = candidateAttributes else {
return mostRecentOffset
}
self.mostRecentOffset = CGPoint(x: floor(attributes.center.x - halfWidth), y: proposedContentOffset.y)
return self.mostRecentOffset
}
}
Basically, I know how to make crude collection view but paging and centering is a bit hard for me. I feel like I am close but obviously something is missing. I hope somebody can point me to a right direction! Thanks.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == self.collectionView {
var currentCellOffset = self.collectionView.contentOffset
currentCellOffset.x += self.collectionView.frame.width / 2
if let indexPath = self.collectionView.indexPathForItem(at: currentCellOffset) {
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
}
}
make sure to set the
adapter.scrollDelegate = self
and
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
(for more of a native iOS feel)
this worked for me I hope it helps someone else!
Related
I have a CollectionView where all of my cells are aligned to the center of the view, utilising a custom flow layout. Here is the code for the collectionViewController:
let columnLayout = FlowLayout(
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
var dataSource: [String] = ["USA", "Brazil", "China","Really long long long", "Unite Kingdom", "Japan", "Mexico", "India", "Really long long", "Short", "USA", "Brazil", "China","Really long long long", "Unite Kingdom", "Japan", "Mexico", "India", "Really long long"]
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = columnLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(TestCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dragInteractionEnabled = true
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// dataArary is the managing array for your UICollectionView.
let item: String = dataSource[indexPath.row]
return CGSize(width: item.widthOfString(usingFont: .systemFont(ofSize: 16)) + 25, height: 40)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? TestCell
cell?.textLabel.text = dataSource[indexPath.row]
return cell!
}
fileprivate func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
if let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates ({
self.dataSource.remove(at: sourceIndexPath.item)
self.dataSource.insert(item.dragItem.localObject as! String, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: nil)
collectionView.reloadData()
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
Code for the FlowLayout Class:
class FlowLayout: UICollectionViewFlowLayout {
required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Get the total width of the cells on the same row
let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
partialWidth + attribute.size.width
}
// Calculate the initial left inset
let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
I then added a UICollectionViewDragDelegate and a UICollectionViewDropDelegate extensions in order to enable the user to reorder the cells. The two extensions:
extension CollectionViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = self.dataSource[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
extension CollectionViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UICollectionViewDropProposal(operation: .forbidden)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
var destinationindexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationindexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationindexPath = IndexPath(item: row - 1, section: 0)
}
if coordinator.proposal.operation == .move {
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationindexPath, collectionView: collectionView)
}
}
}
For some strange reason the cells glitch out in the app when reordering them:
https://imgur.com/dmEQLqh
Thanks for the help
I followed several cars to create a collectionViewDataSource, and I am extremely happy with the result:
let dataSource = CollectionViewDataSourceProvider(items: domains, cell: EditDomainsCollectionViewCell.self) { indexPath, item, cell in
cell.setup(with: item)
}.registerCell(for: domainsCollectionView)
self.datasource = dataSource
domainsCollectionView.dataSource = datasource
domainsCollectionView.delegate = datasource
Here my dataSourceProvider class:
class CollectionViewDataSourceProvider<Item, Cell: UICollectionViewCell & NibLoadableView>: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
typealias CellConfigurator = (_ indexPath: IndexPath, _ item: Item, _ cell: Cell) -> ()
typealias SizeConfigurator = (_ indexPath: IndexPath, _ item: Item) -> CGSize
typealias WidthConfigurator = (_ section: Int) -> CGFloat
typealias InsetConfigurator = (_ section: Int) -> UIEdgeInsets
typealias SelectConfigurator = (_ indexPath: IndexPath, _ item: Item) -> ()
private let items: [Item]
private let cell: Cell.Type
private let cellConfigurator: CellConfigurator
private var sizeConfigurator: SizeConfigurator?
private var minimumLineSpacingForSectionAtConfigurator: WidthConfigurator?
private var minimumInteritemSpacingForSectionAt: WidthConfigurator?
private var insetForSectionAt: InsetConfigurator?
private var didSelectItemAt: SelectConfigurator?
init(items: [Item], cell: Cell.Type, cellConfigurator: #escaping CellConfigurator) {
self.items = items
self.cell = cell
self.cellConfigurator = cellConfigurator
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let model = items[indexPath.row]
let cell = loadNIB()
return sizeConfigurator?(indexPath, model) ?? cell.frame.size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return minimumInteritemSpacingForSectionAt?(section) ?? (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing ?? 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return insetForSectionAt?(section) ?? (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset ?? UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
didSelectItemAt?(indexPath, items[indexPath.row])
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return minimumLineSpacingForSectionAtConfigurator?(section) ?? (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing ?? 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = items[indexPath.row]
let cell: Cell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cellConfigurator(indexPath, model, cell)
return cell
}
private func loadNIB() -> Cell {
return Bundle(for: Cell.self as AnyClass).loadNibNamed(String(describing: Cell.self), owner: nil, options: nil)![0] as! Cell
}
}
extension CollectionViewDataSourceProvider {
func registerCell(for collectionView: UICollectionView, flowLayout: UICollectionViewFlowLayout? = nil) -> CollectionViewDataSourceProvider {
collectionView.register(cell)
if let flowLayout = flowLayout {
collectionView.setCollectionViewLayout(flowLayout, animated: true)
}
return self
}
func heightForRow(config: #escaping SizeConfigurator) -> CollectionViewDataSourceProvider {
sizeConfigurator = config
return self
}
func minimumLineSpacingForSectionAt(config: #escaping WidthConfigurator) -> CollectionViewDataSourceProvider {
minimumLineSpacingForSectionAtConfigurator = config
return self
}
func minimumInteritemSpacingForSectionAt(config: #escaping WidthConfigurator) -> CollectionViewDataSourceProvider {
minimumInteritemSpacingForSectionAt = config
return self
}
func insetForSectionAt(config: #escaping InsetConfigurator) -> CollectionViewDataSourceProvider {
insetForSectionAt = config
return self
}
func didSelectedAt(config: #escaping SelectConfigurator) -> CollectionViewDataSourceProvider {
didSelectItemAt = config
return self
}
}
After all, I am trying to implement the possibility of using several different cell by protocol but I cannot adapt it with my provider class.
Here is the pattern I want to make to create multiple cell types quite simply:
enum CellType {
case typeOne, typeTwo
var identifier: String {
switch self {
case .typeOne:
return NSStringFromClass(CellOne.self)
case .typeTwo:
return NSStringFromClass(CellTwo.self)
}
}
}
protocol CustomElement {
var type: CellType { get }
}
class MyFirstObject: CustomElement {
var type: CellType { .typeOne}
}
class MySecondObject: CustomElement {
var type: CellType { .typeTwo}
}
protocol CellElement where Self: UICollectionViewCell & NibLoadableView {
func configure(with object: CustomElement)
}
class CellOne: UICollectionViewCell, CellElement, NibLoadableView {
func configure(with object: CustomElement) {
guard let object = object as? MySecondObject else { return }
//
}
}
class CellTwo: UICollectionViewCell, CellElement, NibLoadableView {
func configure(with object: CustomElement) {
guard let object = object as? MyFirstObject else { return }
//
}
}
The NibLoadableView protocol comes from this gist, to save cells easily https://gist.github.com/gonzalezreal/92507b53d2b1e267d49a
I want my collectionViewDataSourceProvider to be able to receive in the init an array of UICollectionView cell, and if there is more than one cell then implement the protocol logic for multiple cell. But I can't do it.
For example, I am trying to set the protocol as a generic parameter in my datasource, but it tells me that the protocol does not inherit from IUCollectionView.
I have no direction how to do this thank you for your help
Hi I want to do drag drop using uicollectionview. When performing drag and drop it is moving contents i want to do it like in the photo. I want the box to carry itself. For example; when I drag the photo to 4 I should leave the red area with full measurements. Swap photo 6 to photo 1 like taking photo 3 to the left. I have researched in uicollectionview so much but I can’t find anything like this. Please help me
import UIKit
final class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var cellIds = ["image 1","image 2","image 3","image 4","image 5","6","7"]
override func viewDidLoad() {
super.viewDidLoad()
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSize(width: 200, height: 10)
let gestureRecognizer = UILongPressGestureRecognizer(target: self,
action: #selector(self.handleLongPress(gestureRecognizer:)))
collectionView.addGestureRecognizer(gestureRecognizer)
}
#objc func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
guard let view = gestureRecognizer.view else { return }
let location = gestureRecognizer.location(in: view)
switch gestureRecognizer.state {
case .began:
guard let selectedIndexPath = collectionView.indexPathForItem(at: location) else { break }
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(location)
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
}
extension ViewController: UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout
{
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cellIds.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SampleCell", for: indexPath) as! SampleCell
let text = cellIds[indexPath.item]
cell.label.text = text
return cell
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let text = cellIds.remove(at: sourceIndexPath.item)
cellIds.insert(text, at: destinationIndexPath.item)
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if (indexPath.row==0)
{
return CGSize(width: 190, height: 100)
}
if (indexPath.row==1)
{
return CGSize(width: 190, height: 100)
}
if (indexPath.row==2)
{
return CGSize(width: 190, height: 400)
}
if (indexPath.row==3)
{
return CGSize(width: 400, height: 200)
}
return CGSize(width: 0, height: 0)
}
}
final class SampleCell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
let size = self.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
// note: don't change the width
newFrame.size.height = ceil(size.height)
layoutAttributes.frame = newFrame
return layoutAttributes
}
}
picture
You can use UICollectionViewDragDelegate,
For multiple sections, in order to drag to the end item, we should add an extra item when dragging.
Sample Code:
ViewController:
import UIKit
enum CellModel {
case simple(text: String)
case availableToDropAtEnd
}
class SecondController: UIViewController {
private lazy var cellIdentifier = "cellIdentifier"
private lazy var supplementaryViewIdentifier = "supplementaryViewIdentifier"
private lazy var sections = 10
private lazy var itemsInSection = 2
private lazy var numberOfElementsInRow = 3
private lazy var data: [[CellModel]] = {
var count = 0
return (0 ..< sections).map { _ in
return (0 ..< itemsInSection).map { _ -> CellModel in
count += 1
return .simple(text: "cell \(count)")
}
}
}()
override func viewDidLoad() {
super.viewDidLoad()
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.minimumLineSpacing = 5
collectionViewFlowLayout.minimumInteritemSpacing = 5
let _numberOfElementsInRow = CGFloat(numberOfElementsInRow)
let allWidthBetwenCells = _numberOfElementsInRow == 0 ? 0 : collectionViewFlowLayout.minimumInteritemSpacing*(_numberOfElementsInRow-1)
let width = (view.frame.width - allWidthBetwenCells)/_numberOfElementsInRow
collectionViewFlowLayout.itemSize = CGSize(width: width, height: width)
collectionViewFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
collectionView.backgroundColor = .white
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
collectionView.register(SupplementaryView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: supplementaryViewIdentifier)
collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .fast
collectionView.dropDelegate = self
collectionView.dragDelegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension SecondController: UICollectionViewDelegate { }
extension SecondController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CollectionViewCell
switch data[indexPath.section][indexPath.item] {
case .simple(let text):
cell.label?.text = text
cell.backgroundColor = .gray
case .availableToDropAtEnd:
cell.backgroundColor = UIColor.green.withAlphaComponent(0.3)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: supplementaryViewIdentifier, for: indexPath)
}
}
extension SecondController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.row]
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.row]
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {
var itemsToInsert = [IndexPath]()
(0 ..< data.count).forEach {
itemsToInsert.append(IndexPath(item: data[$0].count, section: $0))
data[$0].append(.availableToDropAtEnd)
}
collectionView.insertItems(at: itemsToInsert)
}
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
var removeItems = [IndexPath]()
for section in 0..<data.count {
for item in 0..<data[section].count {
switch data[section][item] {
case .availableToDropAtEnd:
removeItems.append(IndexPath(item: item, section: section))
case .simple:
break
}
}
}
removeItems.forEach { data[$0.section].remove(at: $0.item) }
collectionView.deleteItems(at: removeItems)
}
}
extension SecondController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// useless, just in case
let section = collectionView.numberOfSections - 1
let row = collectionView.numberOfItems(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
switch coordinator.proposal.operation {
case .move:
reorderItems(coordinator: coordinator, destinationIndexPath:destinationIndexPath, collectionView: collectionView)
default:
break
}
}
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
// made the above logic useless
if collectionView.hasActiveDrag, destinationIndexPath != nil {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
else {
return UICollectionViewDropProposal(operation: .forbidden)
}
}
private
func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
let items = coordinator.items
if items.count == 1, let item = items.first,
let sourceIndexPath = item.sourceIndexPath,
let localObject = item.dragItem.localObject as? CellModel {
collectionView.performBatchUpdates ({
data[sourceIndexPath.section].remove(at: sourceIndexPath.item)
data[destinationIndexPath.section].insert(localObject, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
})
}
}
}
View:
import UIKit
class CollectionViewCell: UICollectionViewCell {
weak var label: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
let label = UILabel(frame: .zero)
label.contentMode = .scaleAspectFill
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
label.textAlignment = .center
label.textColor = .white
self.label = label
layer.borderWidth = 1
layer.borderColor = UIColor.white.cgColor
backgroundColor = .white
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForReuse() {
super.prepareForReuse()
label?.text = nil
backgroundColor = .white
}
}
class SupplementaryView: UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.blue.withAlphaComponent(0.7)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
github link
I am working on a App where I can keep track of my substitutions of my youth soccer team.
Following a tutorial by Payal Gupta on drag & drop into collections & tables I managed to get a drag and drop between two collection views (PlayersOntoTheField and Substitutes) working Screenshot.
When I drag a substitute player into my playground it should now snap to the predefined team line-up (e.g. 3-2-1 in the screenshot). Is it possible to get such behavior with a custom UICollectionViewLayout or does anyone have another suggestion?
Thank you you very much in advance for any help.
Danny
//Based on a work by:
//Payal Gupta (https://github.com/pgpt10/DragAndDrop-CollectionView)
import UIKit
class ViewController: UIViewController
{
private var substitutes = ["player1", "player2", "player3", "player4"]
private var players = [String]()
#IBOutlet weak var substitutesCollectionView: UICollectionView!
#IBOutlet weak var playersCollectionView: UICollectionView!
override func viewDidLoad()
{
super.viewDidLoad()
//SubstitutesCollectionView drag and drop configuration
self.substitutesCollectionView.dragInteractionEnabled = true
self.substitutesCollectionView.dragDelegate = self
self.substitutesCollectionView.dropDelegate = self
//PlayersCollectionView drag and drop configuration
self.playersCollectionView.dragInteractionEnabled = true
self.playersCollectionView.dropDelegate = self
self.playersCollectionView.dragDelegate = self
self.playersCollectionView.reorderingCadence = .fast //default value - .immediate
}
//MARK: Private Methods
private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView)
{
let items = coordinator.items
if items.count == 1, let item = items.first, let sourceIndexPath = item.sourceIndexPath
{
var dIndexPath = destinationIndexPath
if dIndexPath.row >= collectionView.numberOfItems(inSection: 0)
{
dIndexPath.row = collectionView.numberOfItems(inSection: 0) - 1
}
collectionView.performBatchUpdates({
if collectionView === self.playersCollectionView
{
self.players.remove(at: sourceIndexPath.row)
self.players.insert(item.dragItem.localObject as! String, at: dIndexPath.row)
}
else
{
self.substitutes.remove(at: sourceIndexPath.row)
self.substitutes.insert(item.dragItem.localObject as! String, at: dIndexPath.row)
}
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [dIndexPath])
})
coordinator.drop(items.first!.dragItem, toItemAt: dIndexPath)
}
}
private func copyItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView)
{
collectionView.performBatchUpdates({
var indexPaths = [IndexPath]()
for (index, item) in coordinator.items.enumerated()
{
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
if collectionView === self.playersCollectionView
{
self.players.insert(item.dragItem.localObject as! String, at: indexPath.row)
}
else
{
self.substitutes.insert(item.dragItem.localObject as! String, at: indexPath.row)
}
indexPaths.append(indexPath)
}
collectionView.insertItems(at: indexPaths)
})
}
}
// MARK: - UICollectionViewDataSource Methods
extension ViewController : UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return collectionView == self.substitutesCollectionView ? self.substitutes.count : self.players.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
if collectionView == self.substitutesCollectionView
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell1", for: indexPath) as! MyCollectionViewCell
cell.customImageView?.image = UIImage(named: self.substitutes[indexPath.row])
cell.customLabel.text = self.substitutes[indexPath.row].capitalized
return cell
}
else
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell2", for: indexPath) as! MyCollectionViewCell
cell.customImageView?.image = UIImage(named: self.players[indexPath.row])
cell.customLabel.text = self.players[indexPath.row].capitalized
return cell
}
}
}
// MARK: - UICollectionViewDragDelegate Methods
extension ViewController : UICollectionViewDragDelegate
{
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
{
let item = collectionView == substitutesCollectionView ? self.substitutes[indexPath.row] : self.players[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]
{
let item = collectionView == substitutesCollectionView ? self.substitutes[indexPath.row] : self.players[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters?
{
if collectionView == substitutesCollectionView
{
let previewParameters = UIDragPreviewParameters()
previewParameters.visiblePath = UIBezierPath(rect: CGRect(x: 15, y: 5, width: 30, height: 30))
return previewParameters
}
return nil
}
}
// MARK: - UICollectionViewDropDelegate Methods
extension ViewController : UICollectionViewDropDelegate
{
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool
{
return session.canLoadObjects(ofClass: NSString.self)
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal
{
if collectionView === self.substitutesCollectionView
{
if collectionView.hasActiveDrag
{
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
else
{
return UICollectionViewDropProposal(operation: .forbidden)
}
}
else
{
if collectionView.hasActiveDrag
{
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
else
{
return UICollectionViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)
{
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath
{
destinationIndexPath = indexPath
}
else
{
// Get last index path of table view.
let section = collectionView.numberOfSections - 1
let row = collectionView.numberOfItems(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
switch coordinator.proposal.operation
{
case .move:
self.reorderItems(coordinator: coordinator, destinationIndexPath:destinationIndexPath, collectionView: collectionView)
break
case .copy:
self.copyItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
default:
return
}
}
}
That my workaround for custom UICollectionViewLayout so far:
import UIKit
class LineUp_3_2_1View: UICollectionViewLayout {
private var center: CGPoint!
private var itemSize: CGSize!
private var radiusOfCircleViews: CGFloat!
private var numberOfItems: Int!
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
radiusOfCircleViews = CGFloat(30.0)
itemSize = CGSize(width: radiusOfCircleViews * 2, height: radiusOfCircleViews * 2)
center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
numberOfItems = collectionView.numberOfItems(inSection: 0)
}
override var collectionViewContentSize: CGSize {
return collectionView!.bounds.size
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
if (indexPath.item == 0) {attributes.center = CGPoint(x: 169, y: 344)}
if (indexPath.item == 1) {attributes.center = CGPoint(x: 46, y: 250)}
if (indexPath.item == 2) {attributes.center = CGPoint(x: 169, y: 250)}
if (indexPath.item == 3) {attributes.center = CGPoint(x: 287, y: 250)}
if (indexPath.item == 4) {attributes.center = CGPoint(x: 80, y: 156)}
if (indexPath.item == 5) {attributes.center = CGPoint(x: 253, y: 156)}
if (indexPath.item == 6) {attributes.center = CGPoint(x: 169, y: 62)}
attributes.size = itemSize
return attributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return (0 ..< collectionView!.numberOfItems(inSection: 0))
.flatMap { item -> UICollectionViewLayoutAttributes? in // `compactMap` in Xcode 9.3
self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))
}
}
}
I have a UITableView within a UIViewController that loads the ranking of users. Having only one ranking list was a little bit too limited, so I have added a custom bar to load different rankings (weekly, monthly and yearly). The reason why I chose to do it this way is because it gave me a lot of control over my layout constraints - segmented control does not.
The current problem is that I don't exactly know how to return the right array based on the selected tab in my menu bar. As of now I use a fourth empty array to copy the data of one of the other three when the tab is selected, but how do I send this data back to my initial view so that I can return the count in my tableView's numberOfRowsInSection?
ViewController & TableViewController
class Rank: NSObject{
var name: String
var points: Int
init(name: String, points: Int) {
self.name = name
self.points = points
}
}
var rankingArrayWeek = [Rank]()
var rankingArrayMonth = [Rank]()
var rankingArrayTotal = [Rank]()
var filteredRanking = [Rank]()
class RankingController: UIViewController, UITableViewDelegate {
weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
setupMenuBar()
tableView?.delegate = self
rankingArrayWeek = [
Rank(name: "Name1", points: 200)
]
rankingArrayMonth = [
Rank(name: "Name1", points: 300),
Rank(name: "Name2", points: 200),
]
rankingArrayTotal = [
Rank(name: "Name1", points: 500),
Rank(name: "Name2", points: 400),
Rank(name: "Name3", points: 300),
]
let rankingTableViewController = RankingTableViewController()
self.addChildViewController(rankingTableViewController)
rankingTableViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(rankingTableViewController.view)
rankingTableViewController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 50).isActive = true
rankingTableViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 10).isActive = true
rankingTableViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
rankingTableViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
lazy var menuBar: MenuBar = {
let menuBar = MenuBar()
menuBar.rankingController = self
return menuBar
}()
private func setupMenuBar() {
navigationController?.hidesBarsOnSwipe = true
view.addSubview(menuBar)
view.addConstraintsWithFormat("H:|[v0]|", views: menuBar)
view.addConstraintsWithFormat("V:|[v0(50)]", views: menuBar)
}
}
// MARK: TableViewController
class RankingTableViewController: UITableViewController {
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
tableView?.register(RankCell.self, forCellReuseIdentifier: cellId)
tableView?.tableFooterView = UIView(frame: CGRect.zero)
tableView?.rowHeight = 60
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredRanking.count
}
}
My custom menubar
class MenuBar: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.isScrollEnabled = false
collectionView.backgroundColor = .white
collectionView.dataSource = self
collectionView.delegate = self
return collectionView
}()
let cellId = "cellId"
let names = ["Week", "Month", "Total"]
var rankingController: RankingController?
override init(frame: CGRect) {
super.init(frame: frame)
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)
addSubview(collectionView)
addConstraintsWithFormat("H:|[v0]|", views: collectionView)
addConstraintsWithFormat("V:|[v0]|", views: collectionView)
let selectedIndexPath = NSIndexPath(item: 2, section: 0)
collectionView.selectItem(at: selectedIndexPath as IndexPath, animated: false, scrollPosition: .centeredHorizontally)
setupHorizontalBar()
}
var horizontalBarLeftAnchorConstraint: NSLayoutConstraint?
func setupHorizontalBar() {
let horizontalBarView = UIView()
horizontalBarView.backgroundColor = Constants.MAIN_THEME_COLOR
horizontalBarView.translatesAutoresizingMaskIntoConstraints = false
addSubview(horizontalBarView)
horizontalBarLeftAnchorConstraint = horizontalBarView.leftAnchor.constraint(equalTo: self.leftAnchor)
horizontalBarLeftAnchorConstraint?.isActive = true
horizontalBarView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
horizontalBarView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1/3).isActive = true
horizontalBarView.heightAnchor.constraint(equalToConstant: 4).isActive = true
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let x = CGFloat(indexPath.item) * frame.width / 3
horizontalBarLeftAnchorConstraint?.constant = x
UIView.animate(withDuration: 0.75, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: { self.layoutIfNeeded()
}, completion: nil)
if indexPath.item == 0 {
filteredRanking = rankingArrayWeek
print(filteredRanking.count)
} else if indexPath.item == 1 {
filteredRanking = rankingArrayMonth
print(filteredRanking.count)
} else {
filteredRanking = rankingArrayTotal
print(filteredRanking.count)
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
cell.buttonView.text = "\(names[indexPath.item])"
cell.buttonView.textColor = Constants.MAIN_THEME_COLOR
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width / 3, height: frame.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
Updated code:
To preselect the first row, I also added this to my cellForItemAt indexPath:
if (cell.isSelected == false) {
didSelect?(kinds[0])
}
Suggest that you add a callback on your menu bar that says what kind of rank has been selected. You can also use this "Kind" to drive the display of the menu bar.
enum RankKind: String {
case week = "Week"
case month = "Month"
case total = "Total"
}
class MenuBar {
let kinds = [RankKind.week, .month, .total]
var didSelect: ((RankKind)->())?
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return kinds.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
cell.buttonView.text = kinds[indexPath.item].rawValue)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
didSelect?(kinds[indexPath.item])
}
}
Then the RankingController can set itself up to know when the menu has changed kind.
class RankingController {
func viewDidLoad() {
menuBar.didSelect = { kind in
rankingTableViewController.rankingArray = self.rankingArrayFor(kind: kind)
}
}
func rankingArrayFor(kind: RankKind) -> [Rank] {
switch kind {
case .week: return rankingArrayWeek
case .month: return rankingArrayMonth
case .total:return rankingArrayTotal
}
}
}
Lastly, the RankingTableViewController exposes its model (an array), and reloads its tableview when that model is reset.
class RankingTableViewController: UITableViewController {
var rankingArray: [Rank] = [] {
didSet {
self.tableView.reloadData()
}
}
}
The above code is additional to the original question's code existing for brevity, i.e it is not stand alone.