How can I declare NSAttributedStringKey.font for custom font? - swift

I want to calculate the height of the text to get the estimated height for the collection view cell. I use the following code inside collectionViewLayout function;
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let messageText = messages[indexPath.row]?.text {
let size = CGSize(width: view.frame.width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: messageText).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17)], context: nil)
return CGSize(width: view.frame.width, height: estimatedFrame.height + 20)
}
return CGSize(width: view.frame.width, height: 100)
}
This is working for system fonts, but not for my custom font inside my project. The problem is that the estimatedFrame is not equal to the result with a system font. I think the problem will be the param of options: attributes. Is there a way like UIFont.systemFont(ofSize: 17) for custom fonts?

If I'm reading your question right, you want:
UIFont(name: "yourCustomFontNameString", size: 17)
So:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let messageText = messages[indexPath.row]?.text {
let size = CGSize(width: view.frame.width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: messageText).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font: UIFont(name: "yourCustomFontNameString", size: 17)], context: nil)
return CGSize(width: view.frame.width, height: estimatedFrame.height + 20)
}
return CGSize(width: view.frame.width, height: 100)
}

Related

UICollectionView Items Data distorts after scrolling in Swift 4.2

I am trying to implement a chat screen using UICollectionView and data is displayed as expected. However when I try to scroll it a few times my data gets distorted as explained in the screenshots. Can anyone suggest what's going wrong and how to solve it? Thanks!
First it shows:
After scrolling a few times it shows:
Code of all the methods related to UICollectionView I'm using:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let count = chatCategoriesArray.messages.count
if count != 0 {
return count
}
return 0
}
var allCellsHeight: CGFloat = 0.0
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ChatLogMessageCell
cell.messageTextView.text = chatCategoriesArray.messages[indexPath.item]
let size = CGSize(width: 250, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: chatCategoriesArray.messages[indexPath.item]).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18)], context: nil)
if chatCategoriesArray.senderIds[indexPath.item] == "s_\(self.studentInstance.tutorIdFound)"{
cell.profileImageView.image = UIImage(named: "three.png")
cell.profileImageView.isHidden = false
cell.messageTextView.frame = CGRect(x: 48 + 8, y:0, width: estimatedFrame.width + 16, height: estimatedFrame.height + 20)
cell.textBubbleView.frame = CGRect(x: 48, y: 0, width: estimatedFrame.width + 16 + 8, height: estimatedFrame.height + 20)
self.currentCellWidth = Double(estimatedFrame.width + 16 + 8)
cell.textBubbleView.backgroundColor = .white
cell.addSubview(cell.profileImageView)
cell.addConstraintsWithFormat(format: "H:|-8-[v0(30)]", views: cell.profileImageView)
cell.addConstraintsWithFormat(format: "V:[v0(30)]|", views: cell.profileImageView)
}
else{
cell.profileImageView.image = UIImage(named: "two.png")
cell.textBubbleView.backgroundColor = UIColor(r: 28, g:168, b:261)
cell.messageTextView.frame = CGRect(x: view.frame.width - estimatedFrame.width - 16 - 46, y:0, width: estimatedFrame.width + 16, height: estimatedFrame.height + 20)
cell.messageTextView.textColor = .white
cell.textBubbleView.frame = CGRect(x: view.frame.width - estimatedFrame.width - 16 - 8 - 46, y: 0, width: estimatedFrame.width + 16 + 8, height: estimatedFrame.height + 20)
self.currentCellWidth = Double(estimatedFrame.width + 16 + 8)
cell.addSubview(cell.profileImageView)
cell.addConstraintsWithFormat(format: "H:[v0(30)]-8-|", views: cell.profileImageView)
cell.addConstraintsWithFormat(format: "V:[v0(30)]|", views: cell.profileImageView)
}
allCellsHeight += cell.frame.height
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if allCellsHeight < (collectionView.frame.height){
return UIEdgeInsets(top: view.frame.height - allCellsHeight, left: 0, bottom: 0, right: 0)
}
else {
return UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let size = CGSize(width: 250, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: chatCategoriesArray.messages[indexPath.item]).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18)], context: nil)
return CGSize(width: view.frame.width, height: estimatedFrame.height + 20)
}
Collection cells are reused. That means that when you scroll up/down, the cells that become invisible are removed from hierarchy and queued for reusing. Then the collection view calls cellForItem: again for the items that become visible. dequeueReusableCell does not always create a new instance. Usually it will only return a cell that has become invisible for you to setup it again with new data.
If you add views/constraints during setup, you have to make sure to remove the ones you have added previously, otherwise the cell will have duplicate views and conflicting constraints.
Also note that allCellsHeight cannot work like this. cell.frame.height won't be correct immediately after setup (before actual layout) and since the method can be called several times for the same item, you cannot just add to a global variable. You should rather use collectionView.contentSize.height instead.
This is classic reusable cell trouble. This happens cause collection view reuse your receiver cells setting to create sender message box.
I will suggest you to use two different cell for sender and receiver. With constraint set on first load. This will have a positive impact on performance as well.
Check following image to understand how to use 2 cell.

