UICollectionView not calling intrinsicContentSize - ios

I have a UICollectionViewController which generates cells with a random color for testing purposes. Now that the UICollectionViewController is embedded in a UIScrollView, I want the scrollView to be the same size as it's contentSize.
I made a subclass of UICollectionView, implemented intrinsicContentSize method, and set the UICollectionView's class to my custom class in IB. However intrinsicContentSize never gets called. I have the exact same setup with an UITableView and there it works flawlessly.
Any ideas on this?
- (CGSize)intrinsicContentSize {
[self layoutIfNeeded];
return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}

The correct answer is to do something like this
- (CGSize)intrinsicContentSize
{
return self.collectionViewLayout.collectionViewContentSize;
}
And call -invalidateContentSize whenever you think it needs to change (after reloadData for example).
In Interface Builder you may need to set placeholder intrinsic size constraints to avoid errors.
This subclassing and overriding of -intrinsicContentSize is useful if you want to grow a collection view's frame until it is constrained by a sibling or parent view

I'm not sure why it's happening. Here's another solution to this problem. Set up a height constraint on UICollectionView object. Then set its constant to be equal to self.collectionView.contentSize.height. I use a similar approach in my app, though I've got UITextView instead of UICollectionView.
UPDATE: I've found a way to do this with intrinsicContentSize: UITextView in UIScrollView using auto layout

Use this class to make UICollectionView update its intrinsic content size every time the content is changed.
class AutosizedCollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
registerObserver()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
registerObserver()
}
deinit {
unregisterObserver()
}
override var intrinsicContentSize: CGSize {
return contentSize
}
private func registerObserver() {
addObserver(self, forKeyPath: #keyPath(UICollectionView.contentSize), options: [], context: nil)
}
private func unregisterObserver() {
removeObserver(self, forKeyPath: #keyPath(UICollectionView.contentSize))
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
if keyPath == #keyPath(UICollectionView.contentSize) {
invalidateIntrinsicContentSize()
}
}
}
Though, you should understand that if you embed it into another scroll view, cell recycling will not work. The most appreciated way to deal with nested collection view is described here.

Related

Dynamic height of UITableView inside UIcollectionViewCell

I have a UICollectionViewCell which has a UTableView inside it. I want to calculate the height of UITableView dynamically based on the content inside it. It means, the UITableView shouldn't scroll but should increase/decrease its height according to its content.
You can use a self sizing table view like this:
class SelfSizingTableView: UITableView {
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
}
In addition you have to make sure to set up constraints in your collection view cell correctly (a full set of constraints from the top of the collection view cell to the bottom of it).
I've been there. There are multiple ways to do that, especially if your rows really have constant height, that should be easy. Multiply the number of rows to the constant height, voila, you have your tableView height.
HOWEVER, if you have dynamic cell height of the tableView that is inside the collectionViewCell or tableViewCell (they're the same), then you need another approach.
My approach to that is observing the keyPath contentSize. This one is perfect, I've been using this in my main production project. Here's a full block of the code that I use, including the comments/documentation ;)
/**
Important Notes, as of 12/18/2018, 9:41PM, a eureka moment:
- No need for label height.
- Needs a reference for tableViewHeight.
- After observing the newSize data, update the constraint's offset of the tableViewHeight reference.
- And then let know the controller to not reload the data of the tableView but rather begin and end updates only.
- beginUpdate() and endUpdate() lets the tableView to update the layout without calling the cellForRow, meaning without calling the setupCell method.
*/
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let obj = object as? LLFTableView, obj.tag == 444 {
if obj == self.tableView && keyPath == "contentSize" {
if let newSize = change?[NSKeyValueChangeKey.newKey] as? CGSize {
// Edit heightOfTableViewConstraint's constant to update height of table view
llfPrint("New Size of the tableView: \(newSize) ✅✅✅✅✅")
DispatchQueue.main.async {
self.constraint_TableViewHeight?.update(offset: newSize.height)
self.delegate?.reloadData()
}
}
}
}
}
So, what happens in the controller or viewModel that implements such reloadData delegate method? It calls another delegate method to just let know the controller (That holds the super tableView) that we're updating the height of the cell.
self.tableView.beginUpdates()
self.tableView.endUpdates()
That's it! :) I hope this helps!

