Animate UITableViewCell height - ios

I have a container view inside a UITableViewCell which is attached to the 4 edges of the contentview with a padding. I can resize the containerView by pinching changing the height and padding. All the constraints update correctly but the contentView height doesn't change. Here are the constraints of the containerView.
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerViewLeadingAnchor =
containerView.leadingAnchor .constraint(equalTo: contentView.leadingAnchor, constant: 10)
containerViewTopAnchor =
containerView.topAnchor .constraint(equalTo: contentView.topAnchor, constant: 10)
containerViewBottomAnchor =
containerView.bottomAnchor .constraint(equalTo: contentView.bottomAnchor, constant: -10)
containerViewHeightAnchor =
containerView.heightAnchor .constraint(equalToConstant: 146)
containerViewWidthAnchor =
containerView.widthAnchor .constraint(equalToConstant: UIScreen.main.bounds.width - 20)
containerViewLeadingAnchor.isActive = true
containerViewTopAnchor.isActive = true
containerViewBottomAnchor.isActive = true
containerViewHeightAnchor.isActive = true
containerViewWidthAnchor.isActive = true
And here is how i update them inside the UITableViewCell
containerViewLeadingAnchor.constant = containerPaddingForComfortable - (containerPaddingForComfortable * scaleRatio)
containerViewTopAnchor.constant = containerPaddingForComfortable - (containerPaddingForComfortable * scaleRatio)
containerViewWidthAnchor.constant = (UIScreen.main.bounds.width - 20) + (containerPaddingForComfortable*2 * scaleRatio)
containerViewBottomAnchor.constant = -containerPaddingForComfortable + (containerPaddingForComfortable * scaleRatio)
containerView.layer.cornerRadius = containerCornerRadius - (containerCornerRadius * scaleRatio)
self.layoutIfNeeded()

It is not enough to simply modify view frame or constraints to update the size of content view.
In table view the cell content view is controlled by cell which is controlled by table view or rather it's data source or delegate. In your case you need to have cell height set to automatic dimensions so constraints are used internally to determine cell size. Once that is done you will need to call beginUpdates() and endUpdates() on the table view. So something like:
func resizeMyCell(_ cell: MyCell, to height: CGFloat) {
cell.heightConstraint.constant = height
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
Although it might seem strange the two calls will invalidate sizes and refresh table view cell sizes and positions. By the way changing cell size usually means also moving rest of the cells beneath the resized one.

Related

UIView in UITableView is changing frame automatically

I have this UIView added to my UITableView:
self.tableView.addSubview(viewInfoPlus)
self.viewInfoPlus.frame = CGRect.init(x: kWidth - 193 - 32, y: 751, width: 193, height: 103)
The one signaled with the red arrow.
The UIView in the screenshot have the right frame but when I try to reload the UITableView or just some cell/cells using:
let indexPath = IndexPath.init(row: 0, section: TableSection.plus.rawValue)
UIView.setAnimationsEnabled(false)
self.tableView.beginUpdates()
tableView.reloadRows(at: [indexPath], with: .none)
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
The UIView automatically change the frame to the wrong one as you can see in this screenshot.
But when the user scroll to the top of UITableVIew the UIView automatically change again their frame to the right one.
Obviously I never changed the frame in the code.
Use Constraints instead of frame.
Setting Frames there is no guarantee when resize. Autolayout allows view that dynamically adjust to different size classes and positions. Check following example
Example:
viewInfoPlus.translatesAutoresizingMaskIntoConstraints = false
viewInfoPlus.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
viewInfoPlus.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 20).isActive = true
viewInfoPlus.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
viewInfoPlus.heightAnchor.constraint(equalToConstant: 103).isActive = true

Multi-line UILabel causing extra padding

