I have a cell with description(UILabel) and two buttons after the description I use this to get the description text height
let constraintRect = CGSize(width: maxWidth, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(boundingBox.height)
but the problem is when I do any action on the buttons with a huge description cell the actions is not trigged this is a ver annoying a problem and tried a lot of solutions but nothing works any help ?
this is where I set the height for cells
override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
if self.items.count < 1 {
return CGSize(width: (self.view.bounds.width - 16), height: 200)
}
if indexPath.item < self.items.count {
let item = self.items[indexPath.item]
var h = CGFloat(0)
h = item["description"].stringValue.height(with: (self.collectionView.bounds.width - 44), font: UIFont(name: "Helvetica", size: self.contentLabelFontSize)!)
self.descriptionHeight = h
if item["images"].isEmpty == false {
if item["images"][0]["original"].stringValue != "" {
let hi = 300
let im_h = CGFloat(hi)
h += im_h
}
}
if self.type == "events" {
h += 260
}
else if item["type"].stringValue == "event" {
h += 250
}
else {
h += 170
}
let size = CGSize(width: (self.view.bounds.width - 16), height: h)
return size
}
return CGSize(width: (self.view.bounds.width - 16), height: 50)
}
And this is where is set the height of label at the PostCell class :
self.contentLabel.frame = CGRect(x: 10, y: 110, width: (self.bounds.width - 20), height: (self.bounds.height - self.imageHeight - 170))
Related
Currently I am using UICollectionView to make a grid of items. I want to draw grid lines as in the screenshot that works on both iPhones and iPads.
On iPad, either the label or the grid line is not correctly placed. On iPhone, the adjustments worked fine.
override func viewDidLoad() {
self.automaticallyAdjustsScrollViewInsets = false
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 1.0
layout.minimumInteritemSpacing = 0.0
collectionView.setCollectionViewLayout(layout, animated: false)
collectionView.layoutIfNeeded()
collectionView.contentOffset = CGPoint.zero
}
private let foundationScreenElems = ["ABOUT US", "CHAIRMAN'S MESSAGE", "MESSAGE OF CEO", "OUR GOALS", "PILLARS", "VISION AND MISSION", "QUALITY POLICY", "TERMS & CONDITIONS"]
var foundationGridLayers: [CALayer] = []
func drawHorizontalLines(rowsCount: Int) {
var pos = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3.5 - 0.5 : collectionView.frame.width / 2.5 - 0.5
var i = 1
while i < rowsCount {
if (viewId == .foundation && foundationGridLayers.count < 4) {
let layer = CALayer()
if (i == 3 && UIDevice.current.userInterfaceIdiom == .pad) {
pos = pos + 10
}
layer.frame = CGRect(x: 10, y: CGFloat(i) * pos, width: self.view.frame.width - 20, height: 1.0)
layer.backgroundColor = gridColor
foundationGridLayers.append(layer)
collectionView.layer.addSublayer(layer)
}
i += 1
}
}
func drawVerticalLine(rowsCount: Int) {
let top = getTop()
let pos = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3.5 - 0.5 : collectionView.frame.width / 2.5 - 0.5
let width = self.view.frame.width / 2
let layer = CALayer()
if (UIDevice.current.userInterfaceIdiom == .pad) {
layer.frame = CGRect(x: width, y: top + 10, width: 1.0, height: pos * CGFloat(rowsCount) + 30)
} else {
layer.frame = CGRect(x: width, y: top - 10, width: 1.0, height: pos * CGFloat(rowsCount) - 10)
}
layer.backgroundColor = gridColor
collectionView.layer.addSublayer(layer)
}
func getTop() -> CGFloat {
let pos = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3 - 0.5 : collectionView.frame.width / 2.5 - 0.5
let height = collectionView.frame.height
let rowCount: CGFloat = CGFloat(foundationScreenElems.count / 2)
let emptySpace = height - (pos * rowCount)
return UIDevice.current.userInterfaceIdiom == .pad ? emptySpace / 2 : emptySpace / 4
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let top = getTop()
return UIEdgeInsets(top: top, left: 0, bottom: 10, right: 0)
}
extension GridViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width / 2 - 0.5
let height = UIDevice.current.userInterfaceIdiom == .pad ? collectionView.frame.width / 3 - 0.5 : collectionView.frame.width / 2.5 - 0.5
return CGSize(width: width, height: height)
}
}
On iPad, the Pillars rows is not aligned properly or the grid line is not at right position, but it works fine on iPhone.
Is there a way to draw grid lines independent of devices?
You can try creating 2 different types of custom cell, One for left and one for right with labels filled with border color to look.
You can have a look at this sample
In the collection view controller you can check for the cell and create the appropriate cell.
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
var reuseIdentifier :String = "leftCell"
let itemsPerRow = 2;
let column = indexPath.item % itemsPerRow;
if(column == 1)
{
reuseIdentifier = "rightCell"
}
let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
// Configure the cell
return cell
}
Make the cell fit any device screen
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/3);
}
Add appropriate constraints so that the border labels always maintain a zero margin to the right
Increase or decrease font size smoothly whenever user resize label using pinch gesture on it.
Note
Without compromising quality of font
Not only transforming the scale of UILabel
With support of multiline text
Rotation gesture should work proper with pinch gesture
Reference: SnapChat or Instagram Text Editor tool
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: UIFont(name: font.fontName, size: font.pointSize)!], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: UIFont(name: font.fontName, size: font.pointSize)!], context: nil)
return ceil(boundingBox.width)
}
}
func resizeLabelToText(textLabel : UILabel)
{
let labelFont = textLabel.font
let labelString = textLabel.text
let labelWidth : CGFloat = labelString!.width(withConstrainedHeight: textLabel.frame.size.height, font: labelFont!)
let labelHeight : CGFloat = labelString!.height(withConstrainedWidth: labelWidth, font: labelFont!)
textLabel.frame = CGRect(x: textLabel.frame.origin.x, y: textLabel.frame.origin.y, width: labelWidth, height: labelHeight)
textLabel.font = labelFont
}
func pinchedRecognize(_ pinchGesture: UIPinchGestureRecognizer) {
guard pinchGesture.view != nil else {return}
if (pinchGesture.view is UILabel) {
let selectedTextLabel = pinchGesture.view as! UILabel
if pinchGesture.state == .began || pinchGesture.state == .changed {
let pinchScale = round(pinchGesture.scale * 1000) / 1000.0
if (pinchScale < 1) {
selectedTextLabel.font = selectedTextLabel.font.withSize(selectedTextLabel.font.pointSize - pinchScale)
}
else {
selectedTextLabel.font = selectedTextLabel.font.withSize(selectedTextLabel.font.pointSize + pinchScale)
}
resizeLabelToText(textLabel: selectedTextLabel)
}
}
}
I solved the problem with following code which is working fine with every aspect which are mentioned in question, similar to Snapchat and Instagram:
var pointSize: CGFloat = 0
#objc func pinchRecoginze(_ pinchGesture: UIPinchGestureRecognizer) {
guard pinchGesture.view != nil else {return}
let view = pinchGesture.view!
if (pinchGesture.view is UILabel) {
let textLabel = view as! UILabel
if pinchGesture.state == .began {
let font = textLabel.font
pointSize = font!.pointSize
pinchGesture.scale = textLabel.font!.pointSize * 0.1
}
if 1 <= pinchGesture.scale && pinchGesture.scale <= 10 {
textLabel.font = UIFont(name: textLabel.font!.fontName, size: pinchGesture.scale * 10)
resizeLabelToText(textLabel: textLabel)
}
}
}
func resizeLabelToText(textLabel : UILabel) {
let labelSize = textLabel.intrinsicContentSize
textLabel.bounds.size = labelSize
}
Call following method every time after UILabel size changes.
func labelSizeHasBeenChangedAfterPinch(_ label:UILabel, currentSize:CGSize){
let MAX = 25
let MIN = 8
let RATE = -1
for proposedFontSize in stride(from: MAX, to: MIN, by: RATE){
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attribute = [NSAttributedString.Key.font:UIFont.systemFont(ofSize: CGFloat(proposedFontSize))]
// let context = IF NEEDED ...
let rect = NSString(string: label.text ?? "").boundingRect(with: currentSize, options: options, attributes: attribute, context: nil)
let labelSizeThatFitProposedFontSize = CGSize(width: rect.width , height: rect.height)
if (currentSize.height > labelSizeThatFitProposedFontSize.height) && (currentSize.width > labelSizeThatFitProposedFontSize.width){
DispatchQueue.main.async {
label.font = UIFont.systemFont(ofSize: CGFloat(proposedFontSize))
}
break
}
}
}
you can try:
1 - Set maximum font size for this label
2 - Set line break to Truncate Tail
3 - Set Autoshrink to Minimum font size (minimum size)
I have a collectionView where I am using the following to manage rotation of the cells:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
DispatchQueue.main.async {
coordinator.animate(alongsideTransition: nil, completion: { context in
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.collectionViewLayout.finalizeCollectionViewUpdates()
})
}
}
I see that the size change on rotation is very slow ( takes ~ 3- 4 seconds ). Is there a better way to get smoother rotation transition? I have dynamic cell sizing from sizeForItemAtIndexPath, code is :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var width = CGFloat()
if traitCollection.userInterfaceIdiom == .phone {
width = collectionView.frame.size.width - 2.0 * margin
}
else if traitCollection.userInterfaceIdiom == .pad {
if UIDevice.current.orientation.isLandscape {
width = floor(collectionView.frame.size.width - 14.0 * margin)
}
else {
width = floor(collectionView.frame.size.width - 6.0 * margin)
}
}
let post = self.postListForLoggedInUserFromRealm?.posts[(indexPath as NSIndexPath).item]
if let description = post?.description {
let rect = NSString(string: description).boundingRect(with: CGSize(width: view.frame.width * 0.8, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], attributes: [.font: UIFont(name: "Avenir", size: CGFloat(titleFont))!], context: nil)
var knownHeight : CGFloat = 64
if(post?.imageURL != nil && post?.imageURL != "") {
guard let imageURL = NSURL(string: (post?.imageURL!)!) else { return CGSize(width: width, height: rect.height + knownHeight )}
if let imageSource = CGImageSourceCreateWithURL((imageURL as CFURL), nil) {
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as! CGFloat
let imageMaxHeight: CGFloat = (CIConstants.DeviceType.DeviceTypePhone == deviceType ) ? 400 : 700
let imageHeight = pixelHeight > imageMaxHeight ? imageMaxHeight : pixelHeight
knownHeight = knownHeight + CGFloat(imageHeight)
return CGSize(width: width, height: rect.height + knownHeight)
}
}
}
return CGSize(width: width, height: rect.height + knownHeight)
}
else if(post?.imageURL != nil && post?.imageURL != "") {
var knownHeight : CGFloat = 10 + 44 + 8 + 44 + 10
guard let imageURL = NSURL(string: (post?.imageURL!)!) else { return CGSize(width: width, height: knownHeight + 70)}
if let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, nil) {
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as! CGFloat
let imageMaxHeight: CGFloat = (CIConstants.DeviceType.DeviceTypePhone == deviceType ) ? 400 : 700
let imageHeight = imageMaxHeight > 700 ? imageMaxHeight : pixelHeight
knownHeight = knownHeight + CGFloat(imageHeight)
}
}
return CGSize(width: width, height: knownHeight)
}
return CGSize(width: 850, height: 70)
}
Any suggestions would be truly appreciated! Thanks for your time :)
EDIT: When I use this code, the delay is gone but I lose the margins ( on left and right of collectionView) in Portrait mode.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
DispatchQueue.main.async {
coordinator.animate(alongsideTransition: { context in
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.collectionViewLayout.finalizeCollectionViewUpdates()
})
}
super.viewWillTransition(to: size, with: coordinator)
}
I have a horizontal collection view and it shows 5 cells at a time and i have adjusted that through sizeForItemAt :
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.numberCollectionView?.bounds.size.width)!/5 - 3, height: (self.numberCollectionView?.bounds.size.width)!/5 - 3 )
}
But these 5 cells would have different width and height and there is a ratio to it. So the center item would take 40% of the whole size, the 2nd and 3rd item would take 20% of the size and the 1st and 5th would take 10% of the size. According to this ratio items size would get change on scroll. These items are all Label.
Edit:
unlike the suggested question and other questions in stackoverflow in my case there is only one row and i have custom UICollectionViewFlowLayout to handle the scrolling by cell
UICollectionViewFlowLayout to handle the scrolling by cell:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
Then to find the center cell of the CollectionView:
private func findCenterIndex() -> Int {
let center = self.view.convert(numberCollectionView.center, to: self.numberCollectionView)
let row = numberCollectionView!.indexPathForItem(at: center)?.row
guard let index = row else {
return 0
}
self.rating = index
return index
}
under scrollViewDidScrolltransformed cell:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
transformCell()
}
private func transformCell() {
let row = findCenterIndex()
var offset : Float = 0.0
for cell in numberCollectionView.visibleCells as! [VotingCell] {
if row == numberCollectionView.indexPath(for: cell)?.row {
offset = 1.1
cell.label.font = UIFont.boldSystemFont(ofSize: 28.0)
cell.label.textColor = UIColor.black
cell.alpha = 1.0
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 1 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 1
{
offset = 0.8
cell.label.font = UIFont(name: cell.label.font.fontName, size: 26.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 2 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 2 {
offset = 0.7
cell.label.font = UIFont(name: cell.label.font.fontName, size: 20.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else
{
offset = 0.00
}
cell.transform = CGAffineTransform.identity
cell.transform = CGAffineTransform(scaleX: CGFloat(offset), y: CGFloat(offset))
}
}
also called transformCell() from viewDidLayoutSubviews for initial load.
I've created a collectionView where i have 3 cells the first cells has to fill first row and the third should fill second row however i can't seem to figure out how to do this, since everytime i try to do in sizeForItemAt i get that the collectionView frame is (0, 0, 0, 0).
Here is what i have at the moment
class FlowLayout: UICollectionViewFlowLayout {
var numberOfCells: Int!
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
func setupLayout() {
minimumInteritemSpacing = 1
minimumLineSpacing = 1
}
override var itemSize: CGSize {
set {
}
get {
let numberOfColumns: CGFloat = 2
let itemWidth = (self.collectionView!.frame.width - (numberOfColumns - 1)) / numberOfColumns
let itemHeight = self.collectionView!.frame.height / 2
return CGSize(width: itemWidth, height: itemHeight)
}
}
}
illustration of what i want
setting up collectionView in my view
func createCollectionView() {
let layout = FlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.numberOfCells = 2
collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.backgroundColor = UIColor.white
collectionView.isScrollEnabled = false
self.topWrapperView.addSubview(collectionView)
collectionView.snp.makeConstraints { (make) -> Void in
make.left.equalTo(topWrapperView).offset(0)
make.top.equalTo(topWrapperView).offset(0)
make.bottom.equalTo(topWrapperView).offset(0)
make.right.equalTo(topWrapperView).offset(0)
}
}
I had a similar situation except it was the first cell that I wanted 2 cells in first row, with 3 each row after. Used it for a main profile pic and bio in a collection view of images. I successfully implemented it using sizeForItemAtIndexPath with default UICollectionViewDelegateFlowLayout.
Here is something that should work for your situation:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
let collectionViewWidth = collectionView.frame.width - (insets.left + insets.right + 1)
let collectionViewHeight = collectionView.frame.height - (insets.top + insets.bottom)
switch indexPath.row {
case 0, 1:
return CGSize(width: collectionViewWidth / 2, height: collectionViewHeight / 2)
default:
return CGSize(width: collectionViewWidth, height: collectionViewHeight / 2)
}
}
You can change what you divide height by to get desired height.
This is what I wrote for my situation, figured I'd post it incase it can help you or anyone else who reads it. If you want it to change with rotation, use viewWillTransition and set a bool flag with a didSet listener:
var isPortraitOrientation: Bool = true {
didSet {
if oldValue != isPortraitOrientation {
collectionView?.collectionViewLayout.invalidateLayout()
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
isPortraitOrientation = (size.width - size.height) <= 0 ? true : false
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
let collectionViewWidth = collectionView.frame.width - (insets.left + insets.right + 1)
let collectionViewHeight = collectionView.frame.height - (insets.top + insets.bottom)
switch indexPath.row {
case 0:
if textViewSelected {
return isPortraitOrientation ? CGSize(width: collectionViewWidth, height: collectionViewHeight / 2) : CGSize(width: collectionViewWidth, height: collectionViewWidth / 3)
} else {
return isPortraitOrientation ? CGSize(width: collectionViewWidth / 2, height: collectionViewWidth / 2) : CGSize(width: collectionViewWidth / 2, height: collectionViewHeight / 1.5)
}
case 1:
return isPortraitOrientation ? CGSize(width: collectionViewWidth / 2, height: collectionViewWidth / 2) : CGSize(width: collectionViewWidth / 2, height: collectionViewHeight / 1.5)
default:
return isPortraitOrientation ? CGSize(width: collectionViewWidth / 3, height: collectionViewWidth / 3) : CGSize(width: collectionViewWidth / 5, height: collectionViewWidth / 5)
}
}
EDIT:
based on your additional code, instead of doing CGRect.zero for the collectionView frame use topWrapperView.bounds.