UITableView top to topLayoutGuide programmatically content inset - uitableview

Due to my attempt to implement iAds "properly" (i.e., sharing a single instance of ADBannerView), I am creating a UITableView programatically inside a UIViewController and adding it to the view. Below are a couple of snippets from my subclass of UIViewController:
From viewDidLoad
self.tableView = UITableView(frame: self.view.bounds, style: .Grouped)
self.tableView.allowsSelectionDuringEditing = true
self.tableView.registerNib(UINib(nibName: "TableViewCellWithSwitch", bundle: nil), forCellReuseIdentifier: "SliderCellIdentifier")
self.tableView.dataSource = self
self.tableView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(self.tableView)
self.tableViewBottomLayoutConstraint = NSLayoutConstraint(item: self.tableView, attribute: .Bottom, relatedBy: .Equal, toItem: self.bottomLayoutGuide, attribute: .Top, multiplier: 1, constant: 0)
self.view.addConstraints([
NSLayoutConstraint(item: self.tableView, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1, constant: 0),
//NSLayoutConstraint(item: self.tableView, attribute: .Top, relatedBy: .Equal, toItem: self.view, attribute: .Top, multiplier: 1, constant: 0),
//NSLayoutConstraint(item: self.tableView, attribute: .Top, relatedBy: .Equal, toItem: self.view, attribute: .Top, multiplier: 1, constant: self.topLayoutGuide.length),
//NSLayoutConstraint(item: self.tableView, attribute: .Top, relatedBy: .Equal, toItem: self.topLayoutGuide, attribute: .Bottom, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.tableView, attribute: .Right, relatedBy: .Equal, toItem: self.view, attribute: .Right, multiplier: 1, constant: 0),
self.tableViewBottomLayoutConstraint
])
// This must be called or the use of self.topLayoutGuide will not function
// See: https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewController_Class/index.html#//apple_ref/occ/instp/UIViewController/topLayoutGuide
self.view.layoutSubviews()
iAds implementation (added to try and justify, rightly or wrongly, my implementation of the UITableView)
func showiAds(animated: Bool) {
println("Show iAd")
if !self.showingiAd {
println("Showing iAd")
self.showingiAd = true
// Add the banner view below the content before it's then animated in to view
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
let bannerView = delegate.bannerView
self.bannerBottomConstraint = NSLayoutConstraint(item: bannerView, attribute: .Bottom, relatedBy: .Equal, toItem: self.bottomLayoutGuide, attribute: .Top, multiplier: 1, constant: bannerView.frame.size.height)
if (bannerView.superview != self.view) {
bannerView.removeFromSuperview()
}
self.view.addSubview(bannerView)
self.view.addConstraints([
self.bannerBottomConstraint,
NSLayoutConstraint(item: bannerView, attribute: .Left, relatedBy: .Equal, toItem: self.view, attribute: .Left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: bannerView, attribute: .Right, relatedBy: .Equal, toItem: self.view, attribute: .Right, multiplier: 1, constant: 0),
])
self.view.layoutIfNeeded()
// Only the changing of the value of the top of the banner is animated so it "slides in" from the bottom
self.bannerBottomConstraint.constant = 0
self.view.setNeedsUpdateConstraints()
UIView.animateWithDuration(animated ? 0.5 : 0, animations: { () -> Void in
// Calling layoutIfNeeded here will animate the layout constraint cosntant change made above
self.view.layoutIfNeeded()
}, completion: { (completed) -> Void in
if completed {
println("Completed animation")
}
})
}
}
func hideiAds() {
println("Hide iAd")
if self.self.showingiAd {
self.showingiAd = false
println("Hiding iAd")
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
let bannerView = delegate.bannerView
if bannerView.superview == self.view {
bannerView.removeFromSuperview()
}
self.view.removeConstraint(self.tableViewBottomLayoutConstraint)
self.tableViewBottomLayoutConstraint = NSLayoutConstraint(item: self.tableView, attribute: .Bottom, relatedBy: .Equal, toItem: self.bottomLayoutGuide, attribute: .Top, multiplier: 1, constant: 0)
self.view.addConstraint(self.tableViewBottomLayoutConstraint)
}
}
As you can see, there are 3 constraints commented out. Each of these seem to have different results. I won't post screenshots of them (unless requested), but I will describe them.
NSLayoutConstraint(item: self.tableView, attribute: .Top, relatedBy: .Equal, toItem: self.view, attribute: .Top, multiplier: 1, constant: 0)
iOS 7: Top of table and content of table is at the top of screen. Content is behind the navigation bar
iOS 8: Top of table and content of table is below the navigation bar. Content is below the navigation bar (correct)
NSLayoutConstraint(item: self.tableView, attribute: .Top, relatedBy: .Equal, toItem: self.view, attribute: .Top, multiplier: 1, constant: self.topLayoutGuide.length)
iOS 7: Top of table and content of table is at the top of screen. Content is behind the navigation bar
iOS 8: Top of table and content of table is below the navigation bar. Content is below the navigation bar (correct)
NSLayoutConstraint(item: self.tableView, attribute: .Top, relatedBy: .Equal, toItem: self.topLayoutGuide, attribute: .Bottom, multiplier: 1, constant: 0)
iOS 7: Top of table and content of table is below the navigation bar. Content is below the navigation bar (correct)
iOS 8: Top of table is at the bottom of the navigation bar (correct), but content of table is below the navigation bar, plus (what looks like) the height of the offset again (incorrect)
I am aware that I could just do an if iOS7 {...} else {...}, but that feels pretty dirty and I've got a feeling it's my lack of understanding that's causing this issue, so I'd like to figure out how to have this work on iOS 7 and 8 without having resort to version checking, if possible.

