I have a question on Auto layout. I'm using xib files and I have a view controller like this.
Its embedded inside a UINavigationController so I have the button positioned with a Top Space to Superview constraint.
The problem is when I rotate the device, it looks like this.
As you can see that constraint still keeps its original value so there's a big gap between the navigation bar edge and the button in landscape mode.
How can I make the button position close to the navigation bar like when its in the portrait and have it that way in both portrait and landscape modes? I'm using Xcode 6 by the way.
Thank you.
If you want to define your auto layout top margin constraints in your Xib file, you can add the following code in the ViewController's class file relative to your Xib:
override func viewDidLoad() {
super.viewDidLoad()
if self.respondsToSelector("edgesForExtendedLayout") {
edgesForExtendedLayout = UIRectEdge.None
}
}
Simple. But the problem there is that you won't be able to have a translucent navigation bar.
Fortunately, there are several alternatives to this. You can define your auto layout top margin as relative to your Top Layout Guide (not to your view) with Storyboard or with code.
If you move to Storyboard, click on the Pin button, select your top margin constraint and choose The Top Layout Guide (see image below).
If you decide to define all your UIButton's constraints with code, you can use Visual Format Language as indicated in the following code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var button = UIButton()
button.backgroundColor = UIColor.blueColor()
button.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(button)
var viewsDict = ["button" : button, "topLayoutGuide" : topLayoutGuide]
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide]-20-[button]", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[button]-20-|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
}
}
Finally, there is a fourth (kind of mixed) way to perform what you want to do. Set your constraints in your Xib, drag your top margin constraint and your UIButton to your view controller class (name them topConstraint and button) and set your code as the following:
import UIKit
class ViewControllerTwo: UIViewController {
#IBOutlet weak var topConstraint: NSLayoutConstraint!
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
view.addConstraint(NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.topLayoutGuide, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 10))
view.removeConstraint(topConstraint)
}
}
Related
I have ViewController that looks like this. There is parent ScrollView that holds UIView (wrapperView) and this wrapper view has TextView and ImageView inside.
This simple structure is created form interface builder with all constraints needed so it works fine.
In some cases I need to programmatically add one more UIView inside my wrapperView right under ImageView (green arrow points there).
So I created xib file for my new UIView and separate class for it
class MyView : UIView {
class func instanceFromNib() -> UIView {
return UINib(nibName: "MyView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UIView
}
}
And I code of my ViewController I added
let myView = MyView.instanceFromNib()
myView = CGRect(x: 0, y: 0, width: 300, height: 80)
wrapperView.addSubview(myView)
now i see myView on the screen in top of wrapperView but when i try to apply constraints like
NSLayoutConstraint(item: myView, attribute: .top, relatedBy: .equal, toItem: myImageView, attribute: .bottom, multiplier: 1.0, constant: 8).isActive = true
I got
Unable to simultaneously satisfy constraints
What am I doing wrong?
First you need to set this:
myView.translatesAutoresizingMaskIntoConstraints = false
And then you have to make sure that there are no your own constraints that are in conflict. In storyboard you have probably added a constraint that binds imageView.bottomAnchor to wrapperView.bottomAnchor. Now if you put between these two anchors another view, you need to deactivate that constraint.
Otherwise if you try to squeeze myView there, it may result in constraint conflicts - imagine there is a constraint saying that
imageView.bottomAnchor should be 10 points from wrapperView.bottomAnchor,
but there are also two more constraints saying that
imageView.bottomAnchor should be 10 points from myView.topAnchor and myView.bottomAnchor should be 10 points from wrapperView.bottomAnchor.
Obviously both these scenarios cannot be satisfied.
I think, that the easiest solution would be add an UIStackView to the wrapperView in the interface builder with no height, just add needed constraints (top, left, bottom, right). Then drag an IBOutlet of the UIStackView to your ViewController and when you will need to add a subview:
#IBOutlet weak var stackView: UIStackView!
func someFunc() {
let myView = MyView.instanceFromNib()
myView = CGRect(x: 0, y: 0, width: 300, height: 80)
stackView.addArrangedSubview(myView)
}
UIStackView will change its size based on its subviews.
You may use the Debug View Hierarchy to debug your UI issues including the constraints which are failing to be satisfied. Most probably your .top constraint which is added at runtime is conflicting with .top constraint which you have set in your storyboard.
Similar to what the Spotify or Apple Music app does when a song is playing, it places a custom view on top of the UITabBar:
Solutions I've tried:
UITabBarController in a ViewController with a max-sized Container View, and the custom view on top of the Container View49pt above the Bottom Layout Guide:
Problem: Any content in ViewControllers embedded in the UITabBarController constrained to the bottom don't show because they're hidden behind the custom layout. I've tried overriding size forChildContentContainer in UITabBarController, tried updating the bottom layout guide, Nothing. I need to resize the frame of container view of the UITabBarController.
Tried #1 again, but tried solving the problem of content hiding behind it by increasing the size of UITabBar, and then using ImageInset on every TabBarItem to bring it down, and adding my custom view on top of the UITabBar. Hasn't worked really well. There are going to be times when I want to hide my custom view.
UITabBarController as root, with each children being a ViewController with a Container View + my custom view:
But now I have multiple instances of my custom view floating around. If I want to change a label on it, have to change it to all views. Or hide, etc.
Override the UITabBar property of UITabBarController and return my custom UITabBar (inflated it with a xib) that has a UITabBar + my custom view. Problem: Probably the most frustrating attempt of all. If you override that property with an instance of class MyCustomTabBar : UITabBar {}, no tab shows up! And yes, I set the delegate of myCustomTabBar to self.
Leaning towards #3, but looking for a better solution.
This is actually very easy if you subclass UITabBarController and add your view programmatically. Using this technique automatically supports rotation and size changes of the tab bar, regardless of which version you are on.
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
//...do some of your custom setup work
// add a container view above the tabBar
let containerView = UIView()
containerView.backgroundColor = .red
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// anchor your view right above the tabBar
containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
I got it!
In essence, I increased the size of the original UITabBar to accomodate a custom view (and to shrink the frame of the viewcontrollers above), and then adds a duplicate UITabBar + custom view right on top of it.
Here's the meat of what I had to do. I uploaded a functioning example of it and can be found in this repo:
class TabBarViewController: UITabBarController {
var currentlyPlaying: CurrentlyPlayingView!
static let maxHeight = 100
static let minHeight = 49
static var tabbarHeight = maxHeight
override func viewDidLoad() {
super.viewDidLoad()
currentlyPlaying = CurrentlyPlayingView(copyFrom: tabBar)
currentlyPlaying.tabBar.delegate = self
view.addSubview(currentlyPlaying)
tabBar.isHidden = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
currentlyPlaying.tabBar.items = tabBar.items
currentlyPlaying.tabBar.selectedItem = tabBar.selectedItem
}
func hideCurrentlyPlaying() {
TabBarViewController.tabbarHeight = TabBarViewController.minHeight
UIView.animate(withDuration: 0.5, animations: {
self.currentlyPlaying.hideCustomView()
self.updateSelectedViewControllerLayout()
})
}
func updateSelectedViewControllerLayout() {
tabBar.sizeToFit()
tabBar.sizeToFit()
currentlyPlaying.sizeToFit()
view.setNeedsLayout()
view.layoutIfNeeded()
viewControllers?[self.selectedIndex].view.setNeedsLayout()
viewControllers?[self.selectedIndex].view.layoutIfNeeded()
}
}
extension UITabBar {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = CGFloat(TabBarViewController.tabbarHeight)
return sizeThatFits
}
}
Since iOS 11 this became a little easier. When you add your view, you can do the following:
viewControllers?.forEach {
$0.additionalSafeAreaInsets = UIEdgeInsets(
top: 0,
left: 0,
bottom: yourView.height,
right: 0
)
}
Your idea to put it in a wrapper viewcontroller is good, but it will only cause overhead (more viewcontrollers to load in memory), and issues when you want to change the code later on. If you want the bar to always show on your UITabBarController, then you should add it there.
You should subclass UITabBarController and load the custom bar from a nib. There you will have access to the tabbar (so you can place your bar correctly above it), and you will only load it in once (which solves your problem that you will face having a different bar on each tab).
As for your views not reacting to the size of the custom bar, I don't know how you can do that, but my best suggestion is to use a public variable and notifications that you listen to in your individual tabs.
You can then use that to change the bottom constraint.
Besides playing with UITabBar or container vc, you could also consider adding the view in the App Delegate to the main window like in following post:
View floating above all ViewControllers
Since your view is all around along with the Tab bar, it is totally ok to make it in the App Delegate.
You can always access the Floating view from App Delegate Singleton by making it a property of the App Delegate. It is easy then to control its visibility in anywhere of your code.
Changing constant of the Constraints between the Floating view and super view window can adjust the position of the view, thus handsomely respond to orientation changes.
Another(similar) approach is to make the floating view another window like the uid button.
Unless I've misunderstood, you could create a custom view from your UITabBarController class. You can then insert it above and constrain it to the tabBar object, which is the tabBar associated with the controller.
So from your UITabBarController class, create your custom view
class CustomTabBarController: UITabBarController {
var customView: UIView = {
let bar = UIView()
bar.backgroundColor = .white
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
In viewDidLoad() add your custom view to the UITabBarController's view object and place it above the tabBar object
override func viewDidLoad() {
super.viewDidLoad()
...
self.view.insertSubview(customView, aboveSubview: tabBar)
Then after your custom view is added as a subView, add constraints so it's positioned correctly. This should also be done in viewDidLoad() but only after your view is inserted.
self.view.addConstraints([
NSLayoutConstraint(item: customView, attribute: .leading, relatedBy: .equal, toItem: tabBar, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: customView, attribute: .trailing, relatedBy: .equal, toItem: tabBar, attribute: .trailing, multiplier: 1, constant: 0),
NSLayoutConstraint(item: customView, attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: -50),
NSLayoutConstraint(item: customView, attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0)
])
There's a bunch of creative ways you can setup constraints to do what you want, but the constraints above should attach a view above your tabBar with a height of 50.
Make the view's frame with the height of tab bar and brings it to top, 2. set tabBar hidden is true.
I've been trying to add constraints programmatically to a view that I'm also adding programmatically to my view controller. However, it seems like the constraints are not being followed.
The view has been added to the story board for the view controller, but isn't actually added to the view controller's view until later on (See screenshot below).
I've tried adding a variety of constraints but none of them have worked so far. I've simplified it now to the single constraint below and even this will not work. What am I doing wrong?
#IBOutlet var loadingView: LoadingView!
override func viewDidLoad() {
super.viewDidLoad()
displayLoadingView(true)
}
func displayLoadingView(display: Bool) {
if display {
view.addSubview(loadingView)
let widthConstraint = NSLayoutConstraint(item: loadingView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 50.0)
view.addConstraint(widthConstraint)
}
}
set translatesAutoresizingMaskIntoConstraints = false to any view you are settings constraints programatically.
from the apple doc: translatesAutoresizingMaskIntoConstraints
If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.
You don't set all necessary constraints, that can be the reason. Consider following rough example. MyView interface is defined in standalone xib file. Hope it helps:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let myView = loadFromNib("MyView") else {
return
}
view.addSubview(myView)
myView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-15-[myView]-15-|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["myView": myView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-15-[myView]-15-|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: ["myView": myView]))
}
func loadFromNib(cls: String) -> UIView? {
return NSBundle.mainBundle().loadNibNamed(cls, owner: nil, options: nil)[0] as? UIView
}
}
I have two subviews that I created, one in Storyboard one with code, I want to anchor the second view (created with code) to the first view (in storyboard) with some constraints so that the second view sits below the first view:
class ViewController: UIViewController{
#IBOutlet weak var view1: UIView!
override func viewDidLoad(){
super.viewDidLoad()
setUp()
}
func setUp(){
var view2 = UIView()
view2.setTranslatesAutoresizingMaskIntoConstraints(false)
view2.frame = CGRectMake(10,10,10,10)
self.view.addSubview(view2)
self.view.addConstraint(NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.TopMargin, relatedBy: NSLayoutRelation.Equal, toItem: view2, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 10))
}
}
The problem is that I got an error saying When added to a view, the constraint's items must be descendants of that view. Is it bad practice to have some views in storyboard and others in code?
You have a few problems.
First, the error means that view1 and view2 are not both part of self.view's view hierarchy. This is probably because view1 hasn't been decoded from the storyboard yet. Try moving this code from viewDidLoad() to awakeFromNib(), which is when they're guaranteed to have been loaded.
Second, you're trying to set the view's frame:
view2.frame = CGRectMake(10,10,10,10)
This frame will get overwritten by the layout engine making it pointless. Delete this line.
If you're going to use auto-layout, you need to unambiguously specify both the size (height & width) and position (x & y) of the view. The constraint you added only specifies the y-origin, so you also need to add more constraints (probably 3 more) to specify the x-origin, the width, and the height, which are currently undefined.
Is it bad practice to have some views in storyboard and others in code?
No, it's common.
I have to manage my UIButton position at the right side of a UITableViewCell like an image below.
in this cell I gave all my constraints from storyboard except of button..because I created button at runtime like below
In tableview:cellForRowAtIndexPath method
let mybutton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
var width:CGFloat = UIScreen.mainScreen().bounds.width
mybutton.frame = CGRectMake(width - 108, 8, 100, 31)
mybutton.tag = indexPath.row
cell?.contentView.addSubview(mybutton)
So the problem is when we launch the app in portrait its ok but when we rotate it, button displays at portrait position...for e.g. if the button position at portrait 220 then in landscape it displays at 220 and after scrolls it looks ok because of cell reusability...
To solve this I'll trying to manually add few constraints to button.I don't know much about how to add constraints programatically but i'll add constraints like below one for top position..and similar with trailing
cell?.contentView.addConstraint(
NSLayoutConstraint(item:imageview ,
attribute: NSLayoutAttribute.Top,
relatedBy: NSLayoutRelation.Equal,
toItem:mybutton ,
attribute:NSLayoutAttribute.Top,
multiplier: 1, constant: 8))
but it displays breaking constraints...
So my questions are...
How to deal with storyboard + manually constraints
How to position button at the right side of a cell
I normally use the Auto-layout Visual Format
//Horizontal constraints
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[button]", options: nil, metrics: metrics, views: views)
self.view.addConstraints(horizontalConstraints)
//Vertical constraints
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-20-[button]", options: nil, metrics: metrics, views: views)
self.view.addConstraints(verticalConstraints)
Basically you set V or H for vertical and horizontal fallow by the UI item "|" for the board of the screen, than the -value in pixels- and finally the next [UIComponent]
You can find a nice tutorial here
I have not enough reputation for comment that's why i am answering the question
You can reload Data after orientation changed.
It will solve the problem
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
tableVIew.reloadData()
}