iOS adding constraints programmatically in Swift - ios

So, I'm done using the IB in Xcode and want to write all UI in Swift.
So what I've done is:
Created a new UIView to contain the elements i want to write - lets call it "TestView"
I've added TestView to a VC as a sub view.
In the TestView class I've added the elements like this:
class TestView: UIView {
var someLabel:UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
self.someLabel = UILabel(frame: CGRect(x: self.frame.midX, y: oneSixthHeight, width: 100, height: 22))
self.someLabel.text = "test"
var constraints:[NSLayoutConstraint] = []
self.someLabel.translatesAutoresizingMaskIntoConstraints = false
let rightsideAnchor:NSLayoutConstraint = NSLayoutConstraint(item: self.someLabel, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 1)
constraints.append(rightsideAnchor)
NSLayoutConstraint.activateConstraints(constraints)
}
}
With this I expect the UILabel to be anchored to the right side of the view.
However, I do get this error:
Terminating app due to uncaught exception 'NSGenericException',
reason: 'Unable to activate constraint with items > and > because they have no common ancestor.
Does the constraint reference items in different view hierarchies? That's illegal.'
What am I doing wrong?

You should add constraints only after view is added to the view hierarchy. From your code it is clear that you have not added the UILabel instance to view.

Updated for Swift 3
import UIKit
class ViewController: UIViewController {
let redView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupAutoLayout()
}
func setupViews() {
view.backgroundColor = .white
view.addSubview(redView)
}
func setupAutoLayout() {
// Available from iOS 9 commonly known as Anchoring System for AutoLayout...
redView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
redView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
redView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
redView.heightAnchor.constraint(equalToConstant: 300).isActive = true
// You can also modified above last two lines as follows by commenting above & uncommenting below lines...
// redView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
// redView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
Type of Constraints:
/*
// regular use
1.leftAnchor
2.rightAnchor
3.topAnchor
// intermediate use
4.widthAnchor
5.heightAnchor
6.bottomAnchor
7.centerXAnchor
8.centerYAnchor
// rare use
9.leadingAnchor
10.trailingAnchor
etc. (note: very project to project)
*/

Related

Unable to Anchor UIImageVIew in UICollectionViewCell (programmatically)

Working on specing out a view in Playground and can't seem to figure out why UIIMageView is being placed in the center of a UICollectionViewCell.
Relevant Code:
class BookCell: UICollectionViewCell {
static let identifier = "bookCell"
override init(frame: CGRect) {
super.init(frame: .zero)
self.layer.cornerRadius = 12
self.backgroundColor = .brown
addAllSubviews()
addAllConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var cover: UIImageView = {
let imageview = UIImageView()
imageview.translatesAutoresizingMaskIntoConstraints = false
var largeImage = UIImage(named: "medium.jpg")
imageview.image = largeImage
imageview.contentMode = .scaleAspectFit
//imageview.contentMode = .scaleToFill
return imageview
}()
func coverConstraints(){
NSLayoutConstraint.activate([
cover.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0),
/**widthConstraint*/
NSLayoutConstraint(item: cover,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 1.0, constant: 0.0),
/**heightConstraint*/
NSLayoutConstraint(item: cover,
attribute: .height,
relatedBy: .equal,
toItem: self,
attribute: .height,
multiplier: 0.75, constant: 0.0)
])
}
let wordLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "test"
return label
}()
func wordLabelConstraints() {
NSLayoutConstraint.activate([
wordLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
wordLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
wordLabel.topAnchor.constraint(equalTo: cover.bottomAnchor, constant: 2)
])
}
// MARK: - Add Subviews
func addAllSubviews() {
self.addSubview(cover)
self.addSubview(wordLabel)
}
// MARK: - SubViews Constraints
func addAllConstraints() {
coverConstraints()
wordLabelConstraints()
}
}
BookCell is then used in a UICollectionViewController like so:
class ViewController: UIViewController {
fileprivate let collectionView: UICollectionView = {
let layout = ColumnFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .blue
cv.translatesAutoresizingMaskIntoConstraints = false
return cv
}()
var data: [Int] = Array(0..<10)
override func loadView() {
super.loadView()
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.register(BookCell.self, forCellWithReuseIdentifier: BookCell.identifier)
self.collectionView.alwaysBounceVertical = true
self.collectionView.backgroundColor = .yellow
}
}
Result:
I noticed that using scaleToFill instead of scaleAspectFit results in image covering the entire width of the cell. The result (see image below) fits what I am aiming for but ... see question below
Question:
Is using scaleToFill the only way to pin an image to the edges (leading and trailing) of UICollectionViewCell. If so, why is this?
I also tried adding the UIImageView to a UIStackView and I believe I got the same results.
Please note that I am not interested in doing this via Storyboard.
Thank you for providing feedback
There is one more option: .scaleAspectFill but may be cropped your image's content.
The option to scale the content to fill the size of the view.
Some portion of the content may be clipped to fill the view’s bounds.
Think about getting the image size ratio (width/height), you having a fixed width based on superview, and height will be based on the image Ratio.

