Reload CollectionView Cell with custom UICollectionViewFlowLayout - ios

I have a custom UICollectionViewFlowLayout which lays out items with a left-aligned format.
LAYOUT
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var newAttributesArray = [UICollectionViewLayoutAttributes]()
let superAttributesArray = super.layoutAttributesForElements(in: rect)!
guard let attributesToReturn = superAttributesArray.map( { $0.copy() }) as? [UICollectionViewLayoutAttributes] else {
return nil
}
for (index, attributes) in attributesToReturn.enumerated() {
if index == 0 || attributesToReturn[index - 1].frame.origin.y != attributes.frame.origin.y {
attributes.frame.origin.x = sectionInset.left
} else {
let previousAttributes = attributesToReturn[index - 1]
let previousFrameRight = previousAttributes.frame.origin.x + previousAttributes.frame.width
attributes.frame.origin.x = previousFrameRight + minimumInteritemSpacing
}
newAttributesArray.append(attributes)
}
return attributesToReturn
}
When I reload a cell that is not the first in the horizontal line, the cell tried performs peculiarly is in the below illustration. I believe this is a layout issue. Reloading a cell that's first does not act this way.
RELOAD METHOD
func selectedInterest(for cell: InterestCell) {
guard let indexPath = mainView.collectionView.indexPath(for: cell),
let documentID = cell.interest?.documentID,
let isSaved = cell.interest?.isSaved else { return }
var interest = self.interests[indexPath.item]
interest.isSaved = !isSaved
self.interests[indexPath.item] = interest
self.mainView.collectionView.collectionViewLayout.invalidateLayout()
self.mainView.collectionView.reloadItems(at: [indexPath])
}
I have tried to invalidate the layout before reloading however this has no effect.

Related

UICollectionView destroyed in UITableViewCell

how to clean UITableViewCell before reuse ? In cellForRow table view I set up viewModel and in didSet this View Model i change collectionView height constraint but when I scroll very fast then collection is totaly destroyed.
func setup(collectionView: UICollectionView) {
if self.collectionView != collectionView || self.collectionView == nil {
collectionView.register(UINib(nibName: "HomeRecommendationCollectionViewCell", bundle: nil),
forCellWithReuseIdentifier: HomeStoryboardConsts.Identifier.homeRecommendationCollectionViewCell.rawValue)
collectionView.contentInset = UIEdgeInsetsMake(0, 10, 0, 10)
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.minimumInteritemSpacing = 10
}
disposeBag = DisposeBag()
items.asObservable()
.subscribe(onNext: { [weak collectionView] _ in
collectionView?.reloadData()
})
.addDisposableTo(disposeBag)
self.collectionView = collectionView
}
}
collection View setup
viewModel Setup:
var viewModel: TitleAccessoryButtonCollectionViewModel? {
didSet {
guard let viewModel = viewModel else {
return
}
titleLabel.text = viewModel.title
if let buttonTitle = viewModel.accessoryButtonModel?.title {
setAccessoryButtonTitle(buttonTitle)
}else{
accessoryButton.hideTitleLabel()
}
if let buttonImage = viewModel.accessoryButtonModel?.image {
accessoryButton.buttonImageView.image = buttonImage
}
else {
accessoryButton.hideImageView()
}
sectionContentImage.image = viewModel.sectionContentImage
titleLabelLeadingConstraint.constant = viewModel.titleLabelLeadingSpacing
accessoryButton.isHidden = viewModel.hideAccessoryButton
sectionContentView.isHidden = viewModel.hidePremiumContentView
let collectionViewModel = viewModel.collectionViewModel
collectionViewHeight.constant = CGFloat(collectionViewModel.height)
collectionViewModel.setup(collectionView: collectionView)
collectionView.delegate = collectionViewModel.delegate
collectionView.dataSource = collectionViewModel.dataSource
collectionView.reloadData()
}
}
i have also in console
2018-09-19 14:27:52.335285+0200 App[76102:8433084] The behavior of the UICollectionViewFlowLayout is not defined because:
2018-09-19 14:27:52.335409+0200 App[76102:8433084] the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.
If you have a custom tableviewcell class for cells of the tableview. Then there is a method called prepareForReuse(). Use that method to cleanup a cell and prepare it for the new layout.

Swift CollectionView Deselection not working

