Accurately get size of wrapping NSTextField (Swift) - swift

I need to accurately calculate the size of an NSTextField (because I need that value to calculate the height of the NSTableView's row in which the NSTextField sits). I have a rough approximation now, but it seems off (and I don't want to hard-code fudge it...).
My approximation involves creating a temporary cell, adding the appropriate text to it, and then calculating the height based on that text:
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat
{
let textDistanceFromTableEdge = 192
if let cell = tableView.make(withIdentifier: "IncomingMessage", owner: nil) as? IncomingMessage
{
cell.message.stringValue = messages[row].message
let width = ((tableView.tableColumns.last?.width)! - textDistanceFromTableEdge)
let height = (cell.message.cell!.cellSize(forBounds: NSMakeRect(CGFloat(0.0), CGFloat(0.0), width, CGFloat(FLT_MAX))).height)
return (height + 50)
}
}
This very often gets the right results, but it's just slightly off (often, when a single word wraps to the next line, it will not result in the cell being one line taller).

This seems to work:
let width = self.textField.bounds.width
let cell = (self.textField.cell() as? NSCell)!
let rect = cell.drawingRectForBounds(NSMakeRect(CGFloat(0.0), CGFloat(0.0), width, CGFloat(CGFloat.max)))
let size = cell.cellSizeForBounds(rect)
self.textField.setFrameSize(NSMakeSize(width, size.height))
Similar problem with a different solution: NSTextFieldCell's cellSizeForBounds: doesn't match wrapping behavior?

Related

Change the height of a TableViewCell depending on if it has an image