Dynamic Constraint for UIView & UICollection

On the top of my screen will show one of two UIViews.
One is the minimized version and the other is the maximized verison.
The minimized view is 30 in height while the maximized version is 250.
Underneath this there is a UICollectionView.
I want it so that when the minimized version of the UIView is showing, the UICollectionView's topAnchor will be connected to the UIViews bottomAnchor.
When I click on each of the UIViews they will become hidden and make the other one visible.
Here are some screenshots to help visualize:
Default
Minimized
Attempt to maximize
So when I show the maximized UIView I want the UICollectionViews topAnchor to be connected to that ones bottomAnchor and so forth.
Currently everything is working except the proper resizing of the UICollectionView.
It will resize up when I minimize, but will not resize down when I maximize.
My viewDidLoad calls both of these functions:
private func configureMaxView() {
view.addSubview(maxView)
maxView.layer.cornerRadius = 18
maxView.backgroundColor = .secondarySystemBackground
maxView.translatesAutoresizingMaskIntoConstraints = false
maxView.isHidden = isMaxViewHidden
let gesture = UITapGestureRecognizer(target: self, action: #selector (self.minimizeAction (_:)))
self.maxView.addGestureRecognizer(gesture)
let padding: CGFloat = 20
NSLayoutConstraint.activate([
maxView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
maxView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
maxView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
maxView.heightAnchor.constraint(equalToConstant: 250),
])
}
private func configureMinView() {
view.addSubview(minView)
minView.layer.cornerRadius = 9
minView.backgroundColor = .secondarySystemBackground
minView.translatesAutoresizingMaskIntoConstraints = false
minView.isHidden = !isMaxViewHidden
let gesture = UITapGestureRecognizer(target: self, action: #selector (self.expandAction (_:)))
self.minView.addGestureRecognizer(gesture)
let padding: CGFloat = 20
NSLayoutConstraint.activate([
minView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
minView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
minView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
minView.heightAnchor.constraint(equalToConstant: 30),
])
}
Here are the functions that are called when you click on one of the UIViews:
#objc func minimizeAction(_ sender:UITapGestureRecognizer){
minView.isHidden = false
maxView.isHidden = true
// without this call the hiding and unhiding works fine - But the collectionview won't move
resizeCollectionView()
isMaxViewHidden = !isMaxViewHidden
}
#objc func expandAction(_ sender:UITapGestureRecognizer){
minView.isHidden = true
maxView.isHidden = false
// without this call the hiding and unhiding works fine - But the collectionview won't move
resizeCollectionView()
isMaxViewHidden = !isMaxViewHidden
}
My viewDidLoad will also call this after setting up the uiviews in order to set up the collectionView:
private func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createThreeColumnFlowLayout(in: view))
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.backgroundColor = .systemBackground
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: CustomCell.resuseID)
collectionView.translatesAutoresizingMaskIntoConstraints = false
let bottomAnchor = isMaxViewHidden ? minView.bottomAnchor : maxView.bottomAnchor
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
Here is the function I call from each uiview click handler in an attempt to update the constraint:
private func resizeCollectionView() {
let bottomAnchor = isMaxViewHidden ? maxView.bottomAnchor: minView.bottomAnchor
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: bottomAnchor),
])
// This does move the collection view down, but seems hardcoded and bad
//collectionView.frame.origin.y = 650
}
Error:
"<NSLayoutConstraint:0x600000c4ed50 UIView:0x7ffd60c135e0.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top (active)>",
"<NSLayoutConstraint:0x600000c4fa20 UIView:0x7ffd60c135e0.height == 250 (active)>",
"<NSLayoutConstraint:0x600000c4bed0 UIView:0x7ffd60c13750.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top (active)>",
"<NSLayoutConstraint:0x600000c3c000 UIView:0x7ffd60c13750.height == 30 (active)>",
"<NSLayoutConstraint:0x600000c3dbd0 V:[UIView:0x7ffd60c135e0]-(0)-[UICollectionView:0x7ffd62819600] (active)>",
"<NSLayoutConstraint:0x600000c20cd0 V:[UIView:0x7ffd60c13750]-(0)-[UICollectionView:0x7ffd62819600] (active)>"
)
Will attempt to recover by breaking constraint
"<NSLayoutConstraint:0x600000c4ed50 UIView:0x7ffd60c135e0.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top (active)>",
"<NSLayoutConstraint:0x600000c4fa20 UIView:0x7ffd60c135e0.height == 250 (active)>",
"<NSLayoutConstraint:0x600000c4bed0 UIView:0x7ffd60c13750.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top (active)>",
"<NSLayoutConstraint:0x600000c3c000 UIView:0x7ffd60c13750.height == 30 (active)>",
What's happening in here is that, even though the view is being hidden, the constraints are still there, so assigning numerous top constraints on a single view will result to a conflict.
If you want to have a sort of dynamic constraint, just modify the constraint's constant or multiplier values. That way, you wont need to worry about constraint objects.
Wilson's solution is right. What I can just add from that is just have a single containerView containing your min and max views. and just toggle the visibility of your min and max view inside your containerView.
Here's my (simple) take on it:
everything is called in viewDidLoad() as you do, and
you also need to define a constraint object.
private var containerViewHeight: NSLayoutConstraint!
private func configureContainerView() {
containerView = UIView()
containerView.backgroundColor = .systemGreen
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(containerViewTapped)))
view.addSubview(containerView)
containerViewHeight = containerView.heightAnchor.constraint(equalToConstant: 250)
containerViewHeight.isActive = true
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
])
}
#objc private func containerViewTapped() {
isViewTapped.toggle()
containerViewHeight.constant = isViewTapped ? 30 : 250
innerMinView.isHidden = !isViewTapped
innerMaxView.isHidden = isViewTapped
}
private func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
view.addSubview(collectionView)
collectionView.backgroundColor = .systemYellow
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
private func configureInnerMinView() {
innerMinView = UIView()
innerMinView.backgroundColor = .systemRed
innerMinView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(innerMinView)
NSLayoutConstraint.activate([
innerMinView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.8),
innerMinView.heightAnchor.constraint(equalTo: containerView.heightAnchor, multiplier: 0.8),
innerMinView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
innerMinView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
])
}
private func configureInnerMaxView() {
innerMaxView = UIView()
innerMaxView.backgroundColor = .systemBlue
innerMaxView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(innerMaxView)
NSLayoutConstraint.activate([
innerMaxView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.8),
innerMaxView.heightAnchor.constraint(equalTo: containerView.heightAnchor, multiplier: 0.8),
innerMaxView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
innerMaxView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
])
}
side note: btw. I think I know what project this is. :) SA-GH.F
You're activating conflicting constraints on top of each other. If you want to go between a minimized and maximized view, just change the constant value on the height constraint itself.
I whipped up a sample MyViewController in playgrounds with 2 views, the first of which's height and color changes when you tap it. myView is like your collapsable view, and myOtherView is like your collectionView.
Let me know if you have any questions!
class MyViewController : UIViewController {
private enum ViewState {
case min
case max
var height: CGFloat {
switch self {
case .min:
return 30
case .max:
return 250
}
}
var color: UIColor {
switch self {
case .min:
return .red
case .max:
return .green
}
}
}
private var myViewState: ViewState = .min {
didSet {
viewStateToggled()
}
}
private let myView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let myOtherView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
private lazy var myViewHeightConstraint = NSLayoutConstraint(
item: myView,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1,
constant: myViewState.height
)
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
configureMyViews()
}
private func configureMyViews() {
view.addSubview(myView)
myView.backgroundColor = myViewState.color
NSLayoutConstraint.activate([
myView.topAnchor.constraint(equalTo: view.topAnchor),
myView.leftAnchor.constraint(equalTo: view.leftAnchor),
myView.rightAnchor.constraint(equalTo: view.rightAnchor),
myViewHeightConstraint
])
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
myView.addGestureRecognizer(tap)
view.addSubview(myOtherView)
NSLayoutConstraint.activate([
myOtherView.topAnchor.constraint(equalTo: myView.bottomAnchor),
myOtherView.leftAnchor.constraint(equalTo: view.leftAnchor),
myOtherView.rightAnchor.constraint(equalTo: view.rightAnchor),
myOtherView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func viewStateToggled() {
myViewHeightConstraint.constant = myViewState.height
UIView.animate(withDuration: 0.4) {
self.myView.backgroundColor = self.myViewState.color
self.view.layoutSubviews()
}
}
#objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
myViewState = myViewState == .min ? .max : .min
}
}
I was wondering if you need a call to update your layout. Maybe called from the resizeCollectionView()?
let bottomAnchorMinContraint: NSConstraint = collectionView.topAnchor.constraint(equalTo: minView.bottomAnchor)
let bottomAnchorMaxContraint: NSConstraint = collectionView.topAnchor.constraint(equalTo: maxView.bottomAnchor)
if isMaxViewHidden {
bottomAnchorMinContraint.isActive = true
bottomAnchorMaxContraint.isActive = false
}
if !isMaxViewHidden {
bottomAnchorMinContraint.isActive = false
bottomAnchorMaxContraint.isActive = true
}
view.layoutIfNeeded()

How to embed a UIViewController's view into another view without breaking responder chain

I'm trying to implement a base class for View Controllers that will add a top banner that is shown and hidden based on a specific status. Because my UIViewController extend this base class the view property is already loaded by the time I have to embed the original view property into a new view property that is created programmatically.
The embedding works fine (UI wise) the views adjust themselves appropriately however the issue that I'm seeing is that after embedding it the embedded views no longer respond to touch events, in the controller that I'm originally embedding I've got a table view with 16 rows and a button, before embedding it they both respond to tap and scroll events correctly.
I'm constrained to the fact that I cannot use a split view controller in IB to achieve the dual view split.
Here is my current implementation, can't seem to figure out what I'm missing to have event's propagate, I've tried using a custom view that overrides hitTest() to no avail for both newRootView and contentView variables.
Any help or insights are really appreciated, thanks!
class BaseViewController: UIViewController {
var isInOfflineMode: Bool {
didSet { if isInOfflineMode { embedControllerView() }}
}
var offlineBanner: UIView!
var contentView: UIView!
private func embedControllerView() {
guard let currentRoot = viewIfLoaded else { return }
currentRoot.translatesAutoresizingMaskIntoConstraints = false
let newRootView = UIView(frame: UIScreen.main.bounds)
newRootView.backgroundColor = .yellow // debugging
newRootView.isUserInteractionEnabled = true
newRootView.clipsToBounds = true
view = newRootView
offlineBanner = createOfflineBanner() // returns a button that I've verified to be tapable.
view.addSubview(offlineBanner)
contentView = UIView(frame: .zero)
contentView.backgroundColor = .cyan // debugging
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.isUserInteractionEnabled = true
view.addSubview(contentView)
contentView.addSubview(currentRoot)
let bannerHeight: CGFloat = 40.00
var topAnchor: NSLayoutYAxisAnchor
var bottomAnchor: NSLayoutYAxisAnchor
var trailingAnchor: NSLayoutXAxisAnchor
var leadingAnchor: NSLayoutXAxisAnchor
if #available(iOS 11.0, *) {
topAnchor = view.safeAreaLayoutGuide.topAnchor
bottomAnchor = view.safeAreaLayoutGuide.bottomAnchor
leadingAnchor = view.safeAreaLayoutGuide.leadingAnchor
trailingAnchor = view.safeAreaLayoutGuide.trailingAnchor
} else {
topAnchor = view.topAnchor
bottomAnchor = view.bottomAnchor
leadingAnchor = view.leadingAnchor
trailingAnchor = view.trailingAnchor
}
NSLayoutConstraint.activate([
offlineBanner.heightAnchor.constraint(equalToConstant: bannerHeight),
offlineBanner.topAnchor.constraint(equalTo: topAnchor),
offlineBanner.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
offlineBanner.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
])
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor, constant: bannerHeight),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0),
contentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
])
OfflineViewController.migrateViewContraints(from: currentRoot, to: contentView)
view.setNeedsUpdateConstraints()
offlineBanner.setNeedsUpdateConstraints()
currentRoot.setNeedsUpdateConstraints()
}
private func unembedControllerView() {
let v = contentView.subviews[0]
v.removeFromSuperview()
view = v
OfflineViewController.migrateViewContraints(from: contentView, to: v)
}
/**
Replaces any constraints associated with the current root's safe area`UILayoutGuide` or with the actual
current root view.
*/
private static func migrateViewContraints(from currentRoot: UIView, to newRoot: UIView) {
for ct in currentRoot.constraints {
var firstItem: Any? = ct.firstItem
var secondItem: Any? = ct.secondItem
if #available(iOS 11.0, *) {
if firstItem as? UILayoutGuide == currentRoot.safeAreaLayoutGuide {
debugPrint("Migrating firstItem is currentLayoutGuide")
firstItem = newRoot.safeAreaLayoutGuide
}
if secondItem as? UILayoutGuide == currentRoot.safeAreaLayoutGuide {
debugPrint("Migrating secondItem is currentLayoutGuide")
secondItem = newRoot.safeAreaLayoutGuide
}
}
if firstItem as? UIView == currentRoot {
debugPrint("Migrating firstItem is currentRoot")
firstItem = newRoot
}
if secondItem as? UIView == currentRoot {
debugPrint("Migrating secondItem is currentRoot")
secondItem = newRoot
}
NSLayoutConstraint.deactivate([ct])
NSLayoutConstraint.activate([
NSLayoutConstraint(item: firstItem as Any,
attribute: ct.firstAttribute,
relatedBy: ct.relation,
toItem: secondItem,
attribute: ct.secondAttribute,
multiplier: ct.multiplier,
constant: ct.constant)
])
}
}
}
In this specific view the green buttons does get events it's a button that I create programmatically:
And here's the view that does not respond to events, a table view with a button:
I've figured what the issue was; I was missing constraints between contentView (the new root view) and the original self.view. The constraints essentially made the original currentRoot view take the full space of contentView:
NSLayoutConstraint.activate([
currentRoot.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
currentRoot.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0),
currentRoot.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0),
currentRoot.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0),
])