I am using a custom UICollectionViewFlowLayout class to achieve multi-scroll behaviour of scroll view. No problem in that.
But to make custom selection and deselection, I need to use custom code inside shouldSelectItemAt function for proper selection/deselection.
Here is the code for it:
Inside MyCustomCollectionViewController:
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if(collectionView == customContentCollectionView){
let cell:MyContentCell = collectionView.cellForItem(at: indexPath)! as! MyCollectionViewController.MyContentCell
if(self.bubbleArray[indexPath.section][indexPath.item].umid != ""){
// DESELECTION LOGIC
if(previouslySelectedIndex != nil){
let (bubbleBorder, bubbleFill) = getBubbleColor(selected: false)
// following line is error prone (executes but may or may not fetch the cell, sometimes deselect sometimes doesn't)
let prevCell = try collectionView.cellForItem(at: previouslySelectedIndex) as? MyCollectionViewController.MyContentCell
prevCell?.shapeLayer.strokeColor = bubbleBorder.cgColor
prevCell?.shapeLayer.fillColor = bubbleFill.cgColor
prevCell?.shapeLayer.shadowOpacity = 0.0
prevCell?.labelCount.textColor = bubbleBorder
}
previouslySelectedIndex = []
previouslySelectedIndex = indexPath
// SELECTION LOGIC
if(self.bubbleArray[indexPath.section][indexPath.item].interactions != ""){
let (bubbleBorder, bubbleFill) = getBubbleColor(selected: true)
cell.shapeLayer.strokeColor = bubbleBorder.cgColor
cell.shapeLayer.fillColor = bubbleFill.cgColor
cell.shapeLayer.shadowOffset = CGSize(width: 0, height: 2)
cell.shapeLayer.shadowOpacity = 8
cell.shapeLayer.shadowRadius = 2.0
cell.labelCount.textColor = UIColor.white
}
}
return false
}
return true
}
The code for MyContentCell:
class MyContentCell: UICollectionViewCell{ // CODE TO CREATE CUSTOM CELLS
var gradient = CAGradientLayer()
var shapeLayer = CAShapeLayer()
var horizontalLine = UIView()
var verticalLeftLine = UIView()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
let labelCount: UILabel = {
let myLabel = UILabel()
return myLabel
}()
func setup(){ // initializing cell components here as well as later in cellForItemAt method
............
}
override func prepareForReuse() {
self.shapeLayer.fillColor = UIColor.white.cgColor
self.shapeLayer.shadowOpacity = 0.0
}
}
May be the problem is, when a cell is selected and just dragged out of screen, its perhaps might not being destroyed, that's why the prepare for reuse function is not tracking it to make the deselection. So kindly tell me how to deselect a cell which is just gone outside the screen (visible index)?
You need to invalidate layout
collectionView?.collectionViewLayout.invalidateLayout()

How do I update text input position of the y depending on keyboard height in swift