I have a tableview that has tableview cells that are populated with users posts on the app. users are able to have posts with just text or they can include an image with their text.
The problem i have is with setting the cell height depending on if the post contains an image. for example if the post contains an image i want the cell height = 400 but if it contains only text then i want the cell height = 150.
At the moment i have the height fixed at 400 using this code but i don't know how to make the above modifications.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//auto cell height
return 400
}
post model looks like this...
class Posts {
var id: String
var author: String
var text: String
var createdAt: Date
var degree: String
var university: String
var uid: String
var photoURL: String
var url: URL
var postID: String
var likes: Int
init(id:String, author:String, text:String, timestamp:Double, degree:String, university:String, uid:String, photoURL:String, url:URL, postID:String, likes:Int) {
self.id = id
self.author = author
self.text = text
self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
self.degree = degree
self.university = university
self.uid = uid
self.photoURL = photoURL
self.url = url
self.postID = postID
self.likes = likes
}
}
One decently easy way to do this is to create a custom view cell and hide the imageView if needed.
Create the custom tableView cell and make sure to add a .xib file to be able to edit everything visually:
Set the row height to automatic so that the tableView knows to set the height to whatever you need.
Then, you can add your text and imageView onto this custom cell. The way I've gotten this to work before is to add both elements into a stackView and set up constraints on that.
Make sure you have constraints set up for the height the label and imageView inside of the stackView and the distance from the stackView to the bottom and top of the superView. This lets auto layout know how tall to make the cell
Now, any time you hide the imageView (when you don't have an image to display) the cell should autoresize to be smaller.
I ended up adding this code to the tableview cellforrowat index path function in order to get the images rendering correctly.
let imageIndex = posts[indexPath.row].mediaURL
let url = NSURL(string: imageIndex)! as URL
if let imageData: NSData = NSData(contentsOf: url) {
let image = UIImage(data: imageData as Data)
let newWidth = cell.MediaPhoto.frame.width
let scale = newWidth/image!.size.width
let newHeight = image!.size.height * scale
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
image!.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
cell.MediaPhoto.image = newImage
cell.MediaPhoto.contentMode = .scaleToFill
cell.MediaPhoto.clipsToBounds = true

Calendar-like UICollectionView - how to add left inset before first item only?

I have the following UICollectionView:
It has vertical scrolling, 1 section and 31 items.
It has the basic setup and I am calculating itemSize to fit exactly 7 per row.
Currently it looks like this:
However, I would like to make an inset before first item, so that the layout is even and there are the same number of items in first and last row. This is static and will always contain 31 items, so I am basically trying to add left space/inset before first item, so that it looks like this:
I have tried using a custom UICollectionViewDelegateFlowLayout method:
collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int)
But since there is only 1 section with 31 rows, it insets all of the rows, not just the first.
I know I could probably add two more "blank" items, but I feel like there is a better solution I may not be aware of. Any ideas?
EDIT: I've tried Tarun's answer, but this doesn't work. Origin of first item changes, but the rest stays as is, therefore first overlaps the second and the rest remain as they were. Shifting them all doesn't work either. I ended up with:
You need to subclass UICollectionViewFlowLayout and that will provide you a chance to customize the frame for all items within the collectionView.
import UIKit
class LeftAlignCellCollectionFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
guard let collectionView = self.collectionView else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
let leftMargin = self.sectionInset.left
let layout = collectionView.collectionViewLayout
for attribute in attributes {
if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
// Check for `indexPath.item == 0` & do what you want
// cellAttribute.frame.origin.x = 0 // 80
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
}
Now you can use this custom layout class as your flow layout like following.
let flowLayout = LeftAlignCellCollectionFlowLayout()
collectionView.collectionViewLayout = flowLayout
Following Taran's suggestion, I've decided to use a custom UICollectionViewFlowLayout. Here is a generic answer that works for any number of items in the collectionView, as well as any inset value:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView else { return nil }
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
if let cellAttribute = collectionView.collectionViewLayout.layoutAttributesForItem(at: attribute.indexPath) {
let itemSize = cellAttribute.frame.size.width + self.minimumInteritemSpacing
let targetOriginX = cellAttribute.frame.origin.x + CGFloat(self.itemInset) * itemSize
if targetOriginX <= collectionView.bounds.size.width {
cellAttribute.frame.origin.x = targetOriginX
} else {
let shiftedPosition = lround(Double((targetOriginX / itemSize).truncatingRemainder(dividingBy: CGFloat(self.numberOfColumns))))
cellAttribute.frame.origin.x = itemSize * CGFloat(shiftedPosition)
cellAttribute.frame.origin.y += itemSize
}
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
where:
self.itemInset is the value we want to inset from the left (2 for my initial question, but it can be any number from 0 to the number of columns-1)
self.numberOfColumns is - as the name suggests - number of columns in the collectionView. This pertains to the number of days in my example and would always be equal to 7, but one might want this to be a generic value for some other use case.
Just for the sake of the completeness, I provide a method that calculates a size for my callendar collection view, based on the number of columns (days):
private func collectionViewItemSize() -> CGSize {
let dimension = self.collectionView.frame.size.width / CGFloat(Constants.numberOfDaysInWeek) - Constants.minimumInteritemSpacing
return CGSize(width: dimension, height: dimension)
}
For me, Constants.numberOfDaysInWeek is naturally 7, and Constants.minimumInteritemSpacing is equal to 2, but those can be any numbers you desire.

Collectionview inside tableview causing issue with 2 columns

I have vertical collection-view inside tableview cell. collection view contain feature of load more too. for self sizing of collection view, i make tableview cell automaticDimension.
Also i have give height constant to collection-view. first time its loaded correctly but once i go to last cell and its load-more after reloading it create lot of space after collection view. can any one let me know what i am doing wrong here. or is there any other way around to make collection-view inside tableview self sizing so it increase tableview cell height too
**
TableviewCell Class
**
justForYouCollectionView.dataSource = self
justForYouCollectionView.delegate = self
justForYouCollectionView.isScrollEnabled = false
self.collectionHeight.constant = self.justForYouCollectionView.collectionViewLayout.collectionViewContentSize.height
justForYouCollectionView.reloadData()
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
self.layoutIfNeeded()
let contentSize = self.justForYouCollectionView.collectionViewLayout.collectionViewContentSize
return CGSize(width: contentSize.width, height: contentSize.height + 20)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == self.justForYouArray.count - 1 && self.isLoadMore {
updateNextSet()
}
}
**
CollectionViewCell Class
**
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
// Specify you want _full width_
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
// Calculate the size (height) using Auto Layout
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
// Assign the new size to the layout attributes
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
extra space can be seen in image
I have worked on that earlier all I did is, set tableview height constraint set in storyboard and drop its outlet in viewController then after populate data get array count and divide by 2 and after dividing I multiply it by CollectionViewCell height and set that height to the UITableView height constraint like this.
let count = (array.count / 2) * cellHeight
tableviewHeightConstraint.constant = CGFloat(count)
This will solve your problem.
try
let a = (yourArray.count /2 ) * heightCell
tblViewHeightConstraint.constant = CGFloat(a)

Collection view cell vertical top alignment

I have a problem in collection view(Grid Format) cell height. The grid format contains two columns overall, so basically 2 cells in each row. My collection view cell height increase according to the content inside it but the content is center aligned and not top aligned. I want to achieve the height of the cells in a row which has the greater height. How can I do that? Please advice.
https://drive.google.com/file/d/0B56cE4V6JI-RYmdQT2pIZ0hYQ2phM3Z2YmJNYU1SeXNnYTNN/view?usp=sharing
https://drive.google.com/file/d/0B56cE4V6JI-RQUdqVmV4b244cFI5SGd2TnJfbG1tckdQU21Z/view?usp=sharing
https://drive.google.com/file/d/0B56cE4V6JI-RYTYxVzJyVUp1clpJTkVqYjN6QXBPeERvVHZR/view?usp=sharing
I have added a link above which is the problem right now.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let cellsAcross: CGFloat = 2
let spaceBetweenCells: CGFloat = 10
let dim = (collectionView.bounds.width - (cellsAcross - 1) * spaceBetweenCells) / cellsAcross
print(dim) // calculating width
//calculating my content height
let toppingHeight = sizeOfString(string: PizzaMenuItems[indexPath.row].pizzaToppings, constrainedToWidth: Double(dim))
let pizzaNameHeight = sizeOfString(string: PizzaMenuItems[indexPath.row].menuName, constrainedToWidth: Double(dim))
let newHeight = toppingHeight.height + pizzaNameHeight.height + 57
// to increase my collection view height
let heightt = (view.frame.width)/2
let count = self.PizzaMenuItems.count
if self.PizzaMenuItems.count == 1 {
}
else{
if count % 2 == 0
{ //even Number
collectionViewC_Height.constant = heightt * CGFloat(self.PizzaMenuItems.count/2) + 57
} else
{ // odd Number
collectionViewC_Height.constant = heightt * CGFloat(self.PizzaMenuItems.count/2+1)}
}
return CGSize(width: dim, height: newHeight)}
Verbal work around:
In your collection cell xib don't specify UILabel on the container view.
place a UIView on the bottom layer of container view with (top:0,lead:0,trail:0,bottom:0) constraints.
Now place your UILabel with constraints (top:0,lead:0,trail:0,height:<=UIView)
This helps you to adjust your label height according to their content which help to anchor label alignment on top.

