My UICollectionView does not scroll smoothly using Swift - swift

I have a CollectionView which dequeues a cell depending on the message type (eg; text, image).
The problem I am having is that when I scroll up/down the scroll is really choppy and thus not a very good user experience. This only happens the first time the cells are loaded, after that the scrolling is smooth.
Any ideas how I can fix this?, could this be an issue with the time its taking to fetch data before the cell is displayed?
I am not too familiar with running tasks on background threads etc. and not sure what changes I can make to prefect the data pre/fetching etc.. please help!
The Gif shows scroll up when the view loads, it shows the cells/view being choppy as I attempt to scroll up.
This is my func loadConversation() which loads the messages array
func loadConversation(){
DataService.run.observeUsersMessagesFor(forUserId: chatPartnerId!) { (chatLog) in
self.messages = chatLog
DispatchQueue.main.async {
self.collectionView.reloadData()
if self.messages.count > 0 {
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .bottom , animated: false)
}
}
}//observeUsersMessagesFor
}//end func
This is my cellForItemAt which dequeues cells
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let message = messages[indexPath.item]
let uid = Auth.auth().currentUser?.uid
if message.fromId == uid {
if message.imageUrl != nil {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellImage", for: indexPath) as! ConversationCellImage
cell.configureCell(message: message)
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellSender", for: indexPath) as! ConversationCellSender
cell.configureCell(message: message)
return cell
}//end if message.imageUrl != nil
} else {
if message.imageUrl != nil {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellImageSender", for: indexPath) as! ConversationCellImageSender
cell.configureCell(message: message)
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCell", for: indexPath) as! ConversationCell
cell.configureCell(message: message)
return cell
}
}//end if uid
}//end func
This is my ConversationCell class which configures a custom cell for dequeueing by cellForItemAt (note: in addition there another ConversationCellImage custom cell class which configures an image message):
class ConversationCell: UICollectionViewCell {
#IBOutlet weak var chatPartnerProfileImg: CircleImage!
#IBOutlet weak var messageLbl: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
chatPartnerProfileImg.isHidden = false
}//end func
func configureCell(message: Message){
messageLbl.text = message.message
let partnerId = message.chatPartnerId()
DataService.run.getUserInfo(forUserId: partnerId!) { (user) in
let url = URL(string: user.profilePictureURL)
self.chatPartnerProfileImg.sd_setImage(with: url, placeholderImage: #imageLiteral(resourceName: "placeholder"), options: [.continueInBackground, .progressiveDownload], completed: nil)
}//end getUserInfo
}//end func
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = 10.0
self.layer.shadowRadius = 5.0
self.layer.shadowOpacity = 0.3
self.layer.shadowOffset = CGSize(width: 5.0, height: 10.0)
self.clipsToBounds = false
}//end func
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
//toggles auto-layout
setNeedsLayout()
layoutIfNeeded()
//Tries to fit contentView to the target size in layoutAttributes
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
//Update layoutAttributes with height that was just calculated
var frame = layoutAttributes.frame
frame.size.height = ceil(size.height) + 18
layoutAttributes.frame = frame
return layoutAttributes
}
}//end class
Time Profile results:
Edit: Flowlayout code
if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout,
let collectionView = collectionView {
let w = collectionView.frame.width - 40
flowLayout.estimatedItemSize = CGSize(width: w, height: 200)
}// end if-let
Edit: preferredLayoutAttributesFitting function in my custom cell class
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
//toggles auto-layout
setNeedsLayout()
layoutIfNeeded()
//Tries to fit contentView to the target size in layoutAttributes
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
//Update layoutAttributes with height that was just calculated
var frame = layoutAttributes.frame
frame.size.height = ceil(size.height) + 18
layoutAttributes.frame = frame
return layoutAttributes
}
SOLUTION:
extension ConversationVC: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var height: CGFloat = 80
let message = messages[indexPath.item]
if let text = message.message {
height = estimateFrameForText(text).height + 20
} else if let imageWidth = message.imageWidth?.floatValue, let imageHeight = message.imageHeight?.floatValue{
height = CGFloat(imageHeight / imageWidth * 200)
}
let width = collectionView.frame.width - 40
return CGSize(width: width, height: height)
}
fileprivate func estimateFrameForText(_ text: String) -> CGRect {
let size = CGSize(width: 200, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16)], context: nil)
}
}//end extension