How to position a subView below a subView with dynamic height?

I have the following UIView:
It is a UIView which contains three subviews. A regular UILabel (Hello World) at the top, a custom UIViewController (CategoryList) which contains a CollectionView with buttons (alpha,beta, ...) and another custom UIViewController with just a label (SALUT).
I do auto-layout programmatically and position SALUT (var sCtrl) below the CategoryList (var catList) with
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
This results in the picture above, where SALUT is not positioned below the category list as I would like it to be. I sort of understand why since when I set the constraint the buttons are not yet laid out properly in the CollectionView and thus the bottom anchor of the catList is not set.
In the CategoryList:UIVIewController I have the following in order to get the collection view to get the correct height.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
}
This works but since viewDidLayoutSubviews() is called after the constraint is set on SALUT the SALUT position is wrong. How can I get it right in an easy way? (I guess I could make a controller for my main view and check there when subviews are laid out and then update subview positions but this seems overly complicated to me).
The full code for the main view is below so you can see the layout positioning code. If needed I can also provide the subviews code but I suppose it shouldn't be needed since they should be considered black boxes...
class MyView: UIView {
let label = UILabel()
var sCtrl : SubController
var catList : CategoryList
var topConstraint = NSLayoutConstraint()
override init(frame: CGRect) {
sCtrl = SubController()
catList = CategoryList()
super.init(frame: frame)
setup()
}
private func setup() {
self.backgroundColor = UIColor.green
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.yellow
label.text = "Hello world"
label.textAlignment = .center
self.addSubview(label)
catList.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(catList.view)
sCtrl.view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(sCtrl.view)
let margins = self.layoutMarginsGuide
label.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
label.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 0).isActive = true
label.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0).isActive = true
label.heightAnchor.constraint(equalToConstant: 100.0).isActive=true
catList.view.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0).isActive = true
catList.view.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 1.0).isActive = true
catList.view.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 1.0).isActive = true
catList.view.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant:0).isActive = true
sCtrl.view.topAnchor.constraint(equalTo: catList.view.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let v = MyView(frame: CGRect(x: 0, y: 0, width: 400, height: 600))
Ok, I found the issue. I had missed to add the height constraint to the CategoryList's own view. Adding this it works just fine.
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightConstraint = collectionView.heightAnchor.constraint(equalToConstant: collectionView.collectionViewLayout.collectionViewContentSize.height)
self.view.addConstraint(heightConstraint)
//THE MISSING LINE!!
self.view.heightAnchor.constraint(equalTo: self.collectionView.heightAnchor, constant: 0).isActive = true
}