Self sizing UICollectionView with FlowLayout and variable number of cells

Please note that this is NOT a question about self sizing UICollectionViewCell.
Is it possible to create self sizing UICollectionView (with UICollectionViewFlowLayout) size of which depends on cells inside it?
I have a collection view with variable number of cells. I would like to constrain width of the collection view and then allow it to expand vertically depending on quantity of cells.
This question is similar to this one CollectionView dynamic height with Swift 3 in iOS but I have multiple cells per row.
Bonus points, if one could still use self sizing cells inside of collection view but it is ok if collection view delegate provides cell sizes.
I don't have enough reputation to comment, but I think
this is the answer
that you are looking for. Code below:
class DynamicCollectionView: UICollectionView {
override func layoutSubviews() {
super.layoutSubviews()
if bounds.size != intrinsicContentSize() {
invalidateIntrinsicContentSize()
}
}
override func intrinsicContentSize() -> CGSize {
return self.contentSize
}
}
#Michal Gorzalczany's answer led me to this answer (for my specific case):
Subclass UICollectionView
class DynamicCollectionView : UICollectionView {
weak var layoutResp : LayoutResponder?
override func invalidateIntrinsicContentSize() {
super.invalidateIntrinsicContentSize()
self.layoutResp?.updateLayout()
}
override var intrinsicContentSize : CGSize {
return self.contentSize
}
override var contentSize: CGSize {
get { return super.contentSize }
set {
super.contentSize = newValue
self.invalidateIntrinsicContentSize()
}
}
}
Protocol LayoutResponder should be adopted by the view which deals on a high level with layout of collection view (I have a relatively complex layout).
protocol LayoutResponder : class {
func updateLayout()
}
extension RespView : LayoutResponder {
func updateLayout() {
self.layoutResp?.setNeedsLayout()
self.layoutResp?.layoutIfNeeded()
}
}
In my case I actually forward updateLayout() even further up the chain.
I guess for simpler layouts you can skip step 2 alltogether.
This is in my opinion is a bit "hacky" so if someone has a better approach I would appreciate if you share.

Resize UICollectionView to content size

Summary: I have a child view within a stackview and that child view that is programatically sized. It has a couple of empty views to fill up the empty space. At runtime, however, attempts to resize the child don't work and it remains the same size as in the storyboard. How can I resize the child view so that the stack view honours its new size and positions it accordingly.
Details:
I have a UICollectionView with a custom layout. The layout calculates the positions of subviews correctly (they display where I want) and return the correct content size. The content size is narrower than the screen but possibly longer depending upon orientation.
The custom layout returns the correct, calculated size but the collection view does not resize.
I've tried programatically changing the collection view's size on the parent's viewDidLoad. Didn't work.
I've tried programatically changing the collection view's layoutMargins on the parent's viewDidLoad. Didn't work.
I am using Swift 3, XCode 8.
If I understand your question, you need your UICollectionView to have its size equals to its content, in other words, you want your UICollectionView to have an intrinsic size (just like a label which resizes automatically based on its text).
In that case, you can subclass UICollectionView to add this behaviour, then you don't need to set its size.
class IntrinsicSizeCollectionView: UICollectionView {
// MARK: - lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
if !self.bounds.size.equalTo(self.intrinsicContentSize) {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
get {
let intrinsicContentSize = self.contentSize
return intrinsicContentSize
}
}
// MARK: - setup
func setup() {
self.isScrollEnabled = false
self.bounces = false
}
}
ps: In your .xib, don't forget to set your IntrinsicSizeCollectionView height constraint as a placeholder constraint (check "Remove at build time")

