Update UICollectionView header height based on headers UITextview content - ios

I have a UICollectionViewController header cell that displays information for a business.
A business may or may not have a description.
In my header cell I set a business object, and then set all the text for the labels etc. In here I also try calculating the required text field height based off of this post
What I need is to actually set my header cells height dynamically in the UICollectionViewController where it is used.
BusinessDetailHeader class:
var maxTextViewHeight: CGFloat = 0
var business: Business? {
didSet {
guard let imageUrl = business?.logoUrl else {return}
let url = URL(string: imageUrl)
logoView.kf.setImage(with: url)
headerImageView.kf.setImage(with: url)
guard let businessName = business?.name else {return}
guard let address = business?.address else {return}
guard let hoursOfOperation = business?.hoursOfOperation else {return}
guard let phoneNumber = business?.phoneNumber else {return}
businessNameLabel.text = businessName
addressLabel.text = address
hoursofOperationLabel.text = hoursOfOperation
phoneNumberLabel.text = phoneNumber
if let description = business?.description {
detailsTextField.text = description
let amountOfLinesToBeShown:CGFloat = 6
if let height = detailsTextField.font?.lineHeight {
maxTextViewHeight = height * amountOfLinesToBeShown
}
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
layoutViews()
}
fileprivate func layoutViews() {
addSubview(headerImageView)
headerImageView.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 130)
//ADJUST DETAIL HEIGHT HERE BASED ON CALCULATED HEIGHT FOR TEXTVIEW
addSubview(detailView)
detailView.anchor(top: headerImageView.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 200+maxTextViewHeight)
detailView.addSubview(businessNameLabel)
businessNameLabel.anchor(top: logoView.bottomAnchor, left: nil, bottom: nil, right: nil, paddingTop: 4, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
businessNameLabel.centerXAnchor.constraint(equalTo: detailView.centerXAnchor).isActive = true
detailView.addSubview(detailsTextField)
detailsTextField.anchor(top: businessNameLabel.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: maxTextViewHeight)
detailView.addSubview(addressLabel)
addressLabel.anchor(top: detailsTextField.bottomAnchor, left: nil, bottom: nil, right: nil, paddingTop: 4, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
addressLabel.centerXAnchor.constraint(equalTo: detailView.centerXAnchor).isActive = true
detailView.addSubview(hoursofOperationLabel)
hoursofOperationLabel.anchor(top: addressLabel.bottomAnchor, left: nil, bottom: nil, right: nil, paddingTop: 4, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
hoursofOperationLabel.centerXAnchor.constraint(equalTo: detailView.centerXAnchor).isActive = true
detailView.addSubview(phoneNumberLabel)
phoneNumberLabel.anchor(top: hoursofOperationLabel.bottomAnchor, left: nil, bottom: nil, right: nil, paddingTop: 4, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
phoneNumberLabel.centerXAnchor.constraint(equalTo: detailView.centerXAnchor).isActive = true
}
The height is updated properly with no issues but sometimes when the height is too big, the text and all the following elements overflow out of the header cell. I think this has to do with how I address the height of the cell in my UICollecitonViewController.
I set it's height like this:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: view.frame.width, height: 330)
}
The height of 330 here needs to be dynamic. Is this possible? Should I be setting the height property like this? I've seen posts like this one that try addressing this, but the solutions don't seem like they are the best.

Please check this link, it will be really helpful in how to make UIcollectionView header dynamic height?. Basically you need to set autolayout constraint according to your requirement; Need to call setNeedlayout() in referenceSizeForHeaderInSection (collectionViewFlowLayout delegate) method.

Related

UIView loses constrains when animated

I'm trying to imitate the controller push transition but for views. But it loses constrains the moment I add the constrains of the new view even before the animation occurs. For a reproduction of issue scroll to the gif at the bottom.
Here's a snippet of the relevant prosperities: (the rest is merely a bunch of labels and buttons)
//MARK: Properities
var credential: AuthCredential!
let user = Auth.auth().currentUser
var userAuthenticated = false
let controllerTitle = "Update Email"
private var timer: Timer?
lazy var credentialVerificationView: UIView = {
let centerView = UIView()
centerView.frame.size.height = self.view.frame.height
centerView.frame.size.width = self.view.frame.width
centerView.backgroundColor = UIColor(named: "blankSlate")
return centerView
}()
lazy var emailUpdateView: UIView = {
let centerView = UIView()
centerView.frame.size.height = self.view.frame.height
centerView.frame.size.width = self.view.frame.width
centerView.backgroundColor = UIColor.red
return centerView
}()
I'm using transform to move the current view from the middle of the screen to -view.frame.width and the second view from -view.frame.width to the middle of the screen. func is:
private func startAutoScrollCardsCollectionView() {
/// This method start timer and fire it immediately
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [unowned self] _ in
UIView.animate(withDuration: 1) {
self.credentialVerificationView.transform = CGAffineTransform(translationX: -view.frame.width, y: 0)
self.emailUpdateView.transform = CGAffineTransform(translationX: -view.frame.width, y: 0)
}
}
}
And this is the ugly bit:
//MARK: authenticateUserAndConfigureView
/// the purpose of this func is merely sentimental; as long as its there it stops me from ever writing UI ever again
func authenticateUserAndConfigureView() {
// title = controllerTitle
view.backgroundColor = UIColor(named: "blankSlate")
emailTextField.textContentType = .username
emailTextField.keyboardType = .emailAddress
emailTextField.autocorrectionType = .no
emailTextField.autocapitalizationType = .none
emailTextField.spellCheckingType = .no
print("userAuthenticated = \(userAuthenticated)")
if userAuthenticated == true {
loginToContinueLabel.text = "Update Email"
infoLabel.text = currentEmailLabel1.text
errorLabel.text?.removeAll()
startAutoScrollCardsCollectionView()
emailUpdateView.addSubview(loginToContinueLabel)
loginToContinueLabel.anchor(top: emailUpdateView.topAnchor,
left: nil, bottom: nil,
right: nil,
paddingTop: 200,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
width: 300, height: 50)
loginToContinueLabel.centerXAnchor.constraint(equalTo: emailUpdateView.centerXAnchor).isActive = true
emailUpdateView.addSubview(infoLabel)
infoLabel.anchor(top: loginToContinueLabel.bottomAnchor,
left: emailUpdateView.leftAnchor,
bottom: nil,
right: emailUpdateView.rightAnchor,
paddingTop: 24,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 50)
infoLabel.centerXAnchor.constraint(equalTo: emailUpdateView.centerXAnchor).isActive = true
emailUpdateView.addSubview(emailContainerView)
emailContainerView.anchor(top: infoLabel.bottomAnchor,
left: emailUpdateView.leftAnchor,
bottom: nil,
right: emailUpdateView.rightAnchor,
paddingTop: 15,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 50)
emailUpdateView.centerXAnchor.constraint(equalTo: emailUpdateView.centerXAnchor).isActive = true
emailUpdateView.addSubview(verifyLabel)
verifyLabel.anchor(top: emailContainerView.bottomAnchor,
left: emailUpdateView.leftAnchor,
bottom: nil,
right: emailUpdateView.rightAnchor,
paddingTop: 2,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 20)
emailUpdateView.centerXAnchor.constraint(equalTo: emailUpdateView.centerXAnchor).isActive = true
emailUpdateView.addSubview(errorLabel)
errorLabel.anchor(top: verifyLabel.bottomAnchor,
left: nil,
bottom: nil,
right: nil,
paddingTop: 1,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
width: 0, height: 0)
errorLabel.centerXAnchor.constraint(equalTo: emailUpdateView.centerXAnchor).isActive = true
emailUpdateView.addSubview(updateEmailButton)
updateEmailButton.anchor(top: errorLabel.bottomAnchor,
left: emailUpdateView.leftAnchor,
bottom: nil,
right: emailUpdateView.rightAnchor,
paddingTop: 16,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 50)
updateEmailButton.centerXAnchor.constraint(equalTo: emailUpdateView.centerXAnchor).isActive = true
view.addSubview(emailUpdateView)
emailUpdateView.anchor(top: nil,
left: view.leftAnchor,
bottom: nil,
right: view.rightAnchor,
paddingTop: 0,
paddingLeft: +view.frame.width,
paddingBottom: 0,
paddingRight: -view.frame.width,
width: view.frame.width,
height: 0)
} else {
credentialVerificationView.addSubview(loginToContinueLabel)
loginToContinueLabel.anchor(top: credentialVerificationView.topAnchor,
left: nil, bottom: nil,
right: nil,
paddingTop: 200,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
width: 300, height: 50)
loginToContinueLabel.centerXAnchor.constraint(equalTo: credentialVerificationView.centerXAnchor).isActive = true
credentialVerificationView.addSubview(infoLabel)
infoLabel.anchor(top: loginToContinueLabel.bottomAnchor,
left: credentialVerificationView.leftAnchor,
bottom: nil,
right: credentialVerificationView.rightAnchor,
paddingTop: 24,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 50)
infoLabel.centerXAnchor.constraint(equalTo: credentialVerificationView.centerXAnchor).isActive = true
credentialVerificationView.addSubview(passwordContainerView)
passwordContainerView.anchor(top: infoLabel.bottomAnchor,
left: credentialVerificationView.leftAnchor,
bottom: nil,
right: credentialVerificationView.rightAnchor,
paddingTop: 16,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 50)
passwordContainerView.centerXAnchor.constraint(equalTo: credentialVerificationView.centerXAnchor).isActive = true
credentialVerificationView.addSubview(loginButton)
loginButton.anchor(top: passwordContainerView.bottomAnchor,
left: credentialVerificationView.leftAnchor,
bottom: nil, right:
credentialVerificationView.rightAnchor,
paddingTop: 24,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 50)
loginButton.centerXAnchor.constraint(equalTo: credentialVerificationView.centerXAnchor).isActive = true
credentialVerificationView.addSubview(needHelpButton)
needHelpButton.anchor(top: loginButton.bottomAnchor,
left: loginButton.leftAnchor,
bottom: nil,
right: nil,
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
width: 0, height: 0)
credentialVerificationView.addSubview(errorLabel)
errorLabel.anchor(top: needHelpButton.bottomAnchor,
left: credentialVerificationView.leftAnchor,
bottom: nil,
right: credentialVerificationView.rightAnchor,
paddingTop: 15,
paddingLeft: 32,
paddingBottom: 0,
paddingRight: 32,
width: 0, height: 0)
errorLabel.centerXAnchor.constraint(equalTo: credentialVerificationView.centerXAnchor).isActive = true
self.view.addSubview(credentialVerificationView)
credentialVerificationView.anchor(top: view.topAnchor,
left: view.leftAnchor,
bottom: view.bottomAnchor,
right: view.rightAnchor,
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
width: 0, height: 0)
credentialVerificationView.center = self.view.center
}
}
}

How to wrap long text correctly?

In my application users can post comments on posts. Everything works fine however the lines do not wrap when creating a long text thus I get this
numberOfLines is set to = 0
Here is the code in the commentCell setting the constraints
let commentLabel: ActiveLabel = {
let label = ActiveLabel()
label.font = UIFont.systemFont(ofSize: 13)
label.numberOfLines = 0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(profileImageView)
profileImageView.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 8, paddingLeft: 8, paddingBottom: 0, paddingRight: 0, width: 40, height: 40)
profileImageView.layer.cornerRadius = 40 / 2
addSubview(optionsButton)
optionsButton.anchor(top: topAnchor, left: nil, bottom: nil, right: rightAnchor, paddingTop: 12, paddingLeft: 0, paddingBottom: 0, paddingRight: 8, width: 20, height: 20)
addSubview(commentLabel)
commentLabel.anchor(top: topAnchor, left: profileImageView.rightAnchor, bottom: bottomAnchor, right: optionsButton.leftAnchor, paddingTop: 15, paddingLeft: 5, paddingBottom: 15, paddingRight: 8, width: 0, height: 0)
addSubview(commentTimeLabel)
commentTimeLabel.anchor(top: commentLabel.bottomAnchor, left: commentLabel.leftAnchor, bottom: nil, right: nil, paddingTop: 3, paddingLeft: 1, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
optionsButton.isEnabled = true
optionsButton.isUserInteractionEnabled = true
commentLabel.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
And here is the CommentViewController code that should be resizing the cell for me but it doesn't. I've used this before in the past so I'm not sure if I am missing anything
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
let dummyCell = CommentCell(frame: frame)
dummyCell.comment = comments[indexPath.item]
dummyCell.layoutIfNeeded()
let targetSize = CGSize(width: view.frame.width, height: 1000)
let estimatedSize = dummyCell.systemLayoutSizeFitting(targetSize)
let height = max(40 + 8 + 8, estimatedSize.height)
return CGSize(width: view.frame.width, height: height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 15
}
You need to set
commentLabel.lineBreakMode = .byWordWrapping
By default it's set to .byTruncatingTail.
Also you may need to set .numberOfLines if you want to limit text to certain number of lines
After revisiting this issue, the problem was my constraints.
on
commentLabel.anchor
I changed it to
bottom: commentTimeLabel.topAnchor
and
optionsButton.anchor
changed bottom to
bottom: bottomAnchor

View's Frame get called twice and causes clipping bug

I have a base UIView that has a visualEffectView as it's subview. I set the visualEffectView's view frame.height as 80. When presenting to ViewController, I want to reset its height to 120. Currently, when presented, the height is initially 120 but then quickly clips back down to 80. Shouldn't my frame in my ViewController override the frame set in the UIView? My frame in the ViewController gets called first but later it gets called from the UIView, causing the clipping bug.
class CardView: UIView {
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
addSubview(visualEffectView)
}
override func layoutSubviews() {
super.layoutSubviews()
visualEffectView.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 80)
}
}
class ViewController: ViewController {
let cardView: CardView = {
let cv = CardView()
cv.imageView.clipsToBounds = true
cv.layer.cornerRadius = 18
return cv
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let superviewFrame = view.frame.height
cardView.frame = CGRect(x: 10, y: superviewFrame / 8, width: view.frame.width - 20, height: superviewFrame - (superviewFrame / 5))
cardView.visualEffectView.anchor(top: cardView.topAnchor, left: cardView.leftAnchor, bottom: nil, right: cardView.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 120)
}
}
try moving this code
visualEffectView.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 80)
from override func layoutSubviews() { to override init(frame: CGRect) {
after super.init()
You may need a guard condition and if there is a constraint, just pass.
override func layoutSubviews() {
super.layoutSubviews()
guard visualEffectView.constraintsAffectingLayout(for: .vertical).count == 0 else {return}. // Here
visualEffectView.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 80)
}

Constraint Issue

So I have a cell that is setup to have a collectionView inside of it. The height for that cell is given in this function.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.section == 0 {
return CGSize(width: view.frame.width, height: 300)
}
return CGSize(width: view.frame.width, height: 280)
}
Inside the class for this cell I create a collectionView and again create a collection of cells inside of it.
#objc func setupViews(){
backgroundColor = .red
addSubview(categoryCollectionView)
addSubview(sectionNameLabel)
sectionNameLabel.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 14, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
categoryCollectionView.anchor(top: sectionNameLabel.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
categoryCollectionView.delegate = self
categoryCollectionView.dataSource = self
categoryCollectionView.showsHorizontalScrollIndicator = false
categoryCollectionView.register(CategoryEventCell.self, forCellWithReuseIdentifier: cellID)
}
Each cell has a background image and a title that is below the image. However the text is always somewhat cut off. My current constraints are here
#objc func setupViews(){
backgroundColor = .clear
setCellShadow()
addSubview(backgroundImageView)
addSubview(eventNameLabel)
backgroundImageView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 9, paddingRight: 0, width: 0, height: 0)
eventNameLabel.anchor(top: backgroundImageView.bottomAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
How would I change my constraints or text/element size to make sure everything fits?
My current view looks like so
You are pinning the image to the bottom of its cell, and the label has no say in how to fit in. Instead you should pin the image's to the label's top, and the label's bottom to the cell's bottom. Like this in the visual format language: [image]-[label]-|. You also need to edit the backgroundImageView's content compression resistance.
Here is the fixed version where the label causes the image's height to shrink upwards to fit in:
#objc func setupViews(){
backgroundColor = .clear
setCellShadow()
addSubview(backgroundImageView)
addSubview(eventNameLabel)
// ensure the image compresses, not the eventNameLabel, by lowering its priority
backgroundImageView.setContentCompressionResistancePriority(UILayoutPriority(600), for: .vertical)
backgroundImageView.anchor(top: topAnchor, left: leftAnchor, bottom: eventNameLabel.topAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
eventNameLabel.anchor(top: backgroundImageView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}

Add UIView behind iPhone X status bar

I have an App, that doesn't have a UINavigationController and it has a UICollectionView with some cells as its feed.
To avoid the cells appearing behind the status bar, I added a UIView behind it.
Before iPhone X it was pretty simple to do this, as the following:
let v = UIView()
v.backgroundColor = .white
view.addSubview(v)
v.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
By doing so, a tiny white layer would appear behind it as expected.
(this anchor method is from an extension, not language-default).
How can I achieve this for iPhone X ?
So far I tried the following code:
if #available(iOS 11, *) {
v.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, bottom: nil, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
let window = UIWindow(frame: UIScreen.main.bounds)
print("window.safeAreaInsets.top:", window.safeAreaInsets.top)
if window.safeAreaInsets.top > CGFloat(0.0) {
print("iPhone X")
v.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, bottom: nil, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop:-44, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: window.safeAreaInsets.top)
} else {
v.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
}
}
but it doesn't work.
What's the best approach in this case?
Thank you
You have to tell your collectionView to adjust the insets:
if #available(iOS 11, *) {
collectionView.contentInsetAdjustmentBehavior = .scrollableAxes
}
This will make sure your content will be inset enough so it's not overlapped by the iPhone X's status bar.

Resources