I'm trying to implement the comment view controller.
I want using the auto layout via the storyboard.
My question is...
When I tap the input text..then keyboard will move up...but input text is not move up..
Keyboard is overlapping the text input..
Here is TableViewController.swift
import UIKit
import Parse
var commentUUID = [String]()
var commentOwner = [String]()
class CommentViewController: UIViewController, UITextViewDelegate, UITableViewDelegate, UITableViewDataSource{
//UI Objects
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var commentTextView: UITextView!
#IBOutlet weak var sendButton: UIButton!
var refresher = UIRefreshControl()
//values for reseting UI to default
var tableViewHeight : CGFloat = 0
var commentY : CGFloat = 0
var commentHeight : CGFloat = 0
//arryas to hold server data
var usernameArray = [String]()
var profileArray = [PFFile]()
var commentArray = [String]()
var dateArray = [NSDate?]()
//variable to hold keyboard frame
var keyboard = CGRect()
//page size
var page : Int32 = 15
override func viewDidLoad() {
super.viewDidLoad()
tableView.backgroundColor = .redColor()
//title at the top
self.navigationItem.title = "COMMENTS"
self.navigationItem.hidesBackButton = true
let backButton = UIBarButtonItem(title: "back", style: .Plain, target: self, action: #selector(CommentViewController.back(_:)))
self.navigationItem.leftBarButtonItem=backButton
//swipe to go back
let backSwipe = UISwipeGestureRecognizer(target: self, action: #selector(CommentViewController.back(_:)))
backSwipe.direction=UISwipeGestureRecognizerDirection.Right
self.view.addGestureRecognizer(backSwipe)
// catch notification if the keyboard is shown or hidden
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(CommentViewController.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(CommentViewController.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
//disable button from the beginning
sendButton.enabled = false
//call function
alignment()
loadComments()
}
//I think it is not affect on the layout...
func configureTableView() {
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 160.0
}
// preload func
override func viewWillAppear(animated: Bool) {
//hide bottom bar
self.tabBarController?.tabBar.hidden = true
}
// postload func
override func viewWillDisappear(animated: Bool) {
self.tabBarController?.tabBar.hidden = false
}
//func loading when keyboard is shown
func keyboardWillShow(notification : NSNotification){
//define keyboard frame size
keyboard = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey]!.CGRectValue)!
//move UI up
UIView.animateWithDuration(0.4){ () -> Void in
self.tableView.frame.size.height = self.tableViewHeight - self.keyboard.height - self.commentTextView.frame.size.height + self.commentHeight
print("keyboard show")
self.commentTextView.frame.origin.y = self.commentY - self.keyboard.height - self.commentTextView.frame.size.height + self.commentHeight
self.sendButton.frame.origin.y = self.commentTextView.frame.origin.y
self.commentTextView.frame.origin.y = 250
}
}
//func loading when keyboard is hidden
func keyboardWillHide(notification : NSNotification){
//move UI down
UIView.animateWithDuration(0.4){() -> Void in
self.tableView.frame.size.height = self.tableViewHeight
self.commentTextView.frame.origin.y = self.commentY
self.sendButton.frame.origin.y = self.commentY
}
}
//alignment function
func alignment(){
let width = self.view.frame.size.width
let height = self.view.frame.size.height
tableView.frame = CGRectMake(0, 0, width, height - self.navigationController!.navigationBar.frame.size.height)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 160.0
commentTextView.layer.cornerRadius = commentTextView.frame.size.width / 50
//delegates
commentTextView.delegate = self
tableView.delegate = self
tableView.dataSource = self
//assign reseting values
tableViewHeight = tableView.frame.size.height
commentHeight = commentTextView.frame.size.height
commentY = commentTextView.frame.origin.y
}
//while writing something
func textViewDidChange(textView: UITextView) {
//disable button if entered no text
let spacing = NSCharacterSet.whitespaceAndNewlineCharacterSet()
//It shown when user entered type
if !commentTextView.text.stringByTrimmingCharactersInSet(spacing).isEmpty{
sendButton.enabled = true
}else {
sendButton.enabled = false
}
// + paragraph
if textView.contentSize.height > textView.frame.size.height && textView.frame.height < 130{
//find difference to add
let difference = textView.contentSize.height - textView.frame.size.height
//redefine frame of commentText
textView.frame.origin.y = textView.frame.origin.y - difference
textView.frame.size.height = textView.contentSize.height
//move up tableView
if textView.contentSize.height + keyboard.height + commentY >= tableView.frame.size.height {
tableView.frame.size.height = tableView.frame.size.height - difference
}
}
// - parapraph
else if textView.contentSize.height < textView.frame.size.height {
//find difference to deduct
let difference = textView.frame.size.height - textView.contentSize.height
//redefine frame of commentText
textView.frame.origin.y = textView.frame.origin.y + difference
textView.frame.size.height = textView.contentSize.height
//move down tableview
if textView.contentSize.height + keyboard.height + commentY > tableView.frame.size.height {
tableView.frame.size.height = tableView.frame.size.height + difference
}
}
}
//load comments function
func loadComments(){
//STEP 1. Count total comments in order to skip all except page size
let countQuery = PFQuery(className: "comments")
countQuery.whereKey("to", equalTo: commentUUID.last!)
countQuery.countObjectsInBackgroundWithBlock({(count:Int32, error:NSError?) -> Void in
//if comments on the server for current post are more than (page size 15) implement pull to refresh func
if self.page < count {
self.refresher.addTarget(self, action: #selector(CommentViewController.loadMore), forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(self.refresher)
}
//STEP 2. Request last (page size 15) comments
let query = PFQuery(className: "comments")
query.whereKey("to", equalTo: commentUUID.last!)
query.skip = count - self.page
query.addAscendingOrder("createdAt")
query.findObjectsInBackgroundWithBlock({(objects:[PFObject]?, error:NSError?) -> Void in
if error == nil {
//clean up
self.usernameArray.removeAll(keepCapacity: false)
self.profileArray.removeAll(keepCapacity: false)
self.commentArray.removeAll(keepCapacity: false)
self.dateArray.removeAll(keepCapacity: false)
//find related object
for object in objects!{
self.usernameArray.append(object.objectForKey("username") as! String)
self.profileArray.append(object.objectForKey("profileImg") as! PFFile)
self.commentArray.append(object.objectForKey("comment") as! String)
self.dateArray.append(object.createdAt)
self.tableView.reloadData()
//scroll to bottom
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: self.commentArray.count - 1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Bottom, animated: false)
}
}else {
print(error?.localizedDescription)
}
})
})
}
//Pagenation
func loadMore(){
//STEP 1. Count total comments in order to skip all except page size
let countQuery = PFQuery(className: "comments")
countQuery.whereKey("to", equalTo: commentUUID.last!)
countQuery.countObjectsInBackgroundWithBlock({(count:Int32, error:NSError?) -> Void in
//self refresher
self.refresher.endRefreshing()
//remove refresher if loaded all comments
if self.page >= count {
self.refresher.removeFromSuperview()
}
//STEP2. Load more comments
if self.page < count {
//increase page to laod 30 as first paging
self.page = self.page + 15
//request existing comments from the server
let query = PFQuery(className: "comments")
query.whereKey("to", equalTo: commentUUID.last!)
query.skip = count - self.page
query.addAscendingOrder("createdAt")
query.findObjectsInBackgroundWithBlock({(objects:[PFObject]?, error:NSError?) -> Void in
if error==nil{
//clean up
self.usernameArray.removeAll(keepCapacity: false)
self.profileArray.removeAll(keepCapacity: false)
self.commentArray.removeAll(keepCapacity: false)
self.dateArray.removeAll(keepCapacity: false)
//find related objects
for object in objects! {
self.usernameArray.append(object.objectForKey("username") as! String)
self.profileArray.append(object.objectForKey("profileImg") as! PFFile)
self.commentArray.append(object.objectForKey("comments") as! String)
self.dateArray.append(object.createdAt)
self.tableView.reloadData()
}
}else {
print(error?.localizedDescription)
}
})
}
})
}
//Send Button Tapped
#IBAction func sendButtonTapped(sender: AnyObject) {
print("send tapped")
//STEP1. Add row in tableView
usernameArray.append(PFUser.currentUser()!.username!)
profileArray.append(PFUser.currentUser()?.objectForKey("profileImg") as! PFFile)
dateArray.append(NSDate())
commentArray.append(commentTextView.text.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()))
tableView.reloadData()
//STEP2. Send comment to server
let commentObj = PFObject(className: "comments")
commentObj["to"] = commentUUID.last
commentObj["username"] = PFUser.currentUser()?.username
commentObj["profileImg"] = PFUser.currentUser()?.valueForKey("profileImg")
commentObj["comment"] = commentTextView.text.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
commentObj.saveEventually()
//Scroll to bottom
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forItem: commentArray.count - 1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Bottom, animated: false)
//STEP 3. Reset UI
sendButton.enabled = false
commentTextView.text = ""
commentTextView.frame.size.height = commentHeight
commentTextView.frame.origin.y = sendButton.frame.origin.y
tableView.frame.size.height = self.tableViewHeight - self.keyboard.height - self.commentTextView.frame.size.height + self.commentHeight
}
//tableview
//cell numb
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return commentArray.count
}
//cell height
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
//Cell config
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//declaire cell
let cell = tableView.dequeueReusableCellWithIdentifier("commentCell") as! CommentTableViewCell
cell.usernameButton.setTitle(usernameArray[indexPath.row], forState: .Normal)
cell.usernameButton.sizeToFit()
cell.commentLabel.text = commentArray[indexPath.row]
profileArray[indexPath.row].getDataInBackgroundWithBlock({(data:NSData?, error:NSError?) -> Void in
cell.profileImagevView.image = UIImage(data: data!)
})
//calculate date
let from = dateArray[indexPath.row]
let now = NSDate()
let components : NSCalendarUnit = [.Second, .Minute, .Hour, .Day, .WeekOfMonth]
let difference = NSCalendar.currentCalendar().components(components, fromDate: from!, toDate: now, options: [])
if difference.second <= 0 {
cell.dateLabel.text = "now"
}
if difference.second > 0 && difference.minute == 0 {
cell.dateLabel.text = "\(difference.second)s"
}
if difference.minute > 0 && difference.hour == 0 {
cell.dateLabel.text = "\(difference.minute)m"
}
if difference.hour > 0 && difference.day == 0 {
cell.dateLabel.text = "\(difference.hour)h"
}
if difference.day > 0 && difference.weekOfMonth == 0 {
cell.dateLabel.text = "\(difference.day)d."
}
if difference.weekOfMonth > 0 {
cell.dateLabel.text = "\(difference.weekOfMonth)w."
}
cell.usernameButton.layer.setValue(indexPath, forKey: "index")
return cell
}
//clicked username button
#IBAction func usernameButtonTapped(sender: AnyObject) {
//call index of current button
let i = sender.layer.valueForKey("index") as! NSIndexPath
//Call cell to call further cell data
let cell = tableView.cellForRowAtIndexPath(i) as! CommentTableViewCell
//if user tapped on his username go home, else go guest
if cell.usernameButton.titleLabel?.text == PFUser.currentUser()?.username {
let home = self.storyboard?.instantiateViewControllerWithIdentifier("HomeViewController") as! HomeViewController
self.navigationController?.pushViewController(home, animated: true)
}else {
guestname.append(cell.usernameButton.titleLabel!.text!)
let guest = self.storyboard?.instantiateViewControllerWithIdentifier("GuestHomeViewController") as! GuestHomeViewController
self.navigationController?.pushViewController(guest, animated: true)
}
}
//go back
func back(sender : UIBarButtonItem){
//push back
self.navigationController?.popViewControllerAnimated(true)
//clean comment uuid from holding information
if !commentUUID.isEmpty{
commentUUID.removeLast()
}
//clean comment owner from last holding information
if !commentOwner.isEmpty{
commentOwner.removeLast()
}
}
}
And It is my cell controller
import UIKit
class CommentTableViewCell: UITableViewCell {
#IBOutlet weak var profileImagevView: UIImageView!
#IBOutlet weak var usernameButton: UIButton!
#IBOutlet weak var commentLabel: UILabel!
#IBOutlet weak var dateLabel: UILabel!
//default func
override func awakeFromNib() {
super.awakeFromNib()
//round profile
profileImagevView.layer.cornerRadius = profileImagevView.frame.size.width/2
profileImagevView.clipsToBounds = true
}
}
Two comments:
Use UIKeyboardDidShowNotification instead of UIKeyboardWillShowNotification
Do not modify frame directly when you're using auto layout. Just link your bottom layout constraint to the controller and change a constant value when needed.
Try this:
self.tableView.frame.size.height = self.view.frame.height - keyboard.height - self.tableView.frame.size.height
print("keyboard show")
self.commentTextView.frame = CGRect(self.commentTextView.frame.minX, self.commentTextView.frame.minY - keyboard.height, self.commentTextView.frame.width, self.commentTextView.frame.height)
self.sendButton.frame.origin.y = self.commentTextView.frame.origin.y
Edit
#Arsen's Answer makes much more sense and probably much easier, by the way :P But this should work the same.