Swift: Set cell left alignement for UICollectionView horizontally

I have a collectionView where the cells have a dynamic width. The problem is that the cells are aligned in the center, and I want it to be aligned to the left, look at the picture, I drew a red line where I want the alignment:
Here my collection view configuration:
var collectionView: UICollectionView = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 40)
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
collectionView.setCollectionViewLayout(layout, animated: true)
collectionView.register(SportsCell.self, forCellWithReuseIdentifier: "MyCell")
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .white
collectionView.showsHorizontalScrollIndicator = false
return collectionView
}()
Then the method to make the size dynamic:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let text = items[indexPath.row].message
let padding: CGFloat = 5
return CGSize(width: textWidth(text: text, font: .systemFont(ofSize: 22)) + padding, height: 40)
}
func textWidth(text: String, font: UIFont?) -> CGFloat {
let attributes = font != nil ? [NSAttributedString.Key.font: font!] : [:]
return text.size(withAttributes: attributes).width
}

Left Align UICollectionViewCells only works when cells are not correct height, and stops working when scrolled

I am trying to left align the cells in a UICollectionView, and I have used a GitHub repo to try and obtain this, link here:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout
I set up the CollectionView and its cells like so:
lazy var filterCollectionView: UICollectionView = {
let layout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, verticalAlignment: .center)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.register(filterCell.self, forCellWithReuseIdentifier: "filterCellId")
cv.backgroundColor = .white
cv.isScrollEnabled = true
cv.dataSource = self
cv.delegate = self
cv.showsHorizontalScrollIndicator = false
return cv
}()
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return genrArray.count
}
let genArray = ["Test123", "LongTextTest", "Short", "S", "#LongLongLongLong"]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "filterCellId", for: indexPath) as! filterCell
cell.genText.text = genArray[indexPath.row]
cell.genText.sizeToFit()
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.genText.frame.width + 25, height: 24)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let label = UILabel(frame: .zero)
label.text = genArray[indexPath.row]
label.frame = CGRect(x: 0, y: 0, width: label.intrinsicContentSize.width, height: 0)
return CGSize(width: label.frame.width + 25, height: 18)
}
This did nothing to the cells, so I added these lines of code into ViewDidLoad():
let layout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, verticalAlignment: .center)
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
self.filterCollectionView.collectionViewLayout = layout
The line that actually made a change in the CollectionView was layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize. But this still did not achieve the desired effect. This squished my cells and made them have a height of much smaller than 18 (the size declared in sizeForItem. When loaded up, the collectionView looks like this:
But when I scroll through the collectionView, it reverts back to normal as if the left alignment layout is not even there. It then looks like this:
The cells are the correct shape in that image, but the alignment/layout/spacing is off.
What am I doing wrong?

Estimating size of UICollectionView cell for some text

So I'm creating a messaging type app, which consists of some blocks of UITextView containing varying lengths of text and these reside in a "bubble" UIView.
let textView: UITextView = {
let text = UITextView()
text.text = "SAMPLE"
text.translatesAutoresizingMaskIntoConstraints = false
text.backgroundColor = .clear
text.textColor = .white
return text
}()
let bubbleView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(r: 0, g: 137, b: 247)
view.layer.cornerRadius = 14
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var bubbleWidthAnchor: NSLayoutConstraint?
bubbleView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -8).isActive = true
bubbleView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
bubbleWidthAnchor = bubbleView.widthAnchor.constraint(equalToConstant: 250)
bubbleWidthAnchor?.isActive = true
bubbleView.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
textView.leftAnchor.constraint(equalTo: bubbleView.leftAnchor, constant: 8).isActive = true
textView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
textView.rightAnchor.constraint(equalTo: bubbleView.rightAnchor).isActive = true
textView.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
To set the height of the cell I am using a custom function which is supposed to not working properly.
Custom function:
private func estimatedFrameForText(text: String) -> CGRect {
let size = CGSize(width: 250, height: 250)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 16)], context: nil)
}
Which I call in the sizeForItemAt function for UICollectionView:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var height: CGFloat = 80 //Arbitrary number
if let text = messages[indexPath.item].text {
height = estimatedFrameForText(text: text).height + 8
}
return CGSize(width: view.frame.width, height: height)
}
The simple problem I am having is... it does not working great:
Example
Any idea where I am going wrong, or a better solution to getting the estimated size I need for the cell, depending on the text?
As it turns out, all that I was missing was to set the text size in the textView.
Putting text.font = UIFont.systemFont(ofSize: 16) in was required because the function to get the estimated size has:
attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 16)
So you have to define both to be the same.
Swift 4.2 updated answer is to handle height and width of uicollectionviewCell on the basis of uilabel
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let size = (self.FILTERTitles[indexPath.row] as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14.0)])
return CGSize(width: size.width + 38.0, height: size.height + 25.0)
}

