How to insert Item to a collectionView ? Swift3 - ios

I am creating a chatting app in Xcode 8 with Swift 3 and I am using collectionView for the messages. but I am having an issue with inserting a new item to the collection view when press send:
msg.append(message) // msg : Messages Array
let item = msg.count - 1
let IndexPath = NSIndexPath(item: item, section: 0)
self.collectionView.insertItems(at: [IndexPath])
error show up ( Ambiguous reference to member 'collectionView(_:numberOfItemsInSection:)'
How to solve this issue. and is working with storyboard effecting the work with collectionView because I cannot change the cell size with :
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let messageText = msg[indexPath.item].text {
let size = CGSize(width: 250 , height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let esf = NSString(string: messageText).boundingRect(with: size, options: options, attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: CGFloat(18))], context: nil)
print("Changed")
return CGSize(dictionaryRepresentation: esf as! CFDictionary)!
}
print("Not Changed")
return CGSize(width: view.frame.width, height: CGFloat(100))
}
I have but a break point and it has not been reached!

You are using UIViewController and not UICollectionViewController so you don't get the collectionView property for free. You need to drag in an outlet form your storyboard and name it #IBOutlet var collectionView: UICollectionView!.

in my case i reference
self.tableView.SOMEMETHOD
but there is no outlet exist with name "tableView".
please review your code, i think it's issue of outlet

Related

Dynamically resizing collection view cell

I have this older code example that I updated but I am still a newbie and can't figure out how to fix this one error:
In the class that holds the collection view:
var array = ["Just testing this out"]
Now in the UICollectionViewDelegateFlowLayout extension:
private func estimateFrameForText(text: String) -> CGRect {
//we make the height arbitrarily large so we don't undershoot height in calculation
let height: CGFloat = 1000
let size = CGSize(width: 398, height: height)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: UIFont.Weight.light)]
return NSString(string: text).boundingRect(with: size, options: options, attributes: attributes, context: nil)
}
Also:
private func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var height: CGFloat = 295
//we are just measuring height so we add a padding constant to give the label some room to breathe!
var padding: CGFloat = 14
//estimate each cell's height
if let text = array[indexPath.item].text {
height = estimateFrameForText(text).height + padding
}
return CGSize(width: 398, height: height) }
This is where I get the error:
if let text = array[indexPath.item].text
And here's the UILabel from the collection view cell class I have no idea where to implement:
#IBOutlet weak var TextPosted: UILabel!
Your test array needs some adjusting. Try declaring it as an optional (since it likely will be with actual data loaded from somewhere).
var array: [String]? = ["Just testing this out", "Another test"]
Then this:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array?.count ?? 0
}
Then update your if let statement in your collecitonView sizeForItemAtIndexPath function:
if let text = array?[indexPath.item]{
}
On a side note, I wrote an open-source extension to measure string size for things like this a few days ago.
Hope this helps.

UICollectionView reloadData() causes cells to overlap