First thing first, let's try finding the exact location where this problem is arising.
Try 1:
comment this line
//self.chatPartnerProfileImg.sd_setImage(with: url, placeholderImage: #imageLiteral(resourceName: "placeholder"), options: [.continueInBackground, .progressiveDownload], completed: nil)
And run your app to see the results.
Try 2:
Put that line in async block to see the results.
DispatchQueue.main.async {
self.chatPartnerProfileImg.sd_setImage(with: url, placeholderImage: #imageLiteral(resourceName: "placeholder"), options: [.continueInBackground, .progressiveDownload], completed: nil)
}
Try 3: Comment the code for setting corner radius
/*self.layer.cornerRadius = 10.0
self.layer.shadowRadius = 5.0
self.layer.shadowOpacity = 0.3
self.layer.shadowOffset = CGSize(width: 5.0, height: 10.0)
self.clipsToBounds = false*/
Share your results for Try 1, 2 and 3 and then we can get the better idea about where the problem lies.
Hope this way we can get the reason behind flickering.

Always make sure that the data like image or GIF shouldn't download on the main thread.
This is the reason why your scrolling is not smooth. Download the data in separate thread in background use either GCD or NSOperation queue. Then show the downloaded image always on main thread.
use AlamofireImage pods, it will automatically handle the downloaded task in background.
import AlamofireImage
extension UIImageView {
func downloadImage(imageURL: String?, placeholderImage: UIImage? = nil) {
if let imageurl = imageURL {
self.af_setImage(withURL: NSURL(string: imageurl)! as URL, placeholderImage: placeholderImage) { (imageResult) in
if let img = imageResult.result.value {
self.image = img.resizeImageWith(newSize: self.frame.size)
self.contentMode = .scaleAspectFill
}
}
} else {
self.image = placeholderImage
}
}
}

I think the choppiness you are seeing is because the cells are given a size and then override the size they were given which causes the layout to shift around. What you need to do is do the calculation during the creation of the layout first time through.
I have a function that I have used like this...
func height(forWidth width: CGFloat) -> CGFloat {
// do the height calculation here
}
This is then used by the layout to determine the correct size first time without having to change it.
You could have this as a static method on the cell or on the data... or something.
What it needs to do is create a cell (not dequeue, just create a single cell) then populate the data in it. Then do the resizing stuff. Then use that height in the layout when doing the first layout pass.
Yours is choppy because the collection lays out the cells with a height of 20 (for example) and then calculates where everything needs to be based on that... then you go... "actually, this should be 38" and the collection has to move everything around now that you've given it a different height. This then happens for every cell and so causes the choppiness.
I might be able to help a bit more if I could see your layout code.
EDIT
Instead of using that preferredAttributes method you should implement the delegate method func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize.
Do something like this...
This method goes into the view controller.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let message = messages[indexPath.item]
let height: CGFloat
if let url = message.imageURL {
height = // whatever height you want for the images
} else {
height = // whatever height you want for the text
}
return CGSize(width: collectionView.frame.width - 40, height: height)
}
You may have to add to this but it will give you an idea.
Once you've done this... remove all your code from the cell to do with changing the frames and attributes etc.
Also... put your shadow code into the awakeFromNib method.

Related

UICollectionView numberOfItemsInSection not being called on reloadData() - Swift Xcode