I have a collectionView cell that has a label and an imageView. The imageView is 8 points from the top of the cell, the top of the label is 8 points from the bottom of the imageView, and the bottom of the label is 8 points from the bottom of the cell. The label wraps when it gets -10 point away from the right edge of the cell.
The text that goes into the label can span several lines. I use the below function inside the collectionView's sizeForItem to calculate the label's height:
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union([.usesLineFragmentOrigin, .usesFontLeading])
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
The cell expands correctly but the label has extra padding that is added to it and I cannot figure out how to get rid of it.
That is a multi line label size 22 that correctly wraps. I took a picture of it inside the 3D inspector. As you can see there is quite an extra bit of padding on the top and bottom above and below the label text. The label's 8 point spacing below the imageView is correct but the extra padding makes it look like 24 points of spacing.
The odd thing is even when I reduced the label's size to 16 the extra padding was still there.
How can I remove this padding?
The collectionView's sizeForItem where the estimatedLabelHeight function is called:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let profileImageViewHeight: CGFloat = 50 // imageView height is set to 50 inside the cell
let padding: CGFloat = 24 // 8 x 8 x 8 the vertical padding inside the cell between the titleLabel, the imageView, and the cell
// estimatedLabelHeight is called here
let titleLabelHeight = estimatedLabelHeight(text: "some very very long text...", width: view.frame.width - 20, font: UIFont.systemFont(ofSize: 22))
let totalHeightOfCell = titleLabelHeight + profileImageViewHeight + padding
return CGSize(width: view.frame.width, height: totalHeightOfCell)
}
The cell:
class MyCell: UICollectionViewCell{
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 22)
label.textColor = UIColor.black
label.textAlignment = .left
label.sizeToFit()
label.numberOfLines = 0
return label
}()
let profileImageView: UIImageView = {
// created it...
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(profileImageView)
addSubview(titleLabel)
profileImageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
profileImageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
titleLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 8).isActive = true
titleLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
titleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8).isActive = true
// I tried to set the label's height using the below but the same padding issue occured
// let titleLabelHeight = estimatedLabelHeight(text: titleLabel.text!, width: self.frame.width - 20, font: UIFont.systemFont(ofSize: 22))
// titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: titleLabelHeight).isActive = true
}
}
The problem was due to my own carelessness. #CSmith tip of measuring up the height in sizeForItem and the cell's frame is what helped me narrow it down.
Inside the project inside the collectionView cell I had the the imageView's bottom anchor set to 8 instead of -8 and inside the collectionView's sizeForItem I had the total height of 24. There was a conflict because since I had a 8 inside the cell it should've been 16 inside the collectionView and that mismatch was somehow stretching out the label. Once I corrected it and changed the imageView's bottom anchor -8 everything matched up and the label's padding issue was resolved.

Center UIImageView within a UIScrollView with a larger contentSize

I have a full screen scrollView, to which I add an imageView as subview. I want the imageView to be centered and scaled filling the scrollView's size (that is the screen size) at the beginning, but then to allow the user to scroll the image in both directions (vertical and horizontal) with equal offsets at left, right, top and bottom.
I mean: I've set the scroll view's contentSize to be CGSize(width: screenWidth + 200, height: screenHeight + 200), and if I run the app, I see that I am able to scroll those 200 pts of offset only to the right and to the bottom of the image. I'd like the image to be centered in the content size, and to be able to scroll it horizontally to both to the left and to the right with offset 100 pts each side (similar thing with top and bottom when scrolling vertically).
How could I achieve this?
Note: I'm setting all the UI in code, I'm not using storyboards nor xib files
You may find it easier / more intuitive to use constraints and auto-layout rather than screenWidth and screenHeight:
//
// CenteredScrollViewController.swift
// SW4Temp
//
// Created by Don Mag on 4/18/18.
//
import UIKit
class CenteredScrollViewController: UIViewController {
let theScrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.green
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scrollView to the main view
view.addSubview(theScrollView)
// add the imageView to the scrollView
theScrollView.addSubview(theImageView)
// pin the scrollView to all four sides
theScrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
theScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0).isActive = true
theScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
theScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
// constrain the imageView's width and height to the scrollView's width and height
theImageView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, multiplier: 1.0).isActive = true
theImageView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, multiplier: 1.0).isActive = true
// set the imageView's top / bottom / leading / trailing anchors
// this *also* determines the scrollView's contentSize (scrollable area)
// with 100-pt padding on each side
theImageView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 100.0).isActive = true
theImageView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: -100.0).isActive = true
theImageView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 100.0).isActive = true
theImageView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: -100.0).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set the scrollView's contentOffset (to center the imageView)
theScrollView.contentOffset = CGPoint(x: 100, y: 100)
}
}
You can move only down and right because your current content offset is 0,0 so top left - thus you can move down 200 and right 200.
What you want is to be scrolled 1/2 of vertical padding and 1/2 of horizontal padding, so in your case you would do scrollView.contentOffset = CGPoint(x: 100, y: 100)
Also for everything to work, UIImageView has to be same size as scrollView's contentSize, so bigger than screen size.
Given the comments what I think you want is the image to fill the screen and then user could scroll outside of bounds of the image, then you just need to make UIImageView's size be size of the screen its x and y coordinates to be same as contentOffset of the scrollView so (100, 100).
Here is the video of the sample app doing this:
https://dzwonsemrish7.cloudfront.net/items/2v361r2p0O2j1D3x3W10/Screen%20Recording%202018-04-19%20at%2002.32%20PM.mov
try this in
Swift 4.* or 5.*
let maxScale = self.imageScrollView.maximumZoomScale
let minScale = self.imageScrollView.minimumZoomScale
if let imageSize = imageView.image?.size{
let topOffset: CGFloat = (boundsSize.height - minScale * imageSize.height ) / 2
let leftOffset: CGFloat = (boundsSize.width - minScale * imageSize.width ) / 2
self.imageScrollView.contentInset = UIEdgeInsets(top: topOffset, left: leftOffset, bottom: 0, right: 0)
}