Intro
Every time a user submits a comment, it is uploaded to the database. After that happened, the collectionView is reloaded. However, the reload does not have the expected behavior. Instead of adding the one comment to the end of the collectionView, it seems to randomly stack some other comment-cells and only display two comment cells, no matter how many comments there are.
This is the output of the View Debugger, you can clearly see that the collectionView cells are stacked:
At this point I would like to clarify that this is not a sizing error of the cells. They all have the correct size assigned, and if I call reloadData on the collectionView, at a later point in time, all the cells are displayed correctly.
I think things will become more clear if you take a look at the gif:
I really don't have any idea where this error comes from or how to fix it.
Code
Initialization of collectionView
private lazy var timelineCollectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
flowLayout.estimatedItemSize = CGSize(width: self.view.frame.width, height: 10)
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
cv.contentInsetAdjustmentBehavior = .never
cv.backgroundColor = .clear
cv.scrollIndicatorInsets = UIEdgeInsets(top: self.coloredTitleBarHeight - self.getStatusBarHeight(), left: 0, bottom: 0, right: 0)
cv.delegate = self
cv.dataSource = self
cv.register(TLContentCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "content-header-cell")
cv.register(TLContentCell.self, forCellWithReuseIdentifier: "comment-cell")
return cv
}()
Code of the UICollectionViewDelegate methods:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let cell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "content-header-cell", for: indexPath) as! TLContentCell
cell.isHeaderCell = true
cell.timelineContent = tlContentItem
cell.delegate = self
cell.isUserInteractionEnabled = true
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return commentItems.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "comment-cell", for: indexPath) as! TLContentCell
cell.delegate = self
cell.timelineContent = TLContent(nativeContentItem: commentItems[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath) as! TLContentCell
return headerView.contentView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
}
The code of the comment-upload handler function:
#objc func submitComment() {
submitCommentButton.isEnabled = false
submitCommentButton.backgroundColor = ColorCodes.lightGray
guard let user = Auth.auth().currentUser else {
print("No user detected. Redirecting to the login screen!")
self.displayWelcomeScreen()
return
}
guard commentTextView.text.count > 0, let commentContent = commentTextView.text else { print("Submission of comment aborted due to empty content."); return }
commentTextView.resignFirstResponder()
commentTextView.text = ""
calculateTextViewSize()
ContentUploader.uploadComment(uid: user.uid, content: NativeContentBase(msg: commentContent, usersTagged: [], topicsTagged: [], mediaAssigned: []), referencedContent: tlContentItem.nativeContent.contentId) { (error, comment) in
if let err = error {
print("An error occured during the comment upload: ", err)
return
}
print("Successfully uploaded comment!")
self.commentItems.append(comment)
DispatchQueue.main.async {
print(self.commentItems.count)
self.timelineCollectionView.reloadData()
}
}
print("Comment scheduled for submission!")
}
I also made sure that I am in the main thread when calling the reloadData() functionality. However, the weird behavior persists.
In order to adjust the collectionView's contentInset and scrollIndicatorInsets to the display of the keyboard, I am using these two functions:
private func adjustScrollBehavior() {
let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let bottomSafeAreaHeight = window?.safeAreaInsets.bottom ?? 0
// adjust the content Inset of the timelineCollectionView
let heightOfCommentSection: CGFloat = commentTextViewHeightAnchor.constant + abs(commentTextViewBottomAnchor.constant) + 8 // get the absolute value of the bottomAnchor's constant as it would be negative
timelineCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: heightOfCommentSection + bottomSafeAreaHeight, right: 0) // does not automatically take care of safe area insets
timelineCollectionView.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: heightOfCommentSection, right: 0) // automatically takes care of safe area insets
}
func calculateTextViewSize() {
let minimumSize = commentTextView.layoutMargins.top + commentTextView.layoutMargins.bottom + commentTextView.font!.lineHeight
let fixedWidth = self.view.frame.width - 32
let maximumSize: CGFloat = 155
let newSize = commentTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
print(newSize)
if newSize.height > minimumSize && newSize.height <= maximumSize {
commentTextViewHeightAnchor.constant = newSize.height
} else {
commentTextViewHeightAnchor.constant = newSize.height > minimumSize ? maximumSize : minimumSize
}
adjustScrollBehavior()
}
Closing
I know this is a lot of code, but basically, that's everything I use. I have no idea why this happens, and I would be incredibly grateful for any help I could get.
Thanks a lot for your help :)

Cells overlapping in UICollectionView

I am displaying the results of a web request to a custom API in an UICollectionView with dynamically sized cells. However, when I call reloadSections after having fetched the data, many of the cells display overlapping.
As the view is scrolled, the cells display in their proper place.
I have tracked down the error to calling reloadSections in the completion block for my web request. Using hardcoded text, the collection view displays properly with the initial layout. If reloadSections is called in the completion block, the error occurs. Below is the relevant code:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(UINib.init(nibName: "ExhibitionsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "ExhibitionsCollectionViewCell")
layout.estimatedItemSize = CGSize(width: 1, height: 1)
collectionView.dataSource = exhibitionsDataSource
collectionView.delegate = self
store.fetchExhibitions {
(exhibitionsResult) -> Void in
switch exhibitionsResult {
case let .success(exhibitions):
print("Successfully found \(exhibitions.count) exhibitions.")
if exhibitions.first != nil {
self.exhibitionsDataSource.exhibitions = exhibitions
}
case let .failure(error):
print("Error fetching exhibitions: \(error)")
self.exhibitionsDataSource.exhibitions.removeAll()
}
// this is the line that produces the erroneous layout
self.collectionView.reloadSections(IndexSet(integer: 0))
}
}
Code making the web request:
func fetchExhibitions(completion: #escaping (ExhibitionsResult) -> Void) {
let url = WebAPI.exhibitionsURL
let request = URLRequest(url: url)
let task = session.dataTask(with: request) {
(data, response, error) -> Void in
let result = self.processExhibitionsRequest(data: data, error: error)
OperationQueue.main.addOperation {
completion(result)
}
}
task.resume()
}
If there is any other code that would be helpful in answering this question, let me know.
Update: I fixed the issue by implementing the following delegate method of UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.cellForItem(at: indexPath)
let height = cell?.contentView.bounds.height ?? 1
let width = cell?.contentView.bounds.width ?? 1
return CGSize(width: width, height: height)
}
Update2: Upon switching out my fake Lorem Ipsum data for the data actually retrieved from the server the problem resurfaced. However, I was able to finally solve the issue simply by switching the line
self.collectionView.reloadSections(IndexSet(integer: 0))
to
self.reloadData()
I would think that reloadSection(_:) simply performs the same action as reloadData() but for the selected sections and the documentation doesn't seem to indicate otherwise...but either way it works now!
From that code I'm not sure of the reason, but maybe try to implement some of the functions provided by the delegate UICollectionViewDelegateFlowLayout.
Focus on the function
sizeForItemAtIndexPath
Example:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(320, 500);
}