I have two view controllers – SecondTestViewController and SecondContentViewController. SecondContentViewController is a floating panel inside SecondTestViewController. SecondTestViewController has a UITableView and SecondContentViewController has a UICollectionView.
SecondContentViewController:
override func viewDidLoad() {
super.viewDidLoad()
// let layout = UICollectionViewFlowLayout()
// layout.scrollDirection = .vertical
myCollectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout())
guard let myCollectionView = myCollectionView else {
return
}
myCollectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
myCollectionView.delegate = self
myCollectionView.dataSource = self
myCollectionView.backgroundColor = .white
myCollectionView.translatesAutoresizingMaskIntoConstraints = false
myCollectionView.allowsSelection = true
myCollectionView.isSpringLoaded = true
view.backgroundColor = .white
view.addSubview(myCollectionView)
NSLayoutConstraint.activate([
myCollectionView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
myCollectionView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
myCollectionView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
myCollectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
private func collectionViewLayout() -> UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
let cellWidthHeightConstant: CGFloat = UIScreen.main.bounds.width * 0.2
layout.sectionInset = UIEdgeInsets(top: 50,
left: 10,
bottom: 0,
right: 10)
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 0
layout.itemSize = CGSize(width: cellWidthHeightConstant, height: cellWidthHeightConstant)
return layout
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// return contentData.count
print("numberOfItemsInSection activated")
print("numberOfItemsInSection = \(reviewDataController?.tableViewReviewData.count ?? 100)")
return reviewDataController?.tableViewReviewData.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("cellForItemAt activated")
let cell = myCollectionView?.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
// cell.data = self.contentData[indexPath.item]
cell.data = self.reviewDataController.tableViewReviewData[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width / 5, height: collectionView.frame.height / 10)
}
func updateTheCollectionView() {
print("updateCollectionView activated")
print("reviewData count = \(reviewDataController.tableViewReviewData.count)")
myCollectionView?.reloadData()
}
When a cell in SecondTestViewController's tableView is tapped, it takes a photo, passes the data, and then calls SecondContentViewController's updateTheCollectionView() function.
This function is called in SecondTestViewController after a photo has been taken and various other tasks completed (like updating the variable, etc.).
func passDataToContentVC() {
let contentVC = SecondContentViewController()
contentVC.reviewDataController = self.reviewDataController
contentVC.updateTheCollectionView()
}
As you can see, reloadData is supposed to be called inside this function. It should now update the view with the new photo. For some reason, it's like reloadData is never called because neither numberOfItemsInSection nor cellForItemsAt are being activated a second time.
What is going wrong? The print statements show that the data is being passed (they show an updated quantity after a photo has been taken). The view simply is not updating with the new data because the two collectionView functions are not being called.
Edit:
Here is the floating panel code.
fpc = FloatingPanelController()
fpc.delegate = self
guard let contentVC = storyboard?.instantiateViewController(identifier: "fpc_secondContent") as? SecondContentViewController else {
return
}
fpc.isRemovalInteractionEnabled = false
fpc.set(contentViewController: contentVC)
fpc.addPanel(toParent: self)
It's essentially inside of viewDidLoad for the SecondTestViewController.
For more information on how it works: Visit this github
As far as the didSelectRowAt function goes, it's too much code to show here. reviewDataController is dependency injection made up of an array of a struct. Essentially all that happens is when a cell is tapped, it pulls up the camera and allows the user to take a photo, then it stores the photo in the array inside of reviewDataController along with some other properties in the struct that the array is made of. The important part is that it's supposed to pass this data to the floating panel which then updates the collectionView showing the photo that was just taken.
Because you are passing data from SecondTestViewController to SecondContentViewController, which is forwards and not backwards, you don't need delegates or anything.
Instead, all you need to do is retain a reference to SecondContentViewController. Try something like this:
class SecondTestViewController {
var secondContentReference: SecondContentViewController? /// keep a reference here
override func viewDidLoad() {
super.viewDidLoad()
let fpc = FloatingPanelController()
fpc.delegate = self
guard let contentVC = storyboard?.instantiateViewController(identifier: "fpc_secondContent") as? SecondContentViewController else {
return
}
fpc.isRemovalInteractionEnabled = false
fpc.set(contentViewController: contentVC)
fpc.addPanel(toParent: self)
self.secondContentReference = contentVC /// assign contentVC to the reference
}
}
secondContentReference keeps a reference to your SecondContentViewController. Now, inside passDataToContentVC, use that instead of making a new instance with let contentVC = SecondContentViewController().
Replace
func passDataToContentVC() {
let contentVC = SecondContentViewController()
contentVC.reviewDataController = self.reviewDataController
contentVC.updateTheCollectionView()
}
... with
func passDataToContentVC() {
secondContentReference?.reviewDataController = self.reviewDataController
secondContentReference?.updateTheCollectionView()
}
It seems that
let contentVC = SecondContentViewController() // this is a new instance
contentVC.reviewDataController = self.reviewDataController
contentVC.updateTheCollectionView()
You create an instance and add it to SecondTestViewController but passDataToContentVC has another new one , hence no updates for the shown one

UIcollectionView not updating the size and layout of UIcollectionCell upon phone rotation

I have a UIcollectionView with images. When I rotate the screen, the cells do not resize as expected. I have tried adding collectionView.collectionViewLayout.invalidateLayout() to the viewDidLayoutSubviews() - but this only causes a crash. Please can someone advise?
I have a UIcollectionView with images. When I rotate the screen, the cells do not resize as expected. I have tried adding collectionView.collectionViewLayout.invalidateLayout() to the viewDidLayoutSubviews() - but this only causes a crash. Please can someone advise?
Portrait:
Landdscape:
class GridPicksCollectionViewController: UICollectionViewController{
let cellId = "gridyPickCell"
var numCelPerRow: CGFloat = 3
var layout: UICollectionViewFlowLayout!
let borderInset: CGFloat = 3
let interCellSpacing: CGFloat = 3
let spacingBetweenRows: CGFloat = 3
var dataSource = [UIImage]()
var collectionViewWidth: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
generalSetup()
setUpDateSource()
setupCollectionViewLayout()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("viewDidLayoutSubviews")
// Update collectionViewWidth upon rotation
collectionViewWidth = self.collectionView.frame.width
//collectionView.collectionViewLayout.invalidateLayout() - causes app to crash
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
print("viewWillTransition")
}
private func generalSetup(){
navigationController?.navigationBar.barTintColor = UIColor.appDarkGreen
navigationItem.title = "Girdy Picks"
let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
navigationController?.navigationBar.titleTextAttributes = textAttributes
collectionView.backgroundColor = UIColor.white
collectionViewWidth = collectionView.frame.width
// Make sure collectionView is always within the safe area layout
if #available(iOS 11.0, *) {
collectionView?.contentInsetAdjustmentBehavior = .always
}
// Register UICollectionViewCell
collectionView.register(GirdyPickCollectionCell.self, forCellWithReuseIdentifier: cellId)
}
private func setUpDateSource(){
let images: [UIImage] = [UIImage.init(named: "buddha")!, UIImage.init(named: "sharpener")!, UIImage.init(named: "cars")!, UIImage.init(named: "houses")!, UIImage.init(named: "houses2")!, UIImage.init(named: "tower1")!, UIImage.init(named: "tower2")!]
dataSource = images
}
private func setupCollectionViewLayout(){
collectionView.delegate = self
collectionView.dataSource = self
layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
layout.minimumInteritemSpacing = interCellSpacing // distance between cells in a row
layout.minimumLineSpacing = spacingBetweenRows // distance in between rows
layout.sectionInset = UIEdgeInsets.init(top: borderInset, left: borderInset, bottom: borderInset, right: borderInset) // border inset for collectionView
}
}
extension GridPicksCollectionViewController: UICollectionViewDelegateFlowLayout{
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! GirdyPickCollectionCell
cell.imageView.image = dataSource[indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedImage = dataSource[indexPath.row]
let showImageVC = ImageGridViewController()
showImageVC.modalPresentationStyle = .fullScreen
showImageVC.imageToDisplay = selectedImage
self.present(showImageVC, animated: true) {}
}
// Determine size for UICollectionCell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
print("sizeForItemAt")
guard let collectionWidth = collectionViewWidth else{ return CGSize.init()}
let widthCell = (collectionWidth - interCellSpacing * 2 - borderInset * 2) / numCelPerRow
return CGSize.init(width: widthCell, height: widthCell)
}
}
You are wrong about your lifecycle of events. When the device is rotated, your view hierarchy becomes notified of this event, and views start to re-layout themselves based on the information they have. After this has been finished, viewDidLayoutSubviews() will be called, not before.
Since you are updating the collectionViewWidth property in this method, the layout still uses the old value when calling collectionView(_:layout:sizeForItemAt:). You need to set this value in viewWillLayoutSubviews().
Alternatively, you can write a setter for this property which will call invalidateLayout() on the collection view's layout, but this will cause an unnecessary layout pass.