How to set cell spacing and UICollectionView - UICollectionViewFlowLayout size ratio?

I'm trying to add UICollectionView to ViewController, and I need to have 3 cells 'per row' without blank space between cells (it should look like a grid). Cell width should be one third of screen size, so I thought that the layout.item width should be the same. But then I get this:
If I reduce that size (by 7 or 8 pixels e.g.), it's better, but the third cell in row is not completely visible, and I still have that blank space (top & bottom, and left & right) .
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
var collectionView: UICollectionView?
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
screenSize = UIScreen.mainScreen().bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
// Do any additional setup after loading the view, typically from a nib
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 0, bottom: 10, right: 0)
layout.itemSize = CGSize(width: screenWidth / 3, height: screenWidth / 3)
collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
collectionView!.dataSource = self
collectionView!.delegate = self
collectionView!.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
collectionView!.backgroundColor = UIColor.greenColor()
self.view.addSubview(collectionView!)
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as CollectionViewCell
cell.backgroundColor = UIColor.whiteColor()
cell.layer.borderColor = UIColor.blackColor().CGColor
cell.layer.borderWidth = 0.5
cell.frame.size.width = screenWidth / 3
cell.frame.size.height = screenWidth / 3
cell.textLabel?.text = "\(indexPath.section):\(indexPath.row)"
return cell
}
}
Add these 2 lines
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
So you have:
// Do any additional setup after loading the view, typically from a nib.
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 0, bottom: 10, right: 0)
layout.itemSize = CGSize(width: screenWidth/3, height: screenWidth/3)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView!.collectionViewLayout = layout
That will remove all the spaces and give you a grid layout:
If you want the first column to have a width equal to the screen width then add the following function:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
if indexPath.row == 0
{
return CGSize(width: screenWidth, height: screenWidth/3)
}
return CGSize(width: screenWidth/3, height: screenWidth/3);
}
Grid layout will now look like (I've also added a blue background to first cell):
For Swift 3 and XCode 8, this worked. Follow below steps to achieve this:-
{
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
let width = UIScreen.main.bounds.width
layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
layout.itemSize = CGSize(width: width / 2, height: width / 2)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView!.collectionViewLayout = layout
}
Place this code into viewDidLoad() function.
In Certain situations, Setting the UICollectionViewFlowLayout in viewDidLoador ViewWillAppear may not effect on the collectionView.
Setting the UICollectionViewFlowLayout in viewDidAppear may cause see the changes of the cells sizes in runtime.
Another Solution, in Swift 3 :
extension YourViewController : UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 0, bottom: 10, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let collectionViewWidth = collectionView.bounds.width
return CGSize(width: collectionViewWidth/3, height: collectionViewWidth/3)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
}
If you are looking for Swift 3, Follow the steps to achieve this:
func viewDidLoad() {
//Define Layout here
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
//Get device width
let width = UIScreen.main.bounds.width
//set section inset as per your requirement.
layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
//set cell item size here
layout.itemSize = CGSize(width: width / 2, height: width / 2)
//set Minimum spacing between 2 items
layout.minimumInteritemSpacing = 0
//set minimum vertical line spacing here between two lines in collectionview
layout.minimumLineSpacing = 0
//apply defined layout to collectionview
collectionView!.collectionViewLayout = layout
}
This is verified on Xcode 8.0 with Swift 3.
let layout = myCollectionView.collectionViewLayout as? UICollectionViewFlowLayout
layout?.minimumLineSpacing = 8
Swift 4, Swift 5, Easiest Way!
No need create UICollectionViewFlowLayout() instance, just implement UICollectionViewDelegateFlowLayout on your Class.
extension MyCollectionView: UICollectionViewDelegateFlowLayout {
// Distance Between Item Cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
// Cell Margin
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
}
}
Swift 4
let collectionViewLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
collectionViewLayout?.sectionInset = UIEdgeInsetsMake(0, 20, 0, 40)
collectionViewLayout?.invalidateLayout()
Swift 5 : For evenly distributed spaces between cells with dynamic cell width to make the best of container space you may use the code snippet below by providing a minimumCellWidth value.
private func collectionViewLayout() -> UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
// Important: if direction is horizontal use minimumItemSpacing instead.
layout.scrollDirection = .vertical
let itemHeight: CGFloat = 240
let minCellWidth :CGFloat = 130.0
let minItemSpacing: CGFloat = 10
let containerWidth: CGFloat = self.view.bounds.width
let maxCellCountPerRow: CGFloat = floor((containerWidth - minItemSpacing) / (minCellWidth+minItemSpacing ))
let itemWidth: CGFloat = floor( ((containerWidth - (2 * minItemSpacing) - (maxCellCountPerRow-1) * minItemSpacing) / maxCellCountPerRow ) )
// Calculate the remaining space after substracting calculating cellWidth (Divide by 2 because of left and right insets)
let inset = max(minItemSpacing, floor( (containerWidth - (maxCellCountPerRow*itemWidth) - (maxCellCountPerRow-1)*minItemSpacing) / 2 ) )
layout.itemSize = CGSize(width: itemWidth, height: itemHeight)
layout.minimumInteritemSpacing = min(minItemSpacing,inset)
layout.minimumLineSpacing = minItemSpacing
layout.sectionInset = UIEdgeInsets(top: minItemSpacing, left: inset, bottom: minItemSpacing, right: inset)
return layout
}
For Swift 3+ and Xcode 9+ Try using this
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let collectionWidth = collectionView.bounds.width
return CGSize(width: collectionWidth/3, height: collectionWidth/3)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
instead of writing a line of code, we have an option in XCode, select CollectionView and go to navigator and change the "Min Spacing"
For Swift 3 and XCode 8, this worked. Follow below steps to achieve this:-
viewDidLoad()
{
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
var width = UIScreen.main.bounds.width
layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
width = width - 10
layout.itemSize = CGSize(width: width / 2, height: width / 2)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView!.collectionViewLayout = layout
}