Rendering Collection View Cells inside Stackview - ios

I'm wondering how the UICollectionView will behave when we will embed it in UIStackView that is in the UIScrollView. As long as we will have to set collectionView's containerView intrinsicContentSize to be the collectionView.contentSize.height it will probably load all the data at the same time, so as we will add image fetching, shadows etc it will make it not super performant. Can we somehow avoid it? Of course it would be great to leave UIStackView as the collectionView is not the only embedded view in it :)
Main View
final class MainView: UIView {
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isDirectionalLockEnabled = true
return scrollView
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fill
return stackView
}()
/// Adding subviews and setting constraints...
}
And here is the place where we embed our collectionView
final class ContainerView: UIView {
var collectionView: UICollectionView = {
let collectionView = UICollectionView()
return collectionView
}()
override var intrinsicContentSize: CGSize {
return CGSize(width: header.frame.width,
height: collectionView.frame.minY +
collectionView.contentSize.height)
}
/// Adding subviews and setting constraints...
}
This view is embeded in MainViewController stackview

Related

Snapkit LayoutConstraints Unable to simultaneously satisfy constraints

I have simple UITableView and custom UITablViewCell in my project, which created programmatically and use SnapKit for auto layout its.
When I run app, everything work fine and get some LayoutConstraints error in debug console.
UITableViewCell
import UIKit
import SnapKit
class TestTableViewCell: UITableViewCell {
...
private lazy var containerView: UIView = {
let view = UIView()
return view
}()
private lazy var centerStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [titleLabel, disclosureImageView])
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 5.0
return stackView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
return label
}()
private lazy var disclosureImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "disclosure")
return imageView
}()
..
private func setupUI() {
selectionStyle = .none
backgroundColor = .background
contentView.addSubview(containerView)
containerView.backgroundColor = .white
containerView.layer.cornerRadius = 12
containerView.clipsToBounds = true
containerView.addSubview(centerStackView)
setNeedsUpdateConstraints()
}
// MARK: - Update Constraints
override func updateConstraints() {
containerView.snp.updateConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(100)
}
centerStackView.snp.updateConstraints { make in
make.center.equalToSuperview()
}
super.updateConstraints()
}
}
How I can fix this constraint error ?
There are actually two conflicts for each table view cell. You can copy-paste the sets of constraints are in conflict into https://www.wtfautolayout.com and see exactly why they are conflicting. As a result, we get this and this.
We can see that the first conflict is caused by your container view having a height constraint, but the table view itself also adds a height constraint to the cell to enforce its cells' sizes,
"<NSLayoutConstraint:0x7b1400031920 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7b4c00003640.height == 116.5 (active)>"
For some reason, the constant here is 116.5, not 116 as you would expect from the 100 points of height of the container view + the 16 points of inset.
You can either change it to
make.height.greaterThanOrEqualTo(100)
Or remove the height constraint completely:
// make.height.equalTo(100)
and use the delegate method heightForRowAt to control the cell height instead:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
116
}
As for the second conflict, it is caused by a constraint constraining the stack view's width to 0, being in conflict with the fact that there must be some spacing between the image and label.
"<NSLayoutConstraint:0x7b1400031b50 'UIView-Encapsulated-Layout-Width' UIStackView:0x7b4c000039c0.width == 0 (active)>"
If you move the following code from updateConstraints into setupUI, the conflict is resolved:
containerView.snp.updateConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.leading.trailing.equalToSuperview().inset(16)
make.height.greaterThanOrEqualTo(100)
}
centerStackView.snp.updateConstraints { make in
make.center.equalToSuperview()
}

Swift: How to fill a ScrollView from Interface Builder with UIViews programmatically