CollectionView cells Confused

I am busy with a speech app with a soundboard when you tap on a CollectionView Cell then iPhone will speak the text. I have a little bug in my app and I don't know what the reason is.
I have built a CollectionView with images as backgroundViews. It works but when I go to an other view ,for example to the Paint view, and I will go back then the cells will confuse.
Can anybody tell me what goes wrong and how I solve it?
Thanks!
This is my code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
loadData()
let itemSize = UIScreen.main.bounds.width/2 - 5
let itemHeight = itemSize / 2
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets.init(top: 3, left: 3, bottom: 3, right: 3)
layout.itemSize = CGSize(width: itemSize, height: itemHeight)
layout.minimumInteritemSpacing = 3
layout.minimumLineSpacing = 3
soundboard.collectionViewLayout = layout
navigationItem.rightBarButtonItem = editButtonItem
if(traitCollection.forceTouchCapability == .available){
registerForPreviewing(with: self as UIViewControllerPreviewingDelegate, sourceView: collectionView)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
loadData()
}
func loadData() {
let soundRequest:NSFetchRequest<Soundboard> = Soundboard.fetchRequest()
do {
soundBoardData = try managedObjectContext.fetch(soundRequest)
self.soundboard.reloadData()
} catch {
print("Error")
}
}
// MARK: - Collection View
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return soundBoardData.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> soundboardCellVC {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! soundboardCellVC
let soundItem = soundBoardData[indexPath.row]
if let getCellPhoto = soundItem.photo as Data? {
cell.title.text = "";
let cellPhoto = UIImage(data: getCellPhoto)
let cellPhotoFrame = UIImageView(image:cellPhoto)
cellPhotoFrame.frame = CGRect(x: 0, y: 0, width: cell.frame.width, height: cell.frame.height)
cellPhotoFrame.contentMode = UIView.ContentMode.scaleAspectFill
cell.backgroundView = UIView()
cell.backgroundView!.addSubview(cellPhotoFrame)
}
else
{
cell.title.text = soundItem.title;
cell.backgroundColor = UIColor(red: CGFloat(soundItem.colorRed), green: CGFloat(soundItem.colorGreen), blue: CGFloat(soundItem.colorBlue), alpha: 1)
let fontAwesomeIcon = FAType(rawValue: Helper.FANames.index(of: soundItem.icon!)!)
cell.icon.setFAIconWithName(icon: fontAwesomeIcon!, textColor: UIColor.white)
}
cell.layer.cornerRadius = 10
cell.layer.masksToBounds = true
cell.delegate = self as SoundboardCellDelegate
return cell
}
A good practice when working with cells is to clear any properties that you don't want to persist when the cell gets reused.
https://developer.apple.com/documentation/uikit/uicollectionreusableview/1620141-prepareforreuse
For example in your cells class you can do:
override func prepareForReuse() {
super.prepareForReuse()
self.title.text = nil
}
This keeps your cellForItem method cleaner, and you know for sure that the above actions will have been carried out before you reuse it.
Cells are re-used, which means that you are adding new image views to cells that have existing image views.
You should create a single UIImageView that is in your soundboardCellVC class and simply put the image into that, or set the image to nil as required.
A couple of other style points:
By convention, classes should start with an uppercase letter and it is a cell subclass, not a View Controller subclass, so it should be something like SoundboardCell not soundboardCellVC
Finally, your cellForItemAt signature should match the protocol declaration and be declared as returning a UICollectionViewCell, not your specific cell subclass:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

