Collectionview interactive cell swapping - swift

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) {
}];

Related

page control not working for UICollectionView inside UITableView

Im having issue with making page control work for a collection view which is inside table view. There are actually 2 collection views in the view controller, one is outside the tableView and other one is inside. I want page control for inside one. Let me share code below and then explain what is causing trouble:
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
//setup stories collection view
let nib = UINib(nibName: "StoryCollectionViewCell", bundle: nil)
cvStories.register(nib, forCellWithReuseIdentifier: "StoryCollectionViewCell")
let layout1 = UICollectionViewFlowLayout()
layout1.scrollDirection = .horizontal
layout1.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
collectionViewStories.setCollectionViewLayout(layout1, animated: true)
collectionViewStories.delegate = self
collectionViewStories.dataSource = self
tblHome.dataSource = self
tblHome.delegate = self
}
// MARK: - Table View
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tblHome.dequeueReusableCell(withIdentifier: "HomeTableViewCell", for: indexPath) as! HomeTableViewCell
cell.collectionViewPostImages.dataSource = self
cell.collectionViewPostImages.delegate = self
cell.collectionViewPostImages.tag = indexPath.row
cell.collectionViewPostImages.reloadData()
cell.pageControl.hidesForSinglePage = true
cell.pageControl.numberOfPages = self.posts[cell.collectionViewPostImages.tag].imagesArray?.count ?? 0
return cell
}
//MARK: - Collection View
func numberOfSections(in collectionView: UICollectionView) -> Int {
if collectionView == cvStories {
return 2
}
else {
return 1
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == collectionViewStories {
return stories.count
}
else {
return posts[collectionView.tag].imagesArray?.count ?? 0
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == collectionViewStories {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StoryCollectionViewCell", for: indexPath) as! StoryCollectionViewCell
//code to show stories
return cell
}
else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeCollectionViewCell", for: indexPath) as! HomeCollectionViewCell
//code to show images
return cell
}
}
}
//TableViewCell Class
class HomeTableViewCell: UITableViewCell {
#IBOutlet weak var collectionViewPostImages: UICollectionView!
#IBOutlet weak var pageControl: UIPageControl!
var currentPage = 0
override func awakeFromNib() {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: UIScreen.main.bounds.size.width, height: cvPostImages.frame.size.height)
flowLayout.scrollDirection = .horizontal
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
collectionViewPostImages?.collectionViewLayout = flowLayout
collectionViewPostImages.showsHorizontalScrollIndicator = false
collectionViewPostImages.isPagingEnabled = true
}
//ScrollView delegate method
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.width
self.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
self.pageControl.currentPage = self.currentPage
}
}
The whole code is like Instagram app, stories at top and posts with images in tableView. Whole code is in single View controller and everything works fine. Even dots (page number) are correct according to number of pages however when you swipe between them they do not change. I saw many solutions and in those pageControl.numberOfPages was used in cellForItemAt function but its out of scope for me so I just called it as cell.pageControl.numberOfPages in cellForRowAt instead. First time dealing with collectionView inside tableView so getting confused getting it worked.
The main problem is you use cell.collectionViewPostImages.delegate = self in HomeViewController but your method scrollViewDidScroll in cell HomeTableViewCell.
Collectionview's own scrollview delegate methods triggering in UICollectionViewDelegate. Thats why scrollViewDidScroll is not triggered in your cell.
Solution is using scrollViewDidScroll in where you delegete the collectionview. It is HomeViewController
By the way, I dont recommend to use cell.collectionViewPostImages.dataSource = self cell.collectionViewPostImages.delegate = self in reusable cell. You dont want to make ...delegate = self on everytime when cell reused. You can take it in awakeFromNib in cell class

How to use a UISwitch to add and remove data from an Array of Data

I have a tableview with a UISwitch in each of the cells. What I am trying to do is that whenever the UISwitch is Toggled On, it adds that cell into an array and when the switch is Toggled Off it removes it. Right now it only adds and doesn't remove.
Once this is complete I need the CollectionView that is also within this ViewController to update and visually show the newStringArray Cells based on the number in that array and that also is able to appear and disappear based on the cells that have their UISwitch toggled on.
import UIKit
class NewMoveViewController: UIViewController {
private var stringSource = ["String 1,", "String 2", "String 3"]
var newStringArray = Array<String>()
private let tableView: UITableView = {
let tableView = UITableView()
tableView.rowHeight = 100
return tableView
}()
private var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 50, height: 50)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.register(NewMoveCollectionViewCell.self, forCellWithReuseIdentifier: NewMoveCollectionViewCell.identifier)
collectionView?.showsHorizontalScrollIndicator = false
title = "Add to Group"
tableView.register(NewMoveTableViewCell.self, forCellReuseIdentifier: NewMoveTableViewCell.identifier)
tableView.delegate = self
tableView.dataSource = self
collectionView?.backgroundColor = .systemBlue
collectionView?.dataSource = self
collectionView?.delegate = self
guard let myCollection = collectionView else {
return
}
view.addSubview(myCollection)
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = CGRect(x: 0, y: 100, width: view.frame.size.width, height: 50)
tableView.frame = CGRect(x: 0, y: 200, width: view.frame.size.width, height: view.frame.size.height)
}
}
extension NewMoveViewController : UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: NewMoveTableViewCell.identifier, for: indexPath) as! NewMoveTableViewCell
let switchView = UISwitch(frame: .zero)
switchView.setOn(false, animated: true)
switchView.tag = indexPath.row
switchView.addTarget(self, action: #selector(self.switchDidChange(_:)), for: .valueChanged)
cell.accessoryView = switchView
cell.configure(with: "", label: "test")
return cell
}
#objc func switchDidChange(_ sender: UISwitch) {
newStringArray.append(stringSource[sender.tag])
// newStringArray.remove(stringSource.remove(at: [s]))
print(newStringArray)
}
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
return false
}
}
extension NewMoveViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return newStringArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewMoveCollectionViewCell.identifier, for: indexPath) as! NewMoveCollectionViewCell
return cell
}
}
the hardest part is to remove an object from an array, my approach on these situations is to transform my array in a NSMutableArray because it have a function to remove an specific object, then make a delegate in the cell that informs the viewController to remove the object from the list and reload the tableView.
the delegate wil be something like this:
protocol RemoveObjectFromCell {
func removeObjectIncell(object: MyObject)
}
class myCell: UITableViewCell {
//your outlets and variables
var object: MyObject?
var delegate: removeObjectIncell?
func setupCell(object: myObject) {
//configure your cell with the specific object
}
}
make sure of calling the delegate on the switch action inside you cell class like this:
#IBAction func switchPressed(sender: UISwitch) {
if !sender.isOn {
self.delegate?.removeObjectIncell(object: self.object)
}
in the view controller implement your protocol and use the required function like this:
class myViewController: UIViewController, RemoveObjectFromCell {
//everything that goes in your class
func removeObjectIncell(object: MyObject) {
self.myArray.remove(object)
DispatchQueue.main.async {
self.myTableView.reloadData()
}
}
}
In order to get changes you want you have to set property which is gonna indicate whether switch is on or off
Something like: var switchIsActive = false
and simply change it in function and if it is turned on you perform one action when it is off you perform another one. Also after function you have to reload your tableView tableView.reloadData()
You can remove elements in your array by their tag by calling Array.remove(at: Int). It can be done by the cells [indexPath.row]

Update CollectionView supplementaryView header when off screen

I have a view similar to Instagram's main stories feed. I have a horizontal collection view at the top where I have a supplementaryView header cell that looks the same but acts differently to the rest of the cells.
I am trying to update the objects in the supplementaryView which I have managed to do with the following code...
#objc func uploadCompleted() {
DispatchQueue.main.async {
self.collectionView.performBatchUpdates({
if let myCell = self.collectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyCollectionReusableView {
// Do your stuff here
myCell.nameLbl.textColor = .black
...
}
}, completion: nil)
}
}
This works fine when the cell is in view, however when the cell is swiped off screen during this call it doesn't get updated.
Any ideas on how to get around this? Or a better approach to updating just the first cell in a collection view and not the rest.
Thanks
The header is not nil when it's visible you need
var color = UIColor.red
And inside viewForSupplementaryElementOfKind
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView
let myCell = //
myCell.nameLbl.textColor = color
return myCell
}
Then alter color where and when you want after the header appears again it'll be changed to the latest color
#objc func uploadCompleted() {
DispatchQueue.main.async {
self.color = UIColor.black
self.collectionView.performBatchUpdates({
if let myCell = self.collectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyCollectionReusableView {
// Do your stuff here
myCell.nameLbl.textColor = self.color
...
}
}, completion: nil)
}
}

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 {

My UICollectionView does not scroll smoothly using 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.