I am working on a project where I want the user to be able to select two methods of input for the same form. I came up with a scrollview that contains two custom UIViews (made programmatically). Here is the code for the responsible view controller:
import UIKit
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: customView2 = CustomView2()
var frame = CGRect.zero
func setupScrollView() {
pageControl.numberOfPages = 2
frame.origin.x = 0
frame.size = scrollView.frame.size
customView1 = customView1(frame: frame)
self.scrollView.addSubview(customView1)
frame.origin.x = scrollView.frame.size.width
frame.size = scrollView.frame.size
customView2 = CustomView2(frame: frame)
self.scrollView.addSubview(customView2)
self.scrollView.contentSize = CGSize(width: scrollView.frame.size.width * 2, height: scrollView.frame.size.height)
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
While it works, Xcode gives me an error message for auto layout:
Scrollable content size is ambiguous for "ScrollView"
Also a problem: content on the second UIView is not centered, even though it should be:
picture of the not centered content
import UIKit
class customView2: UIView {
lazy var datePicker: UIDatePicker = {
let datePicker = UIDatePicker()
datePicker.translatesAutoresizingMaskIntoConstraints = false
return datePicker
}()
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView () {
self.backgroundColor = .systemYellow
datePicker.datePickerMode = .date
datePicker.addTarget(self, action: #selector(self.datePickerValueChanged(_:)), for: .valueChanged)
addSubview(datePicker)
setupLayout()
}
func setupLayout() {
let view = self
NSLayoutConstraint.activate([
datePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor),
datePicker.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
datePicker.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
datePicker.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2)
])
}
#objc func datePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.yyyy"
let selectedDate: String = dateFormatter.string(from: sender.date)
print("Selected value \(selectedDate)")
}
Any ideas on how to solve this? Thank you very much in advance. And please go easy on me, this is my first question on stackoverflow. I am also fairly new to programming in swift.
To make things easier on yourself,
add a horizontal UIStackView to the scroll view
set .distribution = .fillEqually
constrain all 4 sides to the scroll view's .contentLayoutGuide
constrain its height to the scroll view's .frameLayoutGuide
add your custom views to the stack view
constrain the width of the first custom view to the width of the scroll view's .frameLayoutGuide
Here is your code, modified with that approach:
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: CustomView2 = CustomView2()
func setupScrollView() {
pageControl.numberOfPages = 2
// let's put the two custom views in a horizontal stack view
let stack = UIStackView()
stack.axis = .horizontal
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(customView1)
stack.addArrangedSubview(customView2)
// add the stack view to the scroll view
scrollView.addSubview(stack)
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides of content layout guide
stack.topAnchor.constraint(equalTo: contentG.topAnchor),
stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
// stack view Height equal to scroll view frame layout guide height
stack.heightAnchor.constraint(equalTo: frameG.heightAnchor),
// stack is set to fillEqually, so we only need to set
// width of first custom view equal to scroll view frame layout guide width
customView1.widthAnchor.constraint(equalTo: frameG.widthAnchor),
])
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
}
Edit
Couple additional notes...
UIScrollView layout ambiguity.
As I said in my initial comment, if we add a UIScrollView in Storyboard / Interface Builder, but do NOT give it any constrained content, IB will complain that it has Scrollable Content Size Ambiguity -- because it does. We haven't told IB what the content will be.
We can either ignore it, or select the scroll view and, at the bottom of the Size Inspector pane, change Ambiguity to Never Verify.
As a general rule, you should correct all auto-layout warnings / errors, but in specific cases such as this - where we know that it's setup how we want, and we'll be satisfying constraints at run-time - it doesn't hurt to leave it alone.
UIDatePicker not being centered horizontally.
It actually is centered. If you add this line:
datePicker.backgroundColor = .green
You'll see that the object frame itself is centered, but the UI elements inside the frame are left-aligned:
From quick research, it doesn't appear that can be changed.
Now, from Apple's docs, we see:
You should integrate date pickers in your layout using Auto Layout. Although date pickers can be resized, they should be used at their intrinsic content size.
Curiously, if we add a UIDatePicker in Storyboard, change its Preferred Style to Compact, and give it centerX and centerY constraints... Storyboard doesn't believe it has an intrinsic content size.
If we add it via code, giving it only X/Y position constraints, it will show up where we want it at its intrinsic content size. But... if we jump into Debug View Hierarchy, Xcode tells us its Position and size are ambiguous.
Now, what's even more fun...
Tap that control and watch the Debug console fill with 535 Lines of auto-layout errors / warnings!!!
Some quick investigation -- these are all internal auto-layout issues, and have nothing to do with our code or layout.
We see similar issues with the iOS built-in keyboard when it starts showing auto-complete options.
Those are safe to ignore.