How do I get UIView width and height before adding to subview?

I'm building a UIPageViewController that has a variable number of pages based on the height of views in an array of views.
I have a class called a BlockView that looks like this:
final class BlockView: UIView {
init(viewModel: BlockViewModel) {
super.init(frame: .zero)
let primaryLabel = UILabel()
primaryLabel.text = viewModel.labelText
addSubview(primaryLabel)
constrain(primaryLabel) {
$0.top == $0.superview!.top + 8
$0.bottom == $0.superview!.bottom - 8
$0.left == $0.superview!.left + 8
$0.right == $0.superview!.right - 8
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
What I'd like to be able to do is loop through my array of BlockViews and run print(blockView.frame) and see frames that aren't zero.
Now I know I'm setting my frame to .zero inside of the BlockView.init. That's because I'd like the view to size itself based on its labels.
Is there a function I need to run to achieve this?
Thanks
Try sizeThatFits(_:) to calculate it without putting it to the superview. The only parameter to the method is the CGSize that represents boundaries in which it should be displayed. E.g., if you know the width of the superview (e.g., 340 points) and you want to know how much it will take in height:
let expectedSize = view.sizeThatFits(CGSize(width: 340, height: .greatestFiniteMagnitude))
However, your BlockView does not seem to have a proper constraints set yet. You initialize it with super.init(frame: .zero) - thus it has size 0,0.
And your constraints does not change that, e.g.:
constrain(primaryLabel) {
$0.centerY == $0.superview!.centerY
$0.left == $0.superview!.left + 8
}
This looks like you set the center in Y axis of the label to the center of the block view, and the left anchor of the label to the left anchor of the view. If the blockView would have the size already, that would position the label properly. But right now the size of the block view is not affected by the size of labels at all. I guess you would want to constrain the labels to the left, right, top and bottom anchors of the blockView, so that when you try to calculate the size of the blockView, the autolayout will have to first calculate the size of the labels and based on this the size of the blockView itself.
A possible solution (I am using anchor based autolayout syntax) that you can try to put to initializer of the BlockView:
primaryLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8).isActive = true
primaryLabel.topAchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
primaryLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -8).isActive = true
primaryLabel.bottomAnchor.constraint(equalTo: secondaryLabel.topAnchor, constant: -8).isActive = true
secondaryLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8).isActive = true
secondaryLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -8).isActive = true
secondaryLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8).isActive = true

How to use a slide animation rather than a "ghosting" animation when a UICollectionViewCell changes position due to a change in height of it's parent?

I'm building a UICollectionView which has two cells. When the user taps a button, the second cell needs to move from directly below the first cell to directly to the right of the first cell (off screen) and allow the user to scroll over to it.
The kicker is that I need it to appear to slide to the new position. Using my current code, it's just "ghosting".
Here's how I'm doing it now:
Initialize the UICollectionView with UICollectionViewFlowLayout with horizontal scrolling, paging enabled, and a height of two cells plus margin (196pt * 2 + 8pt = 400pt)
func setupTestCollectionView() {
// Create the layout, the frame, and the collection view
let testLayout = UICollectionViewFlowLayout()
let testFrame = CGRect()
let collectionView = UICollectionView(frame: testFrame, collectionViewLayout: testLayout)
// Specify the attributes of the elements we just created and add them to the primary view
testLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
testLayout.minimumLineSpacing = 8
testLayout.minimumInteritemSpacing = 0
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.isPagingEnabled = true
collectionView.clipsToBounds = false
collectionView.backgroundColor = UIColor.green
collectionView.register(ArtworkCollectionViewCell.self, forCellWithReuseIdentifier: "artworkCell")
collectionView.delegate = self
collectionView.dataSource = self
view.addSubview(collectionView)
// Set all of the constraints to position the UICollectionView in the primary view then append them to the initialConstraints array
let topConstraint = collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8)
let leadingConstraint = collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0)
let trailingConstraint = collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)
testHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 400)
// testHeightConstraint is declared outside of this function so we can modify it in a different funcction when the user taps a button
initialConstraints.append(contentsOf: [topConstraint, leadingConstraint, trailingConstraint, testHeightConstraint])
}
Change the height constraint of the UICollectionView when the user taps a button to a height of 1 cell (196pt) and animate the layout update
func testButtonAction() {
// Change the height constraint of the UICollectionView
testHeightConstraint.constant = 196
// Animate the layout update
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
This achieves the function I'm looking for (see screenshots below), but so far I've been unable to figure out how to change the animation. Any help with this is greatly appreciated!
Thank you in advance!
Screenshot on View Load:
Screenshot after Button is Tapped:
Screenshot of Scrolling (to show functionality & position):

Resources