Checking if view is added on didSelectItem - Swift

I'm hoping you can help me with my code. I'm trying to make it so that when a cell is clicked it checks if the view has already been added. If it has then it dismisses the view and re-adds it. If it doesn't then it simply adds the view.
Here's my current code:
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
let vc = NoteViewController()
if self.view.subviews.contains(vc.view) {
print("view removed")
}
else {
let cell = collectionView.cellForItem(at: indexPath) as! AnnotatedPhotoCell
sourceCell = cell
vc.picture = resizeImage(image: cell.imageView.image!,
targetSize: CGSize(width:(view.bounds.width - 45),height: 0))
vc.comment = cell.commentLabel
vc.parentVC = parentVC.self
parentVC.self.addChildViewController(vc)
parentVC.self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
vc.view.frame = CGRect(x:0, y:self.view.frame.maxY,
width: width, height: height)
}
}
Hoping you can help me with this.

Collection view cellForItemAt indexPath not getting called (Swift)

I created a collection view controller from story board, and set its custom class to ItemCollectionVC, the custom class of its cell to ItemCell, and set its reuse identifier to Cell
Here's my ItemCollectionVC class:
import UIKit
private let reuseIdentifier = "Cell"
class ItemCollectionVC: UICollectionViewController {
var dataSourceItems: [Items] = []
var counterBuildItems: [Items] {
let weaponItemArray = WeaponItems.weaponItems as [Items]
let defenseItemArray = DefenseItems.defenseItems as [Items]
return weaponItemArray + defenseItemArray
}
var freeBuildItems = WeaponItems.weaponItems as [Items]
var captureKrakenItems: [Items] {
let weaponItemArray = WeaponItems.weaponItems as [Items]
let abilityItemArray = AbilityItems.abilityItems as [Items]
return weaponItemArray + abilityItemArray
}
override func viewDidAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSourceItems.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ItemCell
cell.cellImage.image = dataSourceItems[indexPath.row].image
print(dataSourceItems.count)
return cell
}
}
When the collection view controller is presented, it's empty, what could cause the problem?
One of three things caused this problem, pretty much every time I have encountered it, in a TableView or CollectionView:
1) Your ViewController is not the dataSource of your UICollectionView
2) numberOfRows or numberOfSections method returns 0
3) The height of your cell is 0, either due to constraint problems, or a heightForCell method being not/improperly implemented.
It's impossible to say which of these is your problem, and it's always possible that you've encountered something strange. Make certain that none of these is your problems, before exploring less likely options.
If you are pretty sure that the dataSource of the collectionView is connected to the viewController (it should be by default), then you should reloadData() because the collectionView reading from dataSourceItems. To understand the case, add a break point in cellForItemAt and add another one in viewDidAppear and check which one is called first?
override func viewDidAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
collectionView.reloadData()
}
Hope that helped.
Some things that you could easily miss if you use storyboard
1) Don't forget that content in cell must have connected Top, Bottom constraint and
content view must have height, by this cell will know to set height for cell. If you don't have these, cell height will be 0, and function cellForItemAt will never get called.
2) You can use cell layout to set dynamic cell and height for cell if you use this function:
func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout:
UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) ->
CGSize {return CGSize(width: 20.00, height: 20.00)}
I fixed my problem by initializing UICollectionView properly like the following:
fileprivate let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
Check if you get the right number of sections in the numberOfItemsInSection method for the collection view.
If you are adding a flow layout to the collection view, remove the flow layout, and check if the collection view cells show now.
If they do, just adjust your collection view flow layout code it should look like this
let _flowLayout = UICollectionViewFlowLayout()
_flowLayout.sectionInset = UIEdgeInsets(top:0, left: 0, bottom: 0, right: 0)
_flowLayout.scrollDirection = .vertical
yourCollectionView.collectionViewLayout = _flowLayout
you can set the inset to fit your use.
You have to add your code in viewWillAppear, then it will work properly.
/*
override func viewDidAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
}
Like this :-
override func viewWillAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
}
*/
In my case there was a problem with the collection view contentInset, try adding below code in your collection view sub class.
override func layoutSubviews() {
super.layoutSubviews()
self.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
override func reloadData() {
DispatchQueue.main.async {
super.reloadData()
}
}

Resources