How to add self sizing child UICollectionViewController?

I have a view controller which has a container view.
I would like to add a UICollectionViewController as a child view controller i.e., add the view of the UICollectionViewController as a subview of the container view and call didMove(toParent:).
I would like the height of the container view to be dynamic so that it depends on the height of the UICollectionViewController's view. I want the height UICollectionView to be equal to its content height.
Can anyone point out how to do this?
This is what I have done till now-
While adding the UICollectionViewController, set translatesAutoresizingMaskIntoConstraints to false, as given in https://stackoverflow.com/a/35431534.
let viewController = CustomCollectionViewController()
viewController.view.translatesAutoresizingMaskIntoConstraints = false
addChild(viewController)
containerView.addSubview(viewController.view)
viewController.didMove(toParent: self)
viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
viewController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
viewController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
I created a custom UICollectionView such that its height depends on its content size as given in https://stackoverflow.com/a/49297382.
class CustomCollectionView: UICollectionView {
override func reloadData() {
super.reloadData()
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return collectionViewLayout.collectionViewContentSize
}
}
Set the above custom UICollectionView as the collectionView of the CustomCollectionViewController using the answer given in https://stackoverflow.com/a/41404946
class CustomCollectionViewController: UICollectionViewController {
override func loadView() {
collectionView = CustomCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
}
}
What I have done does not work. Can anyone point out how to do this?

Stretchy Layout not working with child view controller