Detecting closest collectionviewcell to the center of the collectionview

Suspect I am doing something fundamentally wrong below... I have a horizontal collectionview and after dragging I want to snap the closest cell to the center. But my results are unpredictable... what am I doing wrong here?
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell] {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
Turns out my original code was missing accounting for the collecitonview content offset. In addition, I've moved the centering into the scrollViewDidEndDecelerating callback. I've modified the original code and included it below.
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell] {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
Try this:
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:self.collectionView.center];
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
SWIFT:
let indexPath = self.collectionView.indexPathForItemAtPoint(self.collectionView.center)
self.collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
None of the solutions reliably worked for me, so I wrote this and it works 100% of the time. Very simple. Even the built in indexPathForItemAtPoint was not working 100%.
extension UICollectionView {
var centerMostCell:UICollectionViewCell? {
guard let superview = superview else { return nil }
let centerInWindow = superview.convert(center, to: nil)
guard visibleCells.count > 0 else { return nil }
var closestCell:UICollectionViewCell?
for cell in visibleCells {
guard let sv = cell.superview else { continue }
let cellFrameInWindow = sv.convert(cell.frame, to: nil)
if cellFrameInWindow.contains(centerInWindow) {
closestCell = cell
break
}
}
return closestCell
}
}
Updated version for Swift 4
var closestCell = collectionView.visibleCells[0]
for cell in collectionView.visibleCells {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPath(for: closestCell)
collectionView.scrollToItem(at: indexPath!, at: .centeredHorizontally, animated: true)
When stop dragging, just pick the center and use it to choose the indexpath
swift 4.2
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let indexPath = photoCollectionView.indexPathForItem(at: photoCollectionView.center)
photoCollectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: .centeredHorizontally, animated: true)
}
var centerUICollectionViewCell: UICollectionViewCell? {
get {
// 1
guard let center = collectionView.superview?.convert(collectionView.center, to: collectionView) else { return nil }
// 2
guard let centerIndexPath = collectionView.indexPathForItem(at: center) else { return nil }
// 3
return collectionView.cellForItem(at: centerIndexPath)
}
}
1: Need to make sure we are converting the point from superview coordinate space to collectionView's space
2: Use the collectionView's local space point to find the indexPath
3: Return the cell for indexPath
public func closestIndexPathToCenter() -> IndexPath? {
guard let cv = collectionView else { return nil }
let visibleCenterPositionOfScrollView = Float(cv.contentOffset.x + (cv.bounds.size.width / 2))
var closestCellIndex = -1
var closestDistance: Float = .greatestFiniteMagnitude
for i in 0..<cv.visibleCells.count {
let cell = cv.visibleCells[i]
let cellWidth = cell.bounds.size.width
let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)
// Now calculate closest cell
let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
if distance < closestDistance {
closestDistance = distance
closestCellIndex = cv.indexPath(for: cell)!.row
}
}
if closestCellIndex != -1 {
return IndexPath(row: closestCellIndex, section: 0)
}else {
return nil
}
}