Collectionview interactive cell swapping

I have implemented a collection view as described here. As you can see, it uses the interactive re-ordering of cells of collectionview provided in iOS 9. But the problem is, I cannot control the reordering of cells.
Say the cells are like this -
1 2 3 4
5 6 7 8
9 10 11 12
I want to swap only cell 6 and 4. So after re-ordering the cells will be
1 2 3 6
5 4 7 8
9 10 11 12
Here, In the tutorial, at the beginning of the program, the collection view is like this -
If I put Starbuck on top of Rose Tyler this happens -
Notice that Sarah Connor has Place of Starbuck.
I want to control the reordering of cells so that here Rose Tyler and Starbuck positions will be swapped.
How do I do that?
The simple solution will be to implement your own interactive cell ordering behaviour by simply moving the cell along the pan in screen and swap when gesture ends on another cell's location (with your own animation and data source update ). Here is a working sample:
class ViewController: UIViewController, UICollectionViewDataSource {
var arr: [String] = (0...100).map { return "\($0)" }
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
cv.register(Cell.self, forCellWithReuseIdentifier: Cell.id)
layout.itemSize = CGSize(width: view.bounds.width/3.5, height: 100)
cv.dataSource = self
cv.addGestureRecognizer(longPressGesture)
return cv
}()
lazy var longPressGesture: UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
private var movingCell: MovingCell?
override func viewDidLoad() {
super.viewDidLoad()
view = collectionView
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
var cell: (UICollectionViewCell?, IndexPath?) {
guard let indexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)),
let cell = collectionView.cellForItem(at: indexPath) else { return (nil, nil) }
return (cell, indexPath)
}
switch(gesture.state) {
case .began:
movingCell = MovingCell(cell: cell.0, originalLocation: cell.0?.center, indexPath: cell.1)
break
case .changed:
/// Make sure moving cell floats above its siblings.
movingCell?.cell.layer.zPosition = 100
movingCell?.cell.center = gesture.location(in: gesture.view!)
break
case .ended:
swapMovingCellWith(cell: cell.0, at: cell.1)
movingCell = nil
default:
movingCell?.reset()
movingCell = nil
}
}
func swapMovingCellWith(cell: UICollectionViewCell?, at indexPath: IndexPath?) {
guard let cell = cell, let moving = movingCell else {
movingCell?.reset()
return
}
// update data source
arr.swapAt(moving.indexPath.row, indexPath!.row)
// swap cells
animate(moving: moving.cell, to: cell)
}
func animate(moving movingCell: UICollectionViewCell, to cell: UICollectionViewCell) {
longPressGesture.isEnabled = false
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.1, initialSpringVelocity: 0.7, options: UIViewAnimationOptions.allowUserInteraction, animations: {
movingCell.center = cell.center
cell.center = movingCell.center
}) { _ in
self.collectionView.reloadData()
self.longPressGesture.isEnabled = true
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return arr.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: Cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.id, for: indexPath) as! Cell
cell.titleLable.text = arr[indexPath.row]
return cell
}
private struct MovingCell {
let cell: UICollectionViewCell
let originalLocation: CGPoint
let indexPath: IndexPath
init?(cell: UICollectionViewCell?, originalLocation: CGPoint?, indexPath: IndexPath?) {
guard cell != nil, originalLocation != nil, indexPath != nil else { return nil }
self.cell = cell!
self.originalLocation = originalLocation!
self.indexPath = indexPath!
}
func reset() {
cell.center = originalLocation
}
}
final class Cell: UICollectionViewCell {
static let id: String = "CellId"
lazy var titleLable: UILabel = UILabel(frame: CGRect(x: 0, y: 20, width: self.bounds.width, height: 30))
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLable)
titleLable.backgroundColor = .green
backgroundColor = .white
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
}
For reordering of collection view item we need to add Long press gesture on collection view . and for reordering the collection View item with a particular indexPath, UICollectionViewDelegate provide the "targetIndexPathForMoveFromItemAt" method.
And for getting the complete tutorial you follow this one Reordering CollectionView Item
I would do in the following steps...
1-st Step Create a model like this
class CrewMembersModel {
var name: String?
var position: Int?
fileprivate init (name: String, position: Int) {
self.name = name
self.position = position
}
static var crewMembersDataList: [CrewMembersModel]? = nil
class func getMembersList() -> [CrewMembersModel] {
if (self.crewMembersDataList == nil) {
self.crewMembersDataList = [CrewMembersModel]()
self.crewMembersDataList?.append(CrewMembersModel(name: "Sam Carter", position: 0))
self.crewMembersDataList?.append(CrewMembersModel(name: "Dana Scully", position: 1))
self.crewMembersDataList?.append(CrewMembersModel(name: "Rose Taylor", position: 2))
self.crewMembersDataList?.append(CrewMembersModel(name: "Sarah Conor", position: 3))
self.crewMembersDataList?.append(CrewMembersModel(name: "Starbuck", position: 4))
self.crewMembersDataList?.append(CrewMembersModel(name: "River Tam", position: 5))
self.crewMembersDataList?.append(CrewMembersModel(name: "Sarah Jane", position: 6))
}
return crewMembersDataList!.sorted(by: { $0.position! < $1.position!})
}
}
Please Note
Here you can add other properties of your model like image, etc...
2-nd Step Get model list in ViewController viewDidLoad method
self.crewMembersDataList = CrewMembersModel.getMembersList()
3-rd Step In func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell parse model data
let crewMembersData: CrewMembersModel = self.crewMembersDataList[indexPath.row - 1]
cell.name = crewMembersData.name!
And finally in 4-th step whenever you made changes change the positions of selected cells and call collectionView.reloadData()
func changePositions(selectedModelOne: CrewMembersModel, selectedModelTwo: CrewMembersModel) {
let tmpPosition = selectedModelOne.position!
selectedModelOne.position = selectedModelTwo.position!
selectedModelTwo.position = tmpPosition
self.crewMembersDataList.sorted(by: { $0.position! < $1.position!})
self.collectionView.reloadData()
}
Please Note I didn't check entire code, might be some syntax issues
It sounds as if you're only moving one object, or in your case Starbuck.
When you do that the list is going to reorder itself; Based on that Rose Tyler will logically be in the position that you've shown.
If you think of it as a queue for a ride in a theme park then the behaviour you've shown makes perfect sense. Sam, Dana and Rose are first second and third in the queue and Sarah, Starbuck and River are fourth, fifth and sixth. If Rose then lets Starbuck go in front of her then Rose is then fourth in line and Sarah is now fifth.
To get what you want you'll need to keep a reference of where Rose is in the list and where Starbuck is in the list. You'll then need to move each person that needs to move one by one rather than just moving Starbuck as you're doing currently.
UICollectionView *collection;
MSMutableArray *datasource;
NSIndexPath *from = [NSIndexPath indexPathForItem:0 inSection:0];
NSIndexPath *to = [NSIndexPath indexPathForItem:1 inSection:0];
// swap animated
[collection performBatchUpdates:^{
// To avoid problems, you should update your data model inside the updates block
[datasource exchangeObjectAtIndex:from.item withObjectAtIndex:to.item];
// for swap need two move operations
[collection moveItemAtIndexPath:from toIndexPath:to];
[collection moveItemAtIndexPath:to toIndexPath:from];
// no need to update the moved cells because the content has not changed
//[collection reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
}];