I'm trying to follow the example described here for making a stretchy layout which includes a UIImageView and UIScrollView. https://github.com/TwoLivesLeft/StretchyLayout/tree/Step-6
The only difference is that I replace the UILabel used in the example with the view of a child UIViewController which itself contains a UICollectionView. This is how my layout looks - the blue items are the UICollectionViewCell.
This is my code:
import UIKit
import SnapKit
class HomeController: UIViewController, UIScrollViewDelegate {
private let scrollView = UIScrollView()
private let imageView = UIImageView()
private let contentContainer = UIView()
private let collectionViewController = CollectionViewController()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.delegate = self
imageView.image = UIImage(named: "burger")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let imageContainer = UIView()
imageContainer.backgroundColor = .darkGray
contentContainer.backgroundColor = .clear
let textBacking = UIView()
textBacking.backgroundColor = #colorLiteral(red: 0.7450980544, green: 0.1235740449, blue: 0.2699040081, alpha: 1)
view.addSubview(scrollView)
scrollView.addSubview(imageContainer)
scrollView.addSubview(textBacking)
scrollView.addSubview(contentContainer)
scrollView.addSubview(imageView)
self.addChild(collectionViewController)
contentContainer.addSubview(collectionViewController.view)
collectionViewController.didMove(toParent: self)
scrollView.snp.makeConstraints {
make in
make.edges.equalTo(view)
}
imageContainer.snp.makeConstraints {
make in
make.top.equalTo(scrollView)
make.left.right.equalTo(view)
make.height.equalTo(imageContainer.snp.width).multipliedBy(0.7)
}
imageView.snp.makeConstraints {
make in
make.left.right.equalTo(imageContainer)
//** Note the priorities
make.top.equalTo(view).priority(.high)
//** We add a height constraint too
make.height.greaterThanOrEqualTo(imageContainer.snp.height).priority(.required)
//** And keep the bottom constraint
make.bottom.equalTo(imageContainer.snp.bottom)
}
contentContainer.snp.makeConstraints {
make in
make.top.equalTo(imageContainer.snp.bottom)
make.left.right.equalTo(view)
make.bottom.equalTo(scrollView)
}
textBacking.snp.makeConstraints {
make in
make.left.right.equalTo(view)
make.top.equalTo(contentContainer)
make.bottom.equalTo(view)
}
collectionViewController.view.snp.makeConstraints {
make in
make.left.right.equalTo(view)
make.top.equalTo(contentContainer)
make.bottom.equalTo(view)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.scrollIndicatorInsets = view.safeAreaInsets
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.safeAreaInsets.bottom, right: 0)
}
//MARK: - Scroll View Delegate
private var previousStatusBarHidden = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if previousStatusBarHidden != shouldHideStatusBar {
UIView.animate(withDuration: 0.2, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
previousStatusBarHidden = shouldHideStatusBar
}
}
//MARK: - Status Bar Appearance
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return shouldHideStatusBar
}
private var shouldHideStatusBar: Bool {
let frame = contentContainer.convert(contentContainer.bounds, to: nil)
return frame.minY < view.safeAreaInsets.top
}
}
Everything is the same as in this file: https://github.com/TwoLivesLeft/StretchyLayout/blob/Step-6/StretchyLayouts/StretchyViewController.swift with the exception of the innerText being replaced by my CollectionViewController.
As you can see, the UICollectionView is displayed properly - however I am unable to scroll up or down anymore. I'm not sure where my mistake is.
It looks like you are constraining the size of your collection view to fit within the bounds of the parent view containing the collection view's container view and the image view. As a result, the container scrollView has no contentSize to scroll over, and that's why you can't scroll. You need to ensure your collection view's content size is reflected in the parent scroll view's content size.
In the example you gave, this behavior was achieved by the length of the label requiring a height greater than the height between the image view and the rest of the view. In your case, the collection view container needs to behave as if it's larger than that area.
Edit: More precisely you need to pass the collectionView.contentSize up to your scrollView.contentSize. A scrollview's contentSize is settable, so you just need to increase the scrollView.contentSize by the collectionView.contentSize - collectionView.height (since your scrollView's current contentSize currently includes the collectionView's height). I'm not sure how you are adding your child view controller, but at the point you do that, I would increment your scrollView's contentSize accordingly. If your collectionView's size changes after that, though, you'll also need to ensure you delegate that change up to your scrollView. This could be accomplished by having a protocol such as:
protocol InnerCollectionViewHeightUpdated {
func collectionViewContentHeightChanged(newSize: CGSize)
}
and then making the controller containing the scrollView implement this protocol and update the scrollView contentSize accordingly. From your collectionView child controller, you would have a delegate property for this protocol (set this when creating the child view controller, setting the delegate as self, the controller containing the child VC and also the scrollView). Then whenever the collectionView height changes (if you add cells, for example) you can do delegate.collectionViewContentHeightChanged(... to ensure your scroll behavior will continue to function.

self sizing UITableViewCell goes small

I'm trying to create a table view cell prototype (similar to one below) programmatically.
The designed the cell with two stack views,
a) a vertical stack view to contain the text labels and,
b) a horizontal stack view to contain the image view & vertical stack view
I create the required views, stuff it in stack view, and pin stack view to table cell's contentView in the init() of tableviewcell.
And from cellForItemAtIndexPath I call configureCell() to populate data of the cell.
My init() looks like this
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
textStackView = UIStackView(arrangedSubviews: [priorityNameLabel, descriptionLabel])
textStackView.axis = .vertical
textStackView.alignment = .leading
textStackView.distribution = .fill
textStackView.spacing = 5
containerStackView = UIStackView(arrangedSubviews: [priorityImageView, textStackView])
containerStackView.axis = .horizontal
containerStackView.alignment = .center
containerStackView.spacing = 5
containerStackView.distribution = .fill
contentView.addSubview(containerStackView)
containerStackView.translatesAutoresizingMaskIntoConstraints = false
pinContainerToSuperview()
}
func pinContainerToSuperview() {
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).activate()
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor).activate()
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).activate()
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).activate()
}
In my view controller, I set tableView rowHeight to automaticDimension and estimated height to some approx. value. When I run the code, all I'm getting is,
The narrow horizontal lines on the top of the image are my tableview cells (My data count is 3 in this case). I couldn't figure out the problem. Can someone point out what's going wrong here?
EDIT 1:
These are the instance members of my TableViewCell class
var containerStackView: UIStackView!
var textStackView: UIStackView!
var priorityImageView: UIImageView! {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}
var priorityNameLabel: UILabel! {
let label = UILabel()
return label
}
var descriptionLabel: UILabel! {
let label = UILabel()
return label
}
You have your labels and image view as computed properties - this means that every time you access them a new instance is created. This is bad. This basically means that when you set them as arranged subvies of your stack views and then try to configure them later, you work on different objects.
Simplest solution would be to create your labels and image view the way you create your stack views in init.

Resources