I ended up finding a way of doing this. I'm not sure just how hacky it is, but it's worked on iOS 7.0-8.2 so far.
override func viewDidLayoutSubviews() {
if self.tableView != nil {
// Setting both of these to 0 seems to fix some auto layout issues that crop up in iOS 7/8 depending on
// which item the layout is to, e.g., in iOS 8, having the UITableView's top be to the topLayoutGuide's bottom will
// cause a gap at the top of the UITableView, but this removes that gap and doesn't seem to affect iOS 7
self.tableView.contentInset = UIEdgeInsetsZero
self.tableView.scrollIndicatorInsets = UIEdgeInsetsZero
if self.showingiAd {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
let bannerViewHeight = bannerView.frame.size.height
self.tableViewBottomLayoutConstraint.constant = -bannerViewHeight
}
}
}
}

Related

Programmatically created UIViewController partially hidden by navigation bar

I have a UIViewController subclass that I create programmatically, without Interface Builder. The class is called ColorController, since it edits a color. When I add it to a popup inside a UINavigationController, its content is hidden under the navigation bar. This did not used to happen when the ColorController was pulled from the IB storyboard file.
Is there some property or method I must override on my ColorController to tell it to adjust its bounds when in a navigation controller?
Right now all I'm doing is creating my root UIView (a ColorPicker) and setting it as self.view in loadView().
class ColorController: UIViewController {
private let colorPicker: ColorPicker
init() {
colorPicker = ColorPicker()
}
override func loadView() {
self.view = colorPicker
}
Don't override loadView, override viewDidLoad and add colorPicker self.view
Use SnapKit
colorPicker.snp.makeConstraints { (make) -> Void in
make.leading.trailing.bottom.equalTo(0)
make.top.equalTo(self.topLayoutGuide.snp.bottom);
}
Use iOS UIKit
colorPicker.translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutConstraint(item: colorPicker, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: colorPicker, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: 0),
NSLayoutConstraint(item: colorPicker, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1, constant: 0)]
if #available(iOS 11.0, *) {
constraints.append(NSLayoutConstraint(item: colorPicker, attribute: .top, relatedBy: .equal, toItem: self.view.safeAreaLayoutGuide, attribute: .top, multiplier: 1, constant: 0))
} else {
constraints.append(NSLayoutConstraint(item: colorPicker, attribute: .top, relatedBy: .equal, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0))
}
self.view.addConstraints(constraints)

SideMenu with AutoLayout

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. :-)

Label disappear as auto layout is applied programmatically - iOS Swift