Preserve Image Aspect Ratio in UICollectionViewCell

Problem: Reloaded cached UIImageView in a UICollectionViewCell (during scrolling or switching between UICollectionViewController) are not preserving the correct contentMode.
I tried using .sizeToFit() on the UIImageView and resetting .contentMode = UIViewContentMode.ScaleAspectFit on every load, but that wasn't helping.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> MyCollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("myCell", forIndexPath: indexPath) as! MyCollectionViewCell
if ( indexPath.item < self.the_array?.count ){
if ( images_cache[self.the_array![indexPath.item].src!] != nil ){
// calling this here doesn't work
cell.myImage.contentMode = UIViewContentMode.ScaleAspectFit
// calling this here doesn't work either
cell.myImage.sizeToFit()
cell.myImage.image = images_cache[self.the_array![indexPath.item].src!]! as UIImage
}
else{
if let url = NSURL(string: "https:" + (self.the_array![indexPath.item].src)!), data = NSData(contentsOfURL: url) {
let the_image = UIImage( data: data )
cell.myImage.image = the_image
images_cache[self.the_array![indexPath.item].src!] = the_image!
}
}
}
return cell
}
Solution: Set the .frame of the UIImage based on the size of the cell!
cell.myImage.frame = CGRect(x: 0, y: 0, width: cell.frame.width, height: cell.frame.height)
I put that line where I had .sizeToFit() and / or .contentMode and now it preserves the images correctly.