Adding constraints to UITableViewCell contentView

I am trying to add constraints to tableViewCellSubViews, like so -
import UIKit
class SnakeTableViewCell: UITableViewCell {
var lessonViews = Array<UIView>()
override func awakeFromNib() {
super.awakeFromNib()
for var i = 0; i < 3; ++i
{
var view = UIView(frame: CGRectMake(CGFloat(i) * 110.0, 0.0, 100.0, 100.0))
view.backgroundColor = UIColor.redColor()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
self.contentView.addSubview(view)
lessonViews.append(view)
}
self.addConstraints(NSLayoutConstraint.constraintsForEvenDistributionOfViews(lessonViews, relativeToCenterOfView: self, vertically: false))
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
And the constraints code -
extension NSLayoutConstraint {
class func constraintsForEvenDistributionOfViews(views:Array<UIView>,relativeToCenterOfView toView:UIView, vertically:Bool ) -> Array<NSLayoutConstraint> {
var constraints = Array<NSLayoutConstraint>()
let attribute = vertically ? NSLayoutAttribute.CenterY : NSLayoutAttribute.CenterX
for (index, view) in enumerate(views) {
let multiplier = CGFloat(2*index + 2) / CGFloat(views.count + 1)
let constraint = NSLayoutConstraint(item: view, attribute: attribute, relatedBy: NSLayoutRelation.Equal, toItem:toView, attribute: attribute, multiplier: multiplier, constant: 0)
constraints.append(constraint)
}
return constraints
}
}
The issue is that when I add the constraints, all the subviews disappears.
Any idea what am I doing wrong ?
Thanks
The multiplier generally is 1. The constant is the variable (horizontal or vertical amount.
You can setup your custom cell, its evenly distributed subViews, and its constraints, all within Storyboard. It's much simpler to do it in Interface Builder, than creating and constraining views in code.
You simply dequeue a reusable cell, and it's already got its properly spaced views, because the cell instantiated and spaced them for you.
You can add constraints to the contentView of a UITableViewCell in init(style:, reuseIdentifier:) as follows (in this case adjusting with an inset of 5pt):
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 5).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true
contentView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -5).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5).isActive = true
It works, but... the system will complain:
Changing the translatesAutoresizingMaskIntoConstraints property of the contentView of a UITableViewCell
is not supported and will result in undefined behavior
The warning appears only one time (even though I have multiple cells)
I am doing this to add an inner border and a shadow (without using additional views that will affect the smooth scroll). But the 'not supported' and 'undefined behavior' are things you may consider if you want to use this in a production environment.

Resources