IOS, Swift: How come my subclass of UIScrollView doesn't scroll?

I'm trying to subclass UIScrollView, to do some custom drawing and creation of customized UIViews. The drawing and creation of UIViews works fine, but the view just doesn't scroll.
The internal height of the view is fixed, and I calculate it in the init method. I also override the intrinsticContentSize method, but that doesn't work.
What am I doind wrong?
import UIKit
class CustomView: UIScrollView, UIScrollViewDelegate {
// MARK: - layout constants
private var _intrinsicSize: CGSize?;
override init(frame: CGRect) {
super.init(frame: frame);
self.didLoad();
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
self.didLoad();
}
private func didLoad() {
self.delegate = self;
var result = CGSize();
result.height = CGFloat(_halfHourHeight * 48);
result.width = 500;
_intrinsicSize = result;
}
override func intrinsicContentSize() -> CGSize {
return self._intrinsicSize!;
}
override func drawRect(rect: CGRect) {
super.drawRect(rect);
// some custom drawing here
}
}
Scroll views generally don't have an intrinsic size, it usually doesn't mean anything. They have a frame, bounds and content size - it's the content size you're interested in setting and it goes into setting the bounds.
The content size is the total size of all the subviews, and the bounds is the window onto the currently visible area of those subviews.
You also wouldn't usually have custom drawing code, though you can. You'd usually add subviews to do that drawing for the scroll view.

UIScrollViewKeyboardDismissModeInteractive changing tableview height with keyboard

Within a UIViewController I've set
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive.
This is great because the user can drag the keyboard down from the tableview.
However, the tableview maintains it's current height when dragging down the keyboard. This looks odd because it leaves empty space between the keyboard and the scrollview.
How can I persistently track the frame of the keyboard so I may resize the tableview as the user drags the keyboard? I've tried using UIKeyboardWillChangeFrameNotification but that seems to only get called after the user finishes dragging.
Your table view shouldn't change its height to accommodate the keyboard.
Instead, the keyboard should be presented overtop of the table view, and you should adjust the contentInset and scrollIndicatorInsets properties on the table view so that the lower table content is not obscured by the keyboard. You need to update the scroll insets whenever the keyboard is presented or dismissed.
You don't have to do anything special while the keyboard is dismissed interactively, because the table content should already scroll down as the keyboard moves out of view.
I'd rather this not be the accepted answer, but for those of you out there also having trouble with this here's what worked for me.
Create a custom subclass of UIView.
In the subclass's willMoveToSuperview: method, add a KVO observer to the superview's center on iOS 8 and frame on lesser versions of iOS (remember to remove the old observer, you may want to use an instance variable to track this).
In your view controller add a 0.0 height input accessory view to the view controller via inputAccessoryView class override. Use your custom UIView subclass for this.
Back in the subclass, in observeValueForKeyPath:..., capture the frame of the view's superview, this should be the frame of the UIKeyboard.
Use NSNotificationCenter or some other means to then pass this frame back to your view controller for processing.
It's a pretty painful process and not guaranteed to work in future versions of iOS. There are likely a ton of edge cases that will pop up later since I just built this, but it's a good start. If anyone has a better idea I'll happily mark your answer as correct.
This is what I come up to, insted of using notification I use a delegate:
protocol MyCustomViewDelegate {
func centerChanged(center: CGPoint)
}
class MyCustomView: UIView{
private var centerContext = 0
var delegate: MyCustomViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func willMoveToSuperview(newSuperview: UIView?) {
super.willMoveToSuperview(newSuperview)
guard let newSuperview = newSuperview else {
self.superview?.removeObserver(self, forKeyPath: "center")
return
}
let options = NSKeyValueObservingOptions([.New, .Old])
newSuperview.addObserver(self, forKeyPath: "center", options: options, context: &centerContext)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
//print("CHANGE",keyPath)
if context == &centerContext {
guard let center = superview?.center else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
delegate?.centerChanged(center)
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
}

Resources