Explanation:
In my ViewController i want to create and adjust a view programmatically depending on a radio button. There is one problem as i create the view and setup the constraint programmatically, one of the labels just disappears and if i continued with that most of the component within the ViewController either disappear and move.
Pictures:
Code:
func serviceHoursRadioButtonAction(button: DLRadioButton) {
if button.currentTitle == "yes" {
serviceHoursView = UIView()
if let myView = serviceHoursView {
myView.backgroundColor = UIColor.lightGrayColor()
view.addSubview(myView)
NSLayoutConstraint(item: myView,
attribute: .Top,
relatedBy: .Equal,
toItem: serviceHoursLabel,
attribute: .Bottom,
multiplier: 1, constant: 10).active = true
NSLayoutConstraint(item: myView,
attribute: .LeftMargin,
relatedBy: .Equal,
toItem: self.view,
attribute: .LeftMargin,
multiplier: 1, constant: 0).active = true
NSLayoutConstraint(item: myView,
attribute: .RightMargin,
relatedBy: .Equal,
toItem: self.view,
attribute: .RightMargin,
multiplier: 1, constant: 0).active = true
NSLayoutConstraint(item: myView,
attribute: .BottomMargin,
relatedBy: .Equal,
toItem: self.view,
attribute: .BottomMargin,
multiplier: 1, constant: 0).active = true
}
} else {
//do nothing, or remove the created view if any.
}
}
Set views property translateAutoresizingMaskIntoConstraints to false.
Setting to self.view.layoutSubviews() fixed mine

Use autolayout on view added to UIWindow

I'm trying to add a subview to the keyWindow of the app, and position it using autolayout. However, autolayout doesn't seem to work at all, whereas setting a frame does. I want to align my view infoSc to the bottom of the keyWindow using the following code:
let infoSc = InfoScreenView()
infoSc.translatesAutoresizingMaskIntoConstraints = false
let keyWindow = UIApplication.sharedApplication().keyWindow!
keyWindow.addSubview(infoSc)
keyWindow.addConstraint(NSLayoutConstraint(item: infoSc, attribute: .Left, relatedBy: .Equal, toItem: keyWindow, attribute: .Left, multiplier: 1, constant: 0))
keyWindow.addConstraint(NSLayoutConstraint(item: infoSc, attribute: .Right, relatedBy: .Equal, toItem: keyWindow, attribute: .Right, multiplier: 1, constant: 0))
keyWindow.addConstraint(NSLayoutConstraint(item: infoSc, attribute: .Bottom, relatedBy: .Equal, toItem: keyWindow, attribute: .Bottom, multiplier: 1, constant: 0))
infoSc.addConstraint(NSLayoutConstraint(item: infoSc, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 100))
However, it appears to have a frame of CGRectZero using this method. Any ideas how to make this work? Ideally I'd also want to align it to something that's inside self.view too, but that throws an error that self.view is not in the view hierarchy of keyWindow.
If you need to draw on the entire window, here is code to do it (in this example I am in the AppDelegate, so window is the AppDelegate.window property).
func tryToDrawOnTheWindow()
{
if let window = window, view = window.rootViewController?.view
{
print("I have a root view")
let infoSc = InfoScreenView(frame: view.frame)
let count = view.subviews.count
view.insertSubview(infoSc, atIndex: count)
infoSc.translatesAutoresizingMaskIntoConstraints = false
let height = NSLayoutConstraint(item: infoSc, attribute: .Height, relatedBy: .Equal, toItem: view, attribute: .Height, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: infoSc, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0)
let offset = NSLayoutConstraint(item: infoSc, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0)
print([height, width, offset])
view.addConstraints([height, width, offset])
}
else
{
print("No root view on which to draw")
}
}
This will let you draw on top of whatever is in the view hierarchy. In my test app, I added a textfield and a blue rect, and the overlay was orange with 40% opacity. Bear in mind that by default the overlay view will consume all taps in this case.
Place and call a method below, based on David S's answer, where you want, passing your view as variable.
func addToWindow(view : UIView) {
guard let delegate = UIApplication.shared.delegate, let window = delegate.window!, let topView = window.rootViewController?.view else {
print("No root view on which to draw")
return
}
print("I have a root view")
let count = topView.subviews.count
topView.insertSubview(view, at: count)
view.translatesAutoresizingMaskIntoConstraints = false
let height = NSLayoutConstraint(item: view, attribute: .height, relatedBy: .equal, toItem: topView, attribute: .height, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: view, attribute: .width, relatedBy: .equal, toItem: topView, attribute: .width, multiplier: 1, constant: 0)
let pinToTop = NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .top, multiplier: 1, constant: 0)
print([height, width, pinToTop])
topView.addConstraints([height, width, pinToTop])
}

Weird issue when setting up auto-layout constraints in code

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:

Resources