I have been stuck on this problem for nearly a week now. I have even reverted my code and wrote the same code as some tutorials but I cannot seem to get my UICollectionView to be displayed to the view for the life of me. I have a lot of other code going on in my view controllers so I am going to display the code that pertains to the UICollectionView. This is what I have so far:
view controller:
class UARTModuleViewController: UIViewController, CBPeripheralManagerDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
setupMenuBar()
}
let menuBar: MenuBar = {
let mb = MenuBar()
return mb
}()
private func setupMenuBar() {
view.addSubview(menuBar)
let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0":menuBar])
let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:|[v0(100)]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0":menuBar])
view.addConstraints(horizontalConstraint)
view.addConstraints(verticalConstraint)
}
}
MenuBar.swift:
class MenuBar : UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
lazy var collectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.red
cv.dataSource = self
cv.delegate = self
return cv
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(collectionView)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "MenuCell")
//took out constraints here just to make the warning mentioned below easier to follow.
}
I do have my protocols implemented but I didn't add them just to save the amount of code I'm posting. I do get this warning after the UARTModuleViewController loads :
[LayoutConstraints] Unable to simultaneously satisfy constraints.
I've tried reasoning through it and can't seem to come to a solution. I do have a cell class as well but thought it was not necessary to include since I can't get the UICollectionView to be displayed. The only other thing that I may add is that I have a UIImageView in the middle of the view that I added in story board and put constraints on like so:
Does anyone have any suggestions as to what I may be doing wrong? Thanks in advance for any help or advice that can be given.
According to comments:
First of all, every time you use constraint in code, you must set translatesAutoresizingMaskIntoConstraints = false to the view you want to add constraint.
You're not telling the collection view to fill entire MenuBar space.
Constraints applied to MenuBar in setupMenuBar are not enaugh to determine the size and position of the view.
These are changes you should apply to tell the MenuBar to fill width, be 60 pt height and to position on bottom of the screen:
private func setupMenuBar() {
view.addSubview(menuBar)
menuBar.translatesAutoresizingMaskIntoConstraints = false // this will make your constraint working
menuBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true // every constraint must be enabled.
menuBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
menuBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
menuBar.heightAnchor.constraint(equalToConstant: 60).isActive = true
}
To tell the collection view to fill entire MenuBar view do this in MenuBar init(frame:) method:
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(collectionView)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "MenuCell")
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
//took out constraints here just to make the warning mentioned below easier to follow.
}
As you can see in my code, I usually use "anchor" methods to apply constraint because are easier than visual format.
Related
I'm stuck with layout issue while displaying collection view cells. All ui is done programmatically. Here's what happening while scrolling down (ui elements appearing from top left corner):
Content of cell is being laid out on the fly, can't fix this. Content should be laid out before appearing. Any suggestions?
Code:
class CollectionView: UICollectionView {
// MARK: - Enums
enum Section {
case main
}
typealias Source = UICollectionViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
private var source: Source!
private var dataItems: [Item] {...}
init(items: [Item]) {
super.init(frame: .zero, collectionViewLayout: .init())
collectionViewLayout = UICollectionViewCompositionalLayout { section, env -> NSCollectionLayoutSection? in
return NSCollectionLayoutSection.list(using: UICollectionLayoutListConfiguration(appearance: .plain), layoutEnvironment: env)
let cellRegistration = UICollectionView.CellRegistration<Cell, Item> { [unowned self] cell, indexPath, item in
cell.item = item
...modifications..
}
source = UICollectionViewDiffableDataSource<Section, Item>(collectionView: self) {
collectionView, indexPath, identifier -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
for: indexPath,
item: identifier)
}
}
func setDataSource(animatingDifferences: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(dataItems, toSection: .main)
source.apply(snapshot, animatingDifferences: animatingDifferences)
}
}
//Cell
class Cell: UICollectionViewListCell {
public weak var item: Item! {
didSet {
guard !item.isNil else { return }
updateUI()
}
}
private lazy var headerView: UIStackView = {...nested UI setup...}()
private lazy var middleView: UIStackView = {...nested UI setup...}()
private lazy var bottomView: UIStackView = {...nested UI setup...}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
private func setupUI() {
let items = [
headerView,
middleView,
bottomView
]
items.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
contentView.addSubviews(items)
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
headerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
middleView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: padding),
middleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
middleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
bottomView.topAnchor.constraint(equalTo: middleView.bottomAnchor, constant: padding),
bottomView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
bottomView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
])
let constraint = bottomView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
constraint.priority = .defaultLow
constraint.isActive = true
}
#MainActor
func updateUI() {
func updateHeader() {
//...colors & other ui...
}
func updateMiddle() {
middleView.addArrangedSubview(titleLabel)
middleView.addArrangedSubview(descriptionLabel)
guard let constraint = titleLabel.getConstraint(identifier: "height"),
let constraint2 = descriptionLabel.getConstraint(identifier: "height")
else { return }
titleLabel.text = item.title
descriptionLabel.text = item.truncatedDescription
//Tried to force layout - didn't help
// middleView.setNeedsLayout()
//calc ptx height
constraint.constant = item.title.height(withConstrainedWidth: bounds.width, font: titleLabel.font)
//Media
if let media = item.media {
middleView.addArrangedSubview(imageContainer)
if let image = media.image {
imageView.image = image
} else if !media.imageURL.isNil {
guard let shimmer = imageContainer.getSubview(type: Shimmer.self) else { return }
shimmer.startShimmering()
Task { [weak self] in
guard let self = self else { return }
try await media.downloadImageAsync()
media.image.publisher
.sink {
self.imageView.image = $0
shimmer.stopShimmering()
}
.store(in: &self.subscriptions)
}
}
}
constraint2.constant = item.truncatedDescription.height(withConstrainedWidth: bounds.width, font: descriptionLabel.font)
// middleView.layoutIfNeeded()
}
func updateBottom() { //...colors & other ui... }
updateHeader()
updateMiddle()
updateBottom()
}
override func prepareForReuse() {
super.prepareForReuse()
//UI cleanup
middleView.removeArrangedSubview(titleLabel)
middleView.removeArrangedSubview(descriptionLabel)
middleView.removeArrangedSubview(imageContainer)
titleLabel.removeFromSuperview()
descriptionLabel.removeFromSuperview()
imageContainer.removeFromSuperview()
imageView.image = nil
}
}
Tried to force layout in UICollectionViewDelegate, it didn't help:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
Why is layout engine behaving so strange and how to fix it?
Hard to debug layout issues. But I don't believe you need to remove your views, like so:
override func prepareForReuse() {
super.prepareForReuse()
middleView.removeArrangedSubview(titleLabel)
middleView.removeArrangedSubview(descriptionLabel)
middleView.removeArrangedSubview(imageContainer)
titleLabel.removeFromSuperview()
descriptionLabel.removeFromSuperview()
imageContainer.removeFromSuperview()
That forced unnecessary layout steps, which might explain why your cell re-layout every time.
Instead just set the field's value to nill. Like you did with the image. When you call your function to updateUI() add the new values from that new item. You should not be updating your constraints here. Your contents' intrinsic content size will change and your cell should be able adapt if the constraints are defined correctly in the initial setupUI() method.
Auto layout constraints are linear equations, so when a variable changes they should be able to adapt once the layout engine recalculates the values.
It may take some time to get it right. You might have to play around with compression resistance and content hugging priorities to ensure the test field doesn't shrink, A good rule of thumb is to try and simplify your constraints as much as possible. Sorry cant be more specific as its hard to debug layout without a simulator.
Another point.
I doubt this is the root cause here. But a potential optimisation to keep in mind.
You seem to be using your model - Item - (which conforms to hashable) as the ItemIdentifierType of the diffable data source.
UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
This is indeed how many tutorials show it, yet it is no longer the recommended way. Using a hash of the entire object results in the cell being destroyed and added every time a field changes.
This is described in the docs
And has been explained in WWDC 21
The optimal approach is to pass the model / view model into your cell and update the layout it if some the model properties change. Kind of like what you are doing already. Combine is handy for this. This way cells are only destroyed when an entirely new model is added or removed from the data source.
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.
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
I want to open a view controller over an existing view controller via button click without using storyboards. How do I do this? Here is what I mean:
Let's say we have three view controllers I can scroll between:
"zeroVC", "oneVC", and "twoVC"
When I press a button on "twoVC" I want to now scroll between:
"zeroVC", "oneVC", and "threeVC"
I tried looking all through stack overflow but they all use storyboards.
Let's assume we have four view controllers: RedViewController, GreenViewController, BlueViewController, and the one to contain them all, ContainerViewController.
Although you mentioned a scrolling view controller with three children within, we'll make it a two screen setup to keep it simple.
The following approach is scalable, so you would easily adopt it with an arbitrary number of view controllers.
Our RedViewController is 7 lines long:
class RedViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
}
Before we move on to GreenViewController and BlueViewController, we will define protocol SwapViewControllerDelegate:
protocol SwapViewControllerDelegate: AnyObject {
func swap()
}
GreenViewController and BlueViewController will have a delegate that conforms to this protocol, which will handle the swapping.
We will make ContainerViewController conform to this protocol.
Note that SwapViewControllerDelegate has the AnyObject in its inheritance list to make it a class-only protocol–we can thus make the delegate weak, to avoid memory retain cycle.
The following is GreenViewController:
class GreenViewController: UIViewController {
weak var delegate: SwapViewControllerDelegate?
override func loadView() {
let view = UIView()
view.backgroundColor = .green
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.setTitle("Swap Me!", for: .normal)
button.setTitleColor(.black, for: .normal)
button.titleLabel?.font = .boldSystemFont(ofSize: 50)
button.addTarget(
self,
action: #selector(swapButtonWasTouched),
for: .touchUpInside)
view.addSubview(button)
// Put button at the center of the view
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc private func swapButtonWasTouched(_ sender: UIButton) {
delegate?.swap()
}
}
It has weak var delegate: SwapViewControllerDelegate? which will handle the swap when the button added in viewDidLoad is touched, triggering the swapButtonWasTouched method.
BlueViewController is implemented likewise:
class BlueViewController: UIViewController {
weak var delegate: SwapViewControllerDelegate?
override func loadView() {
let view = UIView()
view.backgroundColor = .blue
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.setTitle("Swap Me!", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .boldSystemFont(ofSize: 50)
button.addTarget(
self,
action: #selector(swapButtonWasTouched),
for: .touchUpInside)
view.addSubview(button)
// Put button at the center of the view
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc private func swapButtonWasTouched(_ sender: UIButton) {
delegate?.swap()
}
}
The only difference is the view's backgroundColor and the button's titleColor.
Finally, we'll take a look at ContainerViewController.
ContainerViewController has four properties:
class ContainerViewController: UIViewController {
let redVC = RedViewController()
let greenVC = GreenViewController()
let blueVC = BlueViewController()
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.bounces = false
scrollView.isPagingEnabled = true
return scrollView
}()
...
}
scrollView is the view that will contain child view controllers, redVC, greenVC, and blueVC.
We will use autolayout, so don't forget to mark translatesAutoresizingMaskIntoConstraints as false.
Now, setup autolayout constraints of the scrollView:
class ContainerViewController: UIViewController {
...
private func setupScrollView() {
view.addSubview(scrollView)
let views = ["scrollView": scrollView]
[
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[scrollView]|",
metrics: nil,
views: views),
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[scrollView]|",
metrics: nil,
views: views),
]
.forEach { NSLayoutConstraint.activate($0) }
}
...
}
I used VFL, but you can manually set autolayou constraints as we did for the button above.
Using autolayout, we don't have to set contentSize of the scrollView ourselves.
For more information about using autolayout with UIScrollView, see Technical Note TN2154: UIScrollView And Autolayout.
Now the most important setupChildViewControllers():
class ContainerViewController: UIViewController {
...
private func setupChildViewControllers() {
[redVC, greenVC, blueVC].forEach { addChild($0) }
let views = [
"redVC": redVC.view!,
"greenVC": greenVC.view!,
"blueVC": blueVC.view!,
]
views.values.forEach {
scrollView.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
$0.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
[
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[redVC][greenVC]|",
options: .alignAllTop,
metrics: nil,
views: views),
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[redVC][blueVC]|",
options: .alignAllTop,
metrics: nil,
views: views),
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[redVC(==greenVC,==blueVC)]|",
metrics: nil,
views: views),
]
.forEach { NSLayoutConstraint.activate($0) }
[redVC, greenVC, blueVC].forEach { $0.didMove(toParent: self) }
greenVC.view.isHidden = true
greenVC.delegate = self
blueVC.delegate = self
}
...
}
We first add each of [redVC, greenVC, blueVC] as child view controllers of ContainerViewController.
Then add the view's of child view controllers to scrollView.
Set widthAnchor and heightAnchor of the child view controllers to be view.widthAnchor and view.heightAnchor, in order to make them fullscreen.
Moreover, this will also work when the screen rotates.
Using views dictionary, we use VFL to set autolayout constraints.
We will put greenVC.view on the right of redVC.view: H:|[redVC][greenVC]|, and similarly for the blueVC.view: H:|[redVC][blueVC]|.
To fix the vertical position of greenVC.view and blueVC.view, add .alignAllTop option to the constraints.
Then apply vertical layout for redVC.view, and set the height of the greenVC.view and blueVC.view: "V:|[redVC(==greenVC,==blueVC)]|.
The vertical position is set, as we used .alignAllTop while setting the horizontal constraints.
We should call didMove(toParent:) methods on the child view controllers after we add then as child view controllers.
(If you are wondering about what didMove(toParent:) and addChild(_:) methods do, apparently they do very little; see What does addChildViewController actually do? and didMoveToParentViewController and willMoveToParentViewController.)
Finally, hide greenVC.view, and set greenVC.delegate and blueVC.delegate to self.
Then of course, we need ContainerViewController to conform to SwapViewControllerDelegate:
extension ContainerViewController: SwapViewControllerDelegate {
func swap() {
greenVC.view.isHidden.toggle()
blueVC.view.isHidden.toggle()
}
}
That's it!
The entire project is uploaded here.
I recommend reading Implementing a Container View Controller, which is well-documented by Apple. (It is written in Objective-C, but it is actually straightforward to translate into Swift)
In my app I have a collection view with cells autosizing horizontally.
Here's some code:
// called in viewDidLoad()
private func setupCollectionView() {
let cellNib = UINib(nibName: SomeCell.nibName, bundle: nil)
collectionView.register(cellNib, forCellWithReuseIdentifier: SomeCell.reuseIdentifier)
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
flowLayout.itemSize = UICollectionViewFlowLayout.automaticSize
}
The cell has 1 view, which has constraint for heigth. This view subviews a label, which is limited with 2 rows and is not limited by width. The idea here is to allow label to calculate its own width fitting text in 2 rows.
In order to make this work I've added the following code to the cell class:
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
Now, it works perfectly. Cells are autosizng, scrollView is scrolling horizontally etc. Until I call reloadData() at least once. Then cells have size 50x50 and never autosize any more until I leave the screen and come back again.
Do you have any ideas on why is it happening?
I was reading about auto layout rendering pipelines i mean how auto layout work under the hood. There are some methods which get called at different stages of autoLayout rendering like
layoutIfNeeded()
layoutSubviews()
updateConstraints()
updateConstraintsIfNeeded()
but i don't know which method is called when and what is the significance of that method and if i want to use auto layout then in which order i can use that methods and how can i control the autoLayout rendering pipeline
Usually you don't need to care about the autolayout method chain. You just need to create the constraints for the views to define their sizes and positions. You can add/remove, activate/deactivate constraints anytime in lifecycle of the view, but you want to always have a set of satisfiable (non-conflicting), yet complete set of constraints.
Take an example. You can tell the autolayout that button A should be 50 points wide, 20 points high, with its left top corner positioned at point (0,0) in the viewController's view. Now, this is non-conflicting, yet complete set of constraints for the button A. But lets say you want to expand that button, when the user taps it. So in the tap handler you will add one new constraint saying that the button should be 100 points wide - now you have unsatisfiable constraints - there is a constraint say it should be 50 points wide, and another one saying it shoul be 100 points wide. Therefore, to prevent conflict, before activating the new constraint, you have to deactivate the old one. Incomplete constraints is an opposite case, lets say you deactivate the old width constraint, but never activate the new one. Then autolayout can calculate position (because there are constraints defining it), and height, but not width, which usually ends in undefined behavior (now in case of a UIButton that's not true, because it has intrinsic size, which implicitly defines its width and height, but I hope you get the point).
So when you create those constraints is up to you (in my example you were manipulating them when the user tapped the button). Usually you start in initializer in case of a UIView subclass or in loadView in UIViewController subclass and there you can define and activate the default set of constraints. And then you can use handlers to react to user activity. My recommendation is prepare all the constraints in loadView, keep them in properties, and activate/deactivate them when necessary.
But there are of course some limitation as when and how not to create new constraints - for a more detailed discussion of those cases I really recommend looking at Advanced Autolayout Toolbox by objc.io.
EDIT
See following example of a simple custom SongView that uses autolayout for layout and supports also some dynamic changes in constraints by activating/deactivating them. You can just simply copy paste the whole code into a playground and test it out there, or include it in a project.
Notice there that I don't call any of the autolayout lifecycle methods, except of setNeedsLayout and layoutIfNeeded. setNeedsLayout sets a flag telling the autolayout that constraints have been changed, and layoutIfNeeded then tells it to recalculate frames. Normally, that would happen automatically, but to animate the constraints changes we need to tell it explicitly - see the setExpanded method in SongView. For more detailed explanation of using autolayout in animations, see my different answer.
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let songView = SongView()
let button = UIButton()
override func loadView() {
super.loadView()
view.backgroundColor = .white
self.view.addSubview(button)
self.view.addSubview(songView)
button.setTitle("Expand/Collapse", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(expandCollapse), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
songView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// button has intrinsic size, no need to define constraints for size, position is enough
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
// songView has defined its height (see SongView class), but not width, therefore we need more constraints
songView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
songView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
songView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
}
#objc func expandCollapse() {
if songView.isExpanded {
songView.setExpanded(to: false, animated: true)
} else {
songView.setExpanded(to: true, animated: true)
}
}
}
class SongView: UIView {
private let numberLabel: UILabel = UILabel()
private let nameLabel: UILabel = UILabel()
private var expandedConstraints: [NSLayoutConstraint] = []
private var collapsedConstraints: [NSLayoutConstraint] = []
// this can be triggered by some event
private(set) var isExpanded: Bool = false
func setExpanded(to expanded: Bool, animated: Bool) {
self.isExpanded = expanded
if animated {
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
self.setNeedsLayout()
UIView.animate(withDuration: 0.2, animations: {
self.layoutIfNeeded()
})
} else {
// non animated version (no need to explicitly call setNeedsLayout nor layoutIfNeeded)
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
}
}
var data: (String, String)? {
didSet {
numberLabel.text = data?.0
nameLabel.text = data?.1
}
}
init() {
super.init(frame: CGRect.zero)
setupInitialHierarchy()
setupInitialAttributes()
setupInitialLayout()
}
fileprivate func setupInitialHierarchy() {
self.addSubview(numberLabel)
self.addSubview(nameLabel)
}
fileprivate func setupInitialAttributes() {
numberLabel.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).pointSize)
numberLabel.textColor = UIColor.darkGray
numberLabel.text = "0"
numberLabel.textAlignment = .right
nameLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
nameLabel.text = "NONE"
nameLabel.textAlignment = .left
self.backgroundColor = UIColor.lightGray
}
fileprivate func setupInitialLayout() {
self.translatesAutoresizingMaskIntoConstraints = false
numberLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
// just randomly selected different layouts for collapsed and expanded states
expandedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 35),
self.heightAnchor.constraint(equalToConstant: 80),
]
collapsedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 50),
self.heightAnchor.constraint(equalToConstant: 40),
]
// activating collapsed as default layout
NSLayoutConstraint.activate(collapsedConstraints)
NSLayoutConstraint.activate([
numberLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 4),
numberLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4),
numberLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 4),
nameLabel.centerYAnchor.constraint(equalTo: numberLabel.centerYAnchor),
nameLabel.leftAnchor.constraint(equalTo: numberLabel.rightAnchor, constant: 8),
nameLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -4)
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
PlaygroundPage.current.liveView = ViewController()