iOS13 UICollectionViewFlowLayout subclass not receiving collectionView frame changes - swift

I am testing out some code and it looks like in iOS13 the UICollectionViewFlowLayout is not receiving the changes in the collectionView frame.
Below is a sample code, in which I simply change the height of the collectionView based on the amount I scroll inthe tableview below the collectionView:
ViewController
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.collectionViewLayout.invalidateLayout()
let totalScroll = scrollView.contentSize.height - scrollView.bounds.size.height
let offset = (scrollView.contentOffset.y)
let percentage = offset / totalScroll
var frame = collectionView.frame
frame.size.height = 40 - (40 * percentage)
collectionView.frame = frame
}
CustomCollectionViewFlowLayout: UICollectionViewFlowLayout
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
print(collectionView?.frame)
return attributes
}
The print statement inside the CustomCollectionViewFlowLayout in iOS 12 and below prints out the changes in collectionView.frame correctly i.e. the height actually changes. But in iOS 13, it isn't being reflected at all.
Help anybody?

First of all,
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.collectionViewLayout.invalidateLayout()
...
}
looks suspicious. Are you sure that your layout should be invalidated inside the scrollViewDidScroll function?
There is a proper place where a layout should be invalidated while scrolling:
final class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// proper place to invalidate layout while scrolling
}
}
If you invalidate your layout inside the layout class you will be able to calculate the desired layout container height properly. And then you can inform your collection view that its height should be changed. For example, with the help of some delegate:
protocol CustomCollectionViewFlowLayoutSizeDelegate: class {
func newSizeAvailableFor(layout: CustomCollectionViewFlowLayout, progress: CGFloat)
}
final class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var sizeDelegate: CustomCollectionViewFlowLayoutSizeDelegate?
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// proper place to invalidate layout while scrolling
}
}
final class CustomViewController: CustomCollectionViewFlowLayoutSizeDelegate {
func newSizeAvailableFor(layout: CustomCollectionViewFlowLayout, progress: CGFloat) {
// change collectionView frame
}
}
Conclusion:
Your layout is trying to listen to its container height, but its container height is calculated based on your layout. You can easily drop the container dependency for height calculation or just provide the layout with initial container height. In the end the layout will be responsible for all the calculations.

Related

How can I get scrolling to work for an NSCollectionView hosted in SwiftUI?

I've built an NSCollectionView wrapper using NSViewRepresentable, but it refuses to scroll. The class looks something like this:
final class SwiftNSCollectionView: NSObject, NSViewRepresentable, NSCollectionViewDataSource // etc
{
// ... init ...
typealias NSViewType = NSCollectionView
func makeNSView(context: Context) -> NSCollectionView {
let collectionView = NSCollectionView()
scrollView.documentView = collectionView
updateNSView(collectionView, context: context)
return collectionView
}
func updateNSView(_ scrollView: NSCollectionView, context: Context) {
collectionView.dataSource = self
// ... other collectionView setup
}
// ...
}
Typically, NSCollectionView has a built-in NSScrollView.
I've tried:
No wrapper—the NSCollectionView simply doesn't scroll.
Wrapping this SwiftNSCollectionView in a SwiftUI ScrollView, but that causes two problems:
The height of the the NSCollectionView collapses to 0 (which I can work around somewhat using a GeometryReader)
The NSCollectionView doesn't want to extend to the height of all its objects (which makes sense because it virtualizes them)
Using NSCollectionViewCompositionalLayoutConfiguration with an orthogonal scroll direction (hacky):
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
// If the "official" scroll direction is horizontal,
// then the orthogonal direction becomes vertical,
// and we can scroll our one section 😈
let configuration = NSCollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
But this seemed to mess up keyboarding: after a certain number of elements, arrow keys between elements either stopped working altogether or moved to the completely wrong elements. Seemed like a virtualization bug of some sort.
You can mitigate this by creating an NSScrollView manually.
Update the NSViewType to be NSScrollView.
Update function signatures as required.
Use your existing NSCollectionView as the .documentView of the new scroll view.
Then you can use your SwiftNSCollectionView directly in SwiftUI code and it will scroll properly without any custom work on your side.
final class SwiftNSCollectionView: NSObject, NSViewRepresentable, NSCollectionViewDataSource // etc {
// ... init ...
// No longer NSCollectionView
typealias NSViewType = NSScrollView
func makeNSView(context: Context) -> NSScrollView {
// Create an NSScrollView, too!
let scrollView = NSScrollView()
let collectionView = NSCollectionView()
scrollView.documentView = collectionView
updateNSView(scrollView, context: context)
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
// Since we get an NSScrollView, get the child!
let collectionView = scrollView.documentView as! NSCollectionView
collectionView.dataSource = self
// ... other collectionView setup
}
// ...
}

How to create an UITableView with an intrinsic Height in Swift 5 using UIKit?

I ran into a problem while using UITableViews.
AutoLayout requires a fixed Height Constraint for the UITableView, so the Height does not change when I add or remove TableViewCells.
My Problem is, that I am using a TableView inside a ScrollView. This is not really great since both elements are scrollable.
My Plan is to always resize the TableView in relation to its content Height. In this case the TableView can never get scrolled.
My Idea so far was the following, but because of some reason, it does not really work:
tableView.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: tableView.contentSize.height)
If you have any ideas on how to fix this, please comment down below. Simpler Solutions without class extensions, etc. are preferred;)
Thanks a lot guys!
You can use something along the lines of:
final class AutoSizingTableView: UITableView {
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
setContentCompressionResistancePriority(.required, for: .vertical)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setContentCompressionResistancePriority(.required, for: .vertical)
}
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override func reloadData() {
super.reloadData()
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
setNeedsLayout()
layoutIfNeeded()
return contentSize
}
}
Basically what this does is set an intrinsicContentSize equal to the contentSize when the data of the tableview is reloaded, or if the contentSize changes.
However you have to ask yourself the question: If I don't need dequeuing logic, do I actually need a tableview?
You need to override updateViewConstraints() method in your UIViewController and set the new height of tableView as follows:
override func updateViewConstraints() {
tableViewHeightConstraint?.constant = tableView.contentSize.height
super.updateViewConstraints()
}
And call self.view.setNeedsUpdateConstraints() when you need to recreate your tableView with a new height.

Reposition UIButtons programmatically

I have 4 UIButtons in a 2x2 grid, and I want them to move themselves into one column with 4 rows when I toggle a SegmentedControl.
What's the best way of going about this? Do I need to change the current constraints, or is there another way?
Thanks.
You can use UICollectionViewController.
On tap of UISegmentControl you can change the size of cell under sizeForItemAtIndexPath() method based on condition.
Sample Code:
class YourViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if (listViewType == "kTileView") {
let size = collectionView.frame.width / 2
return CGSize(width: size, height: 50)
}
else {
let size = collectionView.frame.width
return CGSize(width: size, height: 50)
}
}
}
I guess you want something like this:
One way to rearrange four subviews from a column to a 2x2 grid is using stack views. Make a vertical stack view for 2-element column of the 2x2 grid, and put those two column stack views in a horizontal stack view. Then, to turn the grid into a single column, you set the axis of the horizontal stack view to vertical.
Here's my storyboard:
And here's my view controller:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
updateViews()
}
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var rearrangingStackView: UIStackView!
#IBAction func segmentedControlValueChanged(_ sender: Any) {
UIView.animate(withDuration: 0.25) {
self.updateViews()
self.view.layoutIfNeeded()
}
}
private func updateViews() {
if segmentedControl.selectedSegmentIndex == 0 {
rearrangingStackView.axis = .vertical
} else {
rearrangingStackView.axis = .horizontal
}
}
}

How to properly size content on ScrollView for all devices?

I am using a scrollview along with Nibs. The goal is to layout 3 nibs with image inside them and scroll through them. I am also using storyboards to layout the scrollview.
The issue is venting looks good besides on the iPhone Plus sizes. The nibs are not centered right and bleeds on the screen? how do I fix this? Please check my code below.
#IBOutlet private weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
imageArray = [imageOne, imageTwo, imageThree]
scrollView.contentSize = CGSize(width: self.view.bounds.width * CGFloat(imageArray.count), height: scrollView.frame.size.height)
loadOnboardingDescriptions()
}
// MARK: Load onboarding dscription features
func loadOnboardingDescriptions() {
for (index, image) in imageArray.enumerated() {
if let onboardingDescriptionView = Bundle.main.loadNibNamed(Constants.NibName.onboardingDescriptionView.rawValue, owner: self, options: nil)?.first as? OnboardingDescriptionView {
onboardingDescriptionView.onboardingImageView.image = UIImage(named: image["image"]!)
onboardingDescriptionView.frame.size.width = self.view.bounds.size.width
onboardingDescriptionView.frame.origin.x = CGFloat(index) * self.view.bounds.size.width
print(onboardingDescriptionView.frame.origin.x)
self.scrollView.addSubview(onboardingDescriptionView)
}
}
}
EDIT:
As suggested by Matt bellow, I should have called this function inside:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
loadOnboardingDescriptions()
}

Width of UITableView content view does not match Container

I have a TableVC embedded in a Container. The width of the Container running on an iPhone5 is 320. However the width of the ContentView in the TableView cell is 600. How can I make them match? (+/- padding). Am I missing a constraint? I have also tried setNeedsLayout() and layoutSubViews() in cellForRowAtIndexPath and in the custom cells subclass, but this doesn't seem to work either.
In the picture below, I want the width of the darkgrey to match the light grey (+- padding)
Any help much appreciated.....
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("EventCell") as! CustomResultsTVCell
// cell.setNeedsLayout()
// cell.layoutSubviews()
// cell.layoutIfNeeded()
let barViewWidth = Float(cell.barView.frame.width)
print("barview width is \(barViewWidth)")
// prints 584
let contentViewWidth = cell.myContentView.frame.width
print("contentView width is \(contentViewWidth)")
// prints 600
The CustomCell class is
import Foundation
import UIKit
class CustomResultsTVCell: UITableViewCell {
#IBOutlet weak var myContentView: UIView!
#IBOutlet weak var barView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// super.layoutSubviews()
// setNeedsLayout()
// layoutSubviews()
layoutIfNeeded()
}
Can you check property of your leading and trailing constraints. Make sure it unchecked relative to margin for both first item and second item.
see attached screenshots if it helps you.
Hope this will help you to fix your issue.