NSTextView not resizing properly after setFrameSize

In an NSTextView subclass I have created, I want to resize the height of the view to the height of the text within it. To execute this, I used apple's recommended procedure of counting lines within a text view:
private func countln() -> Int {
var nlines: Int
var index: Int
var range = NSRange()
let nGlyphs = lManager.numberOfGlyphs
for (nlines = 0, index = 0; index < nGlyphs; nlines++) {
lManager.lineFragmentRectForGlyphAtIndex(index, effectiveRange: &range)
index = NSMaxRange(range);
}
return nlines
}
This method works as expected and returns the correct number of lines in the text view. The issue lies in the resizing of the view, which I inserted into the delegate method that is called on text change:
func textDidChange(notification: NSNotification) {
let newHeight = CGFloat(28 * countln())
let ogHeight = self.frame.height
self.setFrameSize(NSSize(width: self.frame.width, height: newHeight))
self.setFrameOrigin(NSPoint(x: self.frame.origin.x, y: (self.frame.origin.y - self.frame.height) + ogHeight))
Swift.print(frame.height)
}
The setFrameSize variable function resizes the height of the view based not the number of lines in the view (multiplied by a constant that is more-or-less the height of each line of text). Everything works perfectly until immediately after the change of height is made, when the text view's height changes to an unanticipated incorrect height. I presume there is an issue with the frequent redrawing of the view in relation to the way I am resizing it. Any help on how to solve this issue of incorrect height resizing is greatly appreciated.
Thanks in advance.