Warning: UICollectionViewFlowLayout has cached frame mismatch for index path 'abc'

This is the code causing the warning:
private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let distance = CGRectGetMidX(attributes!.frame) - self.midX;
var transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, -distance, 0, -self.width);
attributes!.transform3D = CATransform3DIdentity;
return attributes
}
The console also prints:
This is likely occurring because the flow layout "xyz" is modifying attributes returned by UICollectionViewFlowLayout without copying them.
How do I fix this warning?
This is likely occurring because the flow layout "xyz" is modifying attributes returned by UICollectionViewFlowLayout without copying them
And sure enough, that's just what you are doing:
private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let distance = CGRectGetMidX(attributes!.frame) - self.midX;
var transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, -distance, 0, -self.width);
attributes!.transform3D = CATransform3DIdentity;
return attributes
}
I expect that if you simply say:
let attributes =
super.layoutAttributesForItemAtIndexPath(indexPath).copy()
as! UICollectionViewLayoutAttributes
or similar, the problem will go away.
In addition to the great answer above.
I know the example code is written in swift, but I thought that it can be helpful to have the Objective-C version.
For Objective-C, this won't work, because the copy function does only a shallow copy. You will have to do this:
NSArray * original = [super layoutAttributesForElementsInRect:rect];
NSArray * attributes = [[NSArray alloc] initWithArray:original copyItems:YES];
I have added a temp variable for readability.
I had this issue when overriding layoutAttributesForElementsInRect. Iterating through each element in the super.layoutAttributesForElementsInRect(rect) array and calling copy wasn't working for me, so I ended up falling back on Foundation classes and using NSArray's copyItems:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// unwrap super's attributes
guard let superArray = super.layoutAttributesForElementsInRect(rect) else { return nil }
// copy items
guard let attributes = NSArray(array: superArray, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil }
// modify attributes
return attributes
}
It's not the answer to original question, but can help for layoutAttributesForElements(in rect: CGRect) (Swift 3.0):
let safeAttributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
safeAttributes?.forEach { /* do something with attributes*/ }
Adding to #Georgi answer
<NSCopying> must be conformed and add copy message call to layoutAttributesForItemAtIndexPath
UICollectionViewLayoutAttributes* attributes = [[super layoutAttributesForItemAtIndexPath:indexPath] copy];
Updated response for Swift 3!
for func layoutAttributesForElements
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
guard let attributesToReturn = attributes.map( { $0.copy() }) as? [UICollectionViewLayoutAttributes] else {
return nil
}
return attributesToReturn
}
for func layoutAttributesForItem
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
return nil
}
return currentItemAttributes
}
If you override both function, you have to call copy on both function!
Good coding!
I have subclassed UICollectionViewFlowLayout. Inside layoutAttributesForElementsInRect() I did this change:
change from
guard let attributesForItem: UICollectionViewLayoutAttributes = self.layoutAttributesForItemAtIndexPath(indexPath) else {
return
}
change to
guard let attributesForItem = self.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
return
}

Resources