Related
As I want to move away from xib and make my layout programmatically, I found that using the same exact constraints doesn't work as I would expect.
I want to make this UITableViewCell
It's a quite simple cell with a small icon to its right as well as an Activity Indicator so I can toggle which one I want to see. They are inside a View and to their left is a label
Those are my constraints in the outline view
And it works perfectly. However when I'm removing the XIB and doing all of the code myself, nothing works anymore
So here's my code:
class StandardRow: UITableViewCell {
private var initialWidth: CGFloat = 20
public var fetching: Bool = false {
didSet {
if (fetching) {
activityIndicator?.startAnimating()
} else {
activityIndicator?.stopAnimating()
}
changeImageWidth()
}
}
public var rightImage: UIImage? = nil {
didSet {
rightImageView?.image = rightImage
changeImageWidth()
}
}
private func changeImageWidth() {
if (activityIndicator?.isAnimating) ?? false || rightImage != nil {
imageWidth?.constant = initialWidth
} else {
imageWidth?.constant = 0
}
}
override func prepareForReuse() {
valueLabel?.text = ""
imageView?.image = nil
rightImage = nil
fetching = false
textLabel?.text = ""
accessoryType = .none
}
//Views
private var imageContainer = UIView()
private var rightImageView = UIImageView()
private var activityIndicator: UIActivityIndicatorView? = UIActivityIndicatorView()
public var valueLabel: UILabel? = UILabel()
private var imageWidth: NSLayoutConstraint? = nil
override init(style: UITableViewCell.CellStyle = .default, reuseIdentifier: String? = nil) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
buildView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
buildView()
}
func buildView() {
contentView.addSubview(valueLabel!)
imageContainer.addSubview(rightImageView)
imageContainer.addSubview(activityIndicator!)
contentView.addSubview(imageContainer)
imageContainer.backgroundColor = .red
}
override func layoutSubviews() {
super.layoutSubviews()
//IMAGE CONTAINER CONSTRAINTS
imageWidth = NSLayoutConstraint(item: imageContainer, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: initialWidth)
imageWidth?.priority = UILayoutPriority(rawValue: 999)
imageWidth?.isActive = true
let bottomImageContainerConstraint = NSLayoutConstraint(item: imageContainer, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1, constant: 0)
bottomImageContainerConstraint.isActive = true
bottomImageContainerConstraint.priority = UILayoutPriority(rawValue: 999)
let topImageContainerConstraint = NSLayoutConstraint(item: imageContainer, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1, constant: 0)
topImageContainerConstraint.isActive = true
topImageContainerConstraint.priority = UILayoutPriority(rawValue: 999)
let trailingImageContainerConstraint = NSLayoutConstraint(item: imageContainer, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: 5)
trailingImageContainerConstraint.priority = UILayoutPriority(rawValue: 999)
trailingImageContainerConstraint.isActive = true
let centerYImageContainerConstraint = NSLayoutConstraint(item: imageContainer, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)
centerYImageContainerConstraint.isActive = true
centerYImageContainerConstraint.priority = UILayoutPriority(rawValue: 999)
//VALUE LABEL CONSTRAINTS
let trailingValueLabelConstraint = NSLayoutConstraint(item: valueLabel!, attribute: .trailing, relatedBy: .equal, toItem: imageContainer, attribute: .leading, multiplier: 1, constant: 5)
trailingValueLabelConstraint.isActive = true
trailingValueLabelConstraint.priority = UILayoutPriority(rawValue: 999)
let centerYValueLabelConstraint = NSLayoutConstraint(item: valueLabel!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)
centerYValueLabelConstraint.isActive = true
centerYValueLabelConstraint.priority = UILayoutPriority(rawValue: 999)
//ACTIVITY INDICATOR CONSTRAINGS
NSLayoutConstraint(item: activityIndicator!, attribute: .trailing, relatedBy: .equal, toItem: imageContainer, attribute: .trailing, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: activityIndicator!, attribute: .leading, relatedBy: .equal, toItem: imageContainer, attribute: .leading, multiplier: 1, constant: 11).isActive = false
NSLayoutConstraint(item: activityIndicator!, attribute: .bottom, relatedBy: .equal, toItem: imageContainer, attribute: .bottom, multiplier: 1, constant: 11).isActive = false
NSLayoutConstraint(item: activityIndicator!, attribute: .top, relatedBy: .equal, toItem: imageContainer, attribute: .top, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: activityIndicator!, attribute: .centerY, relatedBy: .equal, toItem: imageContainer, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
//RIGHT IMAGE VIEW CONSTRAINTS
NSLayoutConstraint(item: rightImageView, attribute: .trailing, relatedBy: .equal, toItem: activityIndicator!, attribute: .trailing, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rightImageView, attribute: .leading, relatedBy: .equal, toItem: rightImageView, attribute: .leading, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rightImageView, attribute: .bottom, relatedBy: .equal, toItem: activityIndicator!, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rightImageView, attribute: .top, relatedBy: .equal, toItem: activityIndicator!, attribute: .top, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: rightImageView, attribute: .centerY, relatedBy: .equal, toItem: activityIndicator!, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
//changeImageWidth()
}}
So I have a few ideas to where it can come from, firstly being "translatesAutoresizingMaskIntoConstraints" set to true by default, but when I'm setting it to false in the superview then my cell doesn't show anymore and in the contentView, Xcode tells me I shouldn't do that because of an undefined behaviour
I'm also using Reveal to debug my UI and then I found those peculiar values:
Which is not what I want, Reveal is reporting that those constraints are translating the autoresizing mask of the view to autolayout so it would confirm the previous theory. I did set the priority to 999 to some of the constraints because otherwise they would be broken.
I'm actually at a dead end and I think I'm missing something but I can't pinpoint what as I don't have enough experience with non-interface builder constraints
Try Anchors, it's much easier.
Example
var redView = UIView()
redView.backgroundColor = .red
anyView.addsubView(redView)
redView.translatesAutoresizingMaskIntoConstraints = false
redView.centerXAnchor.constraint(equalTo: self.parentView.centerXAnchor).isActive = true
redView.centerYAnchor.constraint(equalTo: self.parentView.centerYAnchor).isActive = true
redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
redView.widthAnchor.constraint(equalToConstant: 100).isActive = true
You can add the same method to your UIView extension
func constrainToEdges(_ subview: UIView, top: CGFloat = 0, bottom: CGFloat = 0, leading: CGFloat = 0, trailing: CGFloat = 0) {
subview.translatesAutoresizingMaskIntoConstraints = false
let topContraint = NSLayoutConstraint(
item: subview,
attribute: .top,
relatedBy: .equal,
toItem: self,
attribute: .top,
multiplier: 1.0,
constant: top)
let bottomConstraint = NSLayoutConstraint(
item: subview,
attribute: .bottom,
relatedBy: .equal,
toItem: self,
attribute: .bottom,
multiplier: 1.0,
constant: bottom)
let leadingContraint = NSLayoutConstraint(
item: subview,
attribute: .leading,
relatedBy: .equal,
toItem: self,
attribute: .leading,
multiplier: 1.0,
constant: leading)
let trailingContraint = NSLayoutConstraint(
item: subview,
attribute: .trailing,
relatedBy: .equal,
toItem: self,
attribute: .trailing,
multiplier: 1.0,
constant: trailing)
addConstraints([
topContraint,
bottomConstraint,
leadingContraint,
trailingContraint])
}
I recommend using this framework for building constraint based layouts programmatically, it makes the process straightforward and faster. Take the setup for the contentView of this cell for example:
contentView.addSubview(descriptionLabel)
contentView.addSubview(amountLabel)
contentView.addSubview(dateLabel)
contentView.addSubview(bottomRightLabel)
constrain(descriptionLabel, amountLabel, dateLabel, bottomRightLabel) { desc, amount, date, bottomRight in
desc.top == desc.superview!.top + 16
desc.left == desc.superview!.left + 16
desc.right <= amount.left + 12
desc.bottom == date.top - 12
amount.centerY == desc.centerY
amount.right == amount.superview!.right - 12
date.left == date.superview!.left + 16
date.right <= bottomRight.left - 12
date.bottom == date.superview!.bottom - 16
bottomRight.centerY == date.centerY
bottomRight.right == bottomRight.superview!.right - 12
}
I am loading a custom view class from the nib at viewWillAppear. After loading the nib, I add it as a subview, then apply some constraints, then update the constraints. At that point I attempt to add a shadow to this view, but no shadow is displaying....
Just for some context, the ViewController has scrollview that contains a contentView. All of the view's content goes inside this contentView.
Here is my code:
ViewController
class MyViewController: UIViewController {
#IBOutlet weak var contentView: UIView!
var matches: [Match] = []
var matchViews: [MatchDetailsView] = []
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setMatches()
}
func setMatches() {
// Match One
let matchOneDetailsView = MatchDetailsView.instanceFromNib(match: matches.count > 0 ? matches[0] : nil, delegate: self, tag: 1)
contentView.addSubview(matchOneDetailsView)
matchOneDetailsView.translatesAutoresizingMaskIntoConstraints = false
let matchOneLeadingConstraint = NSLayoutConstraint(item: matchOneDetailsView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 16)
let matchOneTrailingConstraint = NSLayoutConstraint(item: matchOneDetailsView, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: -16)
let matchOneTopConstraint = NSLayoutConstraint(item: matchOneDetailsView, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1, constant: 32)
// Match Two
let matchTwoDetailsView = MatchDetailsView.instanceFromNib(match: matches.count > 1 ? matches[1] : nil, delegate: self, tag: 2)
contentView.addSubview(matchTwoDetailsView)
matchTwoDetailsView.translatesAutoresizingMaskIntoConstraints = false
let matchTwoLeadingConstraint = NSLayoutConstraint(item: matchTwoDetailsView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 16)
let matchTwoTrailingConstraint = NSLayoutConstraint(item: matchTwoDetailsView, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: -16)
let matchTwoTopConstraint = NSLayoutConstraint(item: matchTwoDetailsView, attribute: .top, relatedBy: .equal, toItem: matchOneDetailsView, attribute: .bottom, multiplier: 1, constant: 32)
// Match Three
let matchThreeDetailsView = MatchDetailsView.instanceFromNib(match: matches.count > 2 ? matches[2] : nil, delegate: self, tag: 3)
contentView.addSubview(matchThreeDetailsView)
matchThreeDetailsView.translatesAutoresizingMaskIntoConstraints = false
let matchThreeLeadingConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 16)
let matchThreeTrailingConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: -16)
let matchThreeTopConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .top, relatedBy: .equal, toItem: matchTwoDetailsView, attribute: .bottom, multiplier: 1, constant: 32)
let matchThreeBottomConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1, constant: -32)
NSLayoutConstraint.activate([matchOneLeadingConstraint, matchOneTrailingConstraint, matchOneTopConstraint, matchTwoLeadingConstraint, matchTwoTrailingConstraint, matchTwoTopConstraint, matchThreeLeadingConstraint, matchThreeTrailingConstraint, matchThreeTopConstraint, matchThreeBottomConstraint])
updateViewConstraints()
matchViews.append(matchOneDetailsView)
matchViews.append(matchTwoDetailsView)
matchViews.append(matchThreeDetailsView)
styleMatchViews()
}
func styleMatchViews() {
for view in matchViews {
view.addShadow(opacity: 0.25, yOffset: 0, xOffset: 0, radius: 5.0)
view.roundCorners(withRadius: 5.0)
}
}
}
MatchDetailsView
class MatchDetailsView: UIView {
class func instanceFromNib(match: Match?, delegate: MatchDetailsViewDelegate, tag: Int) -> MatchDetailsView {
let view = UINib(nibName: "MatchDetailsView", bundle: nil).instantiate(withOwner: MatchDetailsView(), options: nil)[0] as! MatchDetailsView
view.translatesAutoresizingMaskIntoConstraints = false
view.updateMatchContent()
return view
}
}
UIViewExtension
extension UIView {
/**
Add Shadow
Puts a Drop Shadow in the UIView
*/
func addShadow(opacity: Float, yOffset: Int, xOffset: Int, radius: CGFloat) {
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = opacity
self.layer.shadowOffset = CGSize(width: xOffset, height: yOffset)
self.layer.shadowRadius = radius
self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
}
}
If MatchDetailsView's nib file has 'clipsToBounds' set to false, why isn't my shadow showing up when the page is loaded? Am I missing a layout function even though they seem to be laid out correctly?
You are setting the .shadowPath to the bounds of the view... but the bounds at that point may not be (probably will not be) the bounds after auto-layout lays out the views.
For a "shadowed view" you are much better off setting the shadow inlayoutSubviews() inside the view itself.
You should also add and setup your subviews in viewDidLoad() viewWillAppear() may be called more than once (depending on your navigation), and you'll end up adding multiple copies of your subviews.
So...
Change your MatchDetailsView class like this:
class MatchDetailsView: UIView {
var opacity: CGFloat = 0.0
var xOffset: CGFloat = 0.0
var yOffset: CGFloat = 0.0
// for the shadowRadius
var sRadius: CGFloat = 0.0
// for the cornerRadius
var cRadius: CGFloat = 0.0
override func layoutSubviews() {
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = Float(opacity)
self.layer.shadowOffset = CGSize(width: xOffset, height: yOffset)
self.layer.shadowRadius = sRadius
self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
self.layer.cornerRadius = cRadius
}
class func instanceFromNib(match: Match?, delegate: MatchDetailsViewDelegate, tag: Int) -> MatchDetailsView {
let view = UINib(nibName: "MatchDetailsView", bundle: nil).instantiate(withOwner: MatchDetailsView(), options: nil)[0] as! MatchDetailsView
view.translatesAutoresizingMaskIntoConstraints = false
view.updateMatchContent()
return view
}
}
Then, in your view controller:
override func viewDidLoad() {
super.viewDidLoad()
setMatches()
}
func setMatches() {
// Match One
let matchOneDetailsView = MatchDetailsView.instanceFromNib(match: matches.count > 0 ? matches[0] : nil, delegate: self, tag: 1)
contentView.addSubview(matchOneDetailsView)
matchOneDetailsView.translatesAutoresizingMaskIntoConstraints = false
let matchOneLeadingConstraint = NSLayoutConstraint(item: matchOneDetailsView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 16)
let matchOneTrailingConstraint = NSLayoutConstraint(item: matchOneDetailsView, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: -16)
let matchOneTopConstraint = NSLayoutConstraint(item: matchOneDetailsView, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1, constant: 32)
// Match Two
let matchTwoDetailsView = MatchDetailsView.instanceFromNib(match: matches.count > 1 ? matches[1] : nil, delegate: self, tag: 2)
contentView.addSubview(matchTwoDetailsView)
matchTwoDetailsView.translatesAutoresizingMaskIntoConstraints = false
let matchTwoLeadingConstraint = NSLayoutConstraint(item: matchTwoDetailsView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 16)
let matchTwoTrailingConstraint = NSLayoutConstraint(item: matchTwoDetailsView, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: -16)
let matchTwoTopConstraint = NSLayoutConstraint(item: matchTwoDetailsView, attribute: .top, relatedBy: .equal, toItem: matchOneDetailsView, attribute: .bottom, multiplier: 1, constant: 32)
// Match Three
let matchThreeDetailsView = MatchDetailsView.instanceFromNib(match: matches.count > 2 ? matches[2] : nil, delegate: self, tag: 3)
contentView.addSubview(matchThreeDetailsView)
matchThreeDetailsView.translatesAutoresizingMaskIntoConstraints = false
let matchThreeLeadingConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 16)
let matchThreeTrailingConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1, constant: -16)
let matchThreeTopConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .top, relatedBy: .equal, toItem: matchTwoDetailsView, attribute: .bottom, multiplier: 1, constant: 32)
let matchThreeBottomConstraint = NSLayoutConstraint(item: matchThreeDetailsView, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1, constant: -32)
NSLayoutConstraint.activate([matchOneLeadingConstraint, matchOneTrailingConstraint, matchOneTopConstraint, matchTwoLeadingConstraint, matchTwoTrailingConstraint, matchTwoTopConstraint, matchThreeLeadingConstraint, matchThreeTrailingConstraint, matchThreeTopConstraint, matchThreeBottomConstraint])
matchViews.append(matchOneDetailsView)
matchViews.append(matchTwoDetailsView)
matchViews.append(matchThreeDetailsView)
styleMatchViews()
}
func styleMatchViews() {
for view in matchViews {
view.opacity = 0.25
view.yOffset = 0.0
view.xOffset = 0.0
view.sRadius = 5.0
view.cRadius = 5.0
}
}
I'am trying to make a side menu and i have some problems with setting it with auto layout.
I have a rootViewController that i add to it the leftMenuVC as childVC then i set the constraints.
class RootVC: UIViewController, NavigationBarDelegate {
var leftMenuVC: UIViewController?
var navigationBar = NavigationBar()
var isMenuCollapsed = true
override func viewDidLoad() {
leftMenuVC = leftVC()
addChildViewController(leftMenuVC!)
view.addSubview(leftMenuVC!.view)
leftMenuVC!.didMove(toParentViewController: self)
}
override func viewDidLayoutSubviews() {
if let v = leftMenuVC?.view {
v.translatesAutoresizingMaskIntoConstraints = false
v.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
v.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
v.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -140).isActive = true
v.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
func menuButtonClicked(){
}
}
So my question is how to change constraints to hide/show the menu with support of orientations
What I usually do when I want to hide a view outside the screen with constraints is:
1 Set all constraints so that the sideview is visible (in active state)
2 Keep in reference the constraint that stick your sideview on one side (here the left one)
leftAnchor = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
view.addConstraint(leftAnchor)
view.addConstraint(NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 0.6, constant: 0))
3 Set one more constraint so that the view will be hidden. Usually it's something like that. Note that the priority is set to 999 to avoid constraint conflicts.
var hiddingConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
hiddingConstraint.priority = 999
view.addConstraint(hiddingConstraint)
4 Animate by activating or not your leftAnchor
UIView.animate(withDuration: 0.3) {
self.leftAnchor.active = false
self.view.layoutIfNeeded()
}
So you should end up with a code like this:
class RootVC: UIViewController, NavigationBarDelegate {
var leftMenuVC: UIViewController?
var navigationBar = NavigationBar()
var isMenuCollapsed = true {
didSet {
UIView.animate(withDuration: 0.3) {
self.leftAnchor?.isActive = self.isMenuCollapsed
self.view.layoutIfNeeded()
}
}
}
var leftAnchor : NSLayoutConstraint?
override func viewDidLoad() {
leftMenuVC = leftVC()
addChildViewController(leftMenuVC!)
view.addSubview(leftMenuVC!.view)
leftMenuVC!.didMove(toParentViewController: self)
}
override func viewDidLayoutSubviews() {
if let v = leftMenuVC?.view {
leftAnchor = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
view.addConstraint(leftAnchor!)
view.addConstraint(NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: v, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 0.6, constant: 0))
var hiddingConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0)
hiddingConstraint.priority = 999
view.addConstraint(hiddingConstraint)
}
}
func menuButtonClicked(){
isMenuCollapsed = !isMenuCollapsed
}
}
PS: I won't put the constraints setting in viewDidLayoutSubviews, maybe in viewWillAppear, as you don't have to set them every time the device is being rotated. That's the purpose of constraints
Instead of writing this code by yourself, save yourself the trouble.
Here is MMDrawerController to your rescue. I am using it myself. It's super easy to implement and offers lots of customization options. Hope you find it useful. :-)
I have a UITableView with custom sections programatically done. I am aiming to have a button in the navigation bar that will trigger a functions that will show or will hide a UIView with an appearing from left and disappearing from right Animation. So far:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
/** Header View **/
let headerView = UITableViewHeaderFooterView()
headerView.backgroundColor = UIColor.VinylRecorderTheme.white
headerView.addBottomBorder(color: UIColor.init(red: 250/255, green: 250/255, blue: 250/255, alpha: 1), height: 1, margins: 0)
headerView.tag = section
headerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerViewTouched)))
/* Header ImageView */
let headerImageView = UIImageView()
if let imageForHeader = vinylRecorderHelper.loadThimbnailFromMetadata(url: iCloudAlbumArray[section].urlTrackArray[0].url){
headerImageView.image = imageForHeader
}
else{
headerImageView.image = UIImage(named: "music_album_ic")
}
headerView.addSubview(headerImageView)
headerImageView.translatesAutoresizingMaskIntoConstraints = false
headerView.addConstraint(NSLayoutConstraint(item: headerImageView, attribute: .leading, relatedBy: .equal, toItem: headerView, attribute: .leading, multiplier: 1, constant: 8))
headerView.addConstraint(NSLayoutConstraint(item: headerImageView, attribute: .centerY, relatedBy: .equal, toItem: headerView, attribute: .centerY, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: headerImageView, attribute: .height, relatedBy: .equal, toItem: headerView, attribute: .height, multiplier: 0.65, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: headerImageView, attribute: .width, relatedBy: .equal, toItem: headerImageView, attribute: .height, multiplier: 1, constant: 0))
/** Header Button **/
let buttonWidth = currentDeviceTraitStatus == .wreghreg ? CGFloat(34):CGFloat(16)
let headerExpandButton = UIButton()
headerExpandButton.translatesAutoresizingMaskIntoConstraints = false
headerExpandButton.contentMode = .scaleAspectFit
headerExpandButton.setImage(#imageLiteral(resourceName: "arrow_rigth_ic"), for: .normal)
headerExpandButton.setImage(#imageLiteral(resourceName: "arrow_down_ic"), for: .selected)
headerExpandButton.tag = expandButtonTag
headerView.addSubview(headerExpandButton)
headerView.addConstraint(NSLayoutConstraint(item: headerExpandButton, attribute: .trailing, relatedBy: .equal, toItem: headerView, attribute: .trailing, multiplier: 1, constant: -8))
headerView.addConstraint(NSLayoutConstraint(item: headerExpandButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: buttonWidth))
headerView.addConstraint(NSLayoutConstraint(item: headerExpandButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: buttonWidth))
headerView.addConstraint(NSLayoutConstraint(item: headerExpandButton, attribute: .centerY, relatedBy: .equal, toItem: headerView, attribute: .centerY, multiplier: 1, constant: 0))
/** Header Label **/
let headerNameLabel = UILabel()
headerNameLabel.text = "\(iCloudAlbumArray[section].artistName) - \(iCloudAlbumArray[section].albumName)"
headerNameLabel.font = currentDeviceTraitStatus == .wreghreg ? .boldSystemFont(ofSize: 18):.boldSystemFont(ofSize: 15)
headerView.addSubview(headerNameLabel)
headerNameLabel.translatesAutoresizingMaskIntoConstraints = false
headerView.addConstraint(NSLayoutConstraint(item: headerNameLabel, attribute: .left, relatedBy: .equal, toItem: headerImageView, attribute: .right, multiplier: 1, constant: 8))
headerView.addConstraint(NSLayoutConstraint(item: headerNameLabel, attribute: .top, relatedBy: .equal, toItem: headerView, attribute: .top, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: headerNameLabel, attribute: .bottom, relatedBy: .equal, toItem: headerView, attribute: .bottom, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: headerExpandButton, attribute: .leading, relatedBy: .equal, toItem: headerNameLabel, attribute: .trailing, multiplier: 1, constant: 8))
/** Remove Button Label **/
let removeButton = UIButton()
removeButton.backgroundColor = UIColor.VinylRecorderTheme.redApple
removeButton.tag = deleteButtonTag
headerView.addSubview(removeButton)
removeButton.translatesAutoresizingMaskIntoConstraints = false
headerView.addConstraint(NSLayoutConstraint(item: removeButton, attribute: .leading, relatedBy: .equal, toItem: headerView, attribute: .leading, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: removeButton, attribute: .top, relatedBy: .equal, toItem: headerView, attribute: .top, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: removeButton, attribute: .bottom, relatedBy: .equal, toItem: headerView, attribute: .bottom, multiplier: 1, constant: 0))
removeButton.widthAnchor.constraint(equalToConstant: 0).isActive = true
removeButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(removeViewTouched)))
return headerView
}
And the function that will perform the desired Animation is:
#IBAction func startStopEditAction(_ sender: UIButton) {
print("\(logClassName): startStopEditAction.")
sender.isSelected = !sender.isSelected
if let sectionView = iCloudExportsTableView.headerView(forSection: 0){
print("\(logClassName): startStopEditAction in header")
let removeButton:UIButton = sectionView.viewWithTag(deleteButtonTag) as! UIButton
//removeButton.isHidden = !removeButton.isHidden
//removeButton.widthAnchor.constraint(equalToConstant: isEditingRemove ? 60 : 0)
UIView.transition(with: view, duration: 1.5, options: .transitionCrossDissolve, animations: {
removeButton.widthAnchor.constraint(equalToConstant: 0).isActive = false
removeButton.widthAnchor.constraint(equalToConstant: 60).isActive = true
//removeButton.isHidden = !removeButton.isHidden
})
}
}
EDIT
The UIView (Button)is
let removeButton = UIButton()
After some work around I have been able to achieve what I was looking for.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
/** Header View **/
let headerView = UITableViewHeaderFooterView()
headerView.backgroundColor = UIColor.VinylRecorderTheme.white
headerView.addBottomBorder(color: UIColor.init(red: 250/255, green: 250/255, blue: 250/255, alpha: 1), height: 1, margins: 0)
headerView.tag = section
headerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerViewTouched)))
**************
/** Remove Button **/
let removeButton = UIButton()
removeButton.backgroundColor = UIColor.VinylRecorderTheme.redApple
removeButton.tag = section
removeButton.setTitle(NSLocalizedString("com.messages.delete", comment: ""), for: .normal)
removeButton.titleLabel?.font = currentDeviceTraitStatus == .wreghreg ? .boldSystemFont(ofSize: 18):.boldSystemFont(ofSize: 15)
removeButton.setTitleColor(UIColor.white, for: .normal)
headerView.addSubview(removeButton)
removeButton.translatesAutoresizingMaskIntoConstraints = false
headerView.addConstraint(NSLayoutConstraint(item: removeButton, attribute: .leading, relatedBy: .equal, toItem: headerView, attribute: .leading, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: removeButton, attribute: .top, relatedBy: .equal, toItem: headerView, attribute: .top, multiplier: 1, constant: 0))
headerView.addConstraint(NSLayoutConstraint(item: removeButton, attribute: .bottom, relatedBy: .equal, toItem: headerView, attribute: .bottom, multiplier: 1, constant: 0))
let widthConstraint = NSLayoutConstraint(item: removeButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: self.isEditingRemove ? 90:0)
widthConstraint.identifier = tableViewSectionWidthId
headerView.addConstraint(widthConstraint)
removeButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(removeViewTouched)))
return headerView
}
And then In the IBAction:
#IBAction func startStopEditAction(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
let numberOfSections = iCloudExportsTableView.numberOfSections
for i in 0..<numberOfSections{
if let sectionView = iCloudExportsTableView.headerView(forSection: i){
let filteredConstraints = sectionView.constraints.filter { $0.identifier == tableViewSectionWidthId }
if let widthConstraint = filteredConstraints.first {
widthConstraint.constant = self.isEditingRemove ? 90:0
UIView.animate(withDuration: 0.5, delay: TimeInterval.init(Double(i) * Double(0.20)), options: UIViewAnimationOptions.curveEaseInOut, animations: {
sectionView.layoutIfNeeded()
}, completion: { (finished) in
})
}
}
}
}
I want to have a footer view on a static-cell UITableView that has three labels which are equally spaced, like so (from the simulator):
I can supply the footer view from my table view controller using this delegate call:
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
// construct view here
return view
}
And I can construct the view in two ways:
Create the labels and the spacers in code and add the appropriate constraints
Do all that in a XIB file then load the view from the file
My problem is that the first approach doesn't work and the second does.
This is my code for the first approach:
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if section == 0 {
// Create footer view
let view = UIView()
view.backgroundColor = UIColor.yellowColor()
view.clipsToBounds = false
view.layer.borderColor = UIColor.greenColor().CGColor
view.layer.borderWidth = 2
view.setTranslatesAutoresizingMaskIntoConstraints(false)
// Create labels
var labels: [UIView] = []
for name in ["Label 1", "AAAAAABBB", "Last label"] {
let v = UILabel()
v.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
v.textColor = UIColor.darkTextColor()
v.textAlignment = .Center
v.text = name
v.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(v)
labels += [v]
}
// Create spacers
var spacers: [UIView] = []
for i in 1...4 {
let v = UIView()
v.backgroundColor = UIColor.blueColor() // Background color is just so we can see where the view is and what size it has
v.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(v)
spacers += [v]
}
// Constrain all views to top and bottom of superview
for i in labels + spacers {
view.addConstraint(NSLayoutConstraint(item: i, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: i, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1, constant: 0))
}
// Equal width for labels
labels.pairs {
view.addConstraint(NSLayoutConstraint(item: $0, attribute: .Width, relatedBy: .Equal, toItem: $1, attribute: .Width, multiplier: 1, constant: 0))
}
// Equal width for spacers
spacers.pairs {
view.addConstraint(NSLayoutConstraint(item: $0, attribute: .Width, relatedBy: .Equal, toItem: $1, attribute: .Width, multiplier: 1, constant: 0))
}
view.addConstraint(NSLayoutConstraint(item: view, attribute: .Left, relatedBy: .Equal, toItem: spacers[0], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: spacers[0], attribute: .Right, relatedBy: .Equal, toItem: labels[0], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: labels[0], attribute: .Right, relatedBy: .Equal, toItem: spacers[1], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: spacers[1], attribute: .Right, relatedBy: .Equal, toItem: labels[1], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: labels[1], attribute: .Right, relatedBy: .Equal, toItem: spacers[2], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: spacers[2], attribute: .Right, relatedBy: .Equal, toItem: labels[2], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: labels[2], attribute: .Right, relatedBy: .Equal, toItem: spacers[3], attribute: .Left, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: spacers[3], attribute: .Right, relatedBy: .Equal, toItem: view, attribute: .Right, multiplier: 1, constant: 0))
return view
}
else {
return nil
}
}
extension Array {
func pairs(block: (Element, Element?)->()) {
if count == 0 { return }
if count == 1 { block(self.first!, nil) }
var last = self[0]
for i in self[1..<count] {
block(last, i)
last = i
}
}
}
This is the result:
Not at all what I was expecting, right?
Now, on to the second method. Instead of posting a bunch of screenshots from Interface Builder, I have created a sample project available here specifically to test this problem. If you open it, the file FooterView.xib contains the footer view constructed in IB that, as far as I know, has exactly the same view structure and auto-layout constraints.
Using that view, like this:
return (NSBundle.mainBundle().loadNibNamed("FooterView", owner: self, options: nil).first as UIView)
yields the result you saw in the first screenshot, which is exactly what I want.
So, with Interface Builder the constraints work as expected. Why doesn't it work when the views & constraints are created in code? What am I missing?
The view's size is unknown at creation time, so setting its setTranslatesAutoresizingMaskIntoConstraintsto true does the trick:
view.setTranslatesAutoresizingMaskIntoConstraints(true)
Result: