I am using Swift 3, iOS 10, XCode 8.2.
In my code, I need to create a UIViewController programmatically and hence, specify its layout and content programmatically as well.
#IBAction func testViewController() {
let detailViewController = UIViewController()
detailViewController.view.backgroundColor = UIColor.white
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.blue
label.text = "Scan Results"
label.textAlignment = .center
label.font = UIFont.boldSystemFont(ofSize: 18)
label.textColor = UIColor.white
return label
}()
let titleConstraints: [NSLayoutConstraint] = [
NSLayoutConstraint(item: titleLabel, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: titleLabel, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: 0),
NSLayoutConstraint(item: titleLabel, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: titleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 40)
]
detailViewController.view.addSubview(titleLabel)
detailViewController.view.addConstraints(titleConstraints)
self.navigationController?.pushViewController(detailViewController, animated: true)
}
In the vertical view (ignore all the other junk; just focus on the blue title bar):
But in the horizontal view:
What is the correct constraint to set so that it takes up the entire width of the bar and there isn't that extra space from the top since the status bar disappears when horizontal?
EDIT
After making #thexande suggestions, I do get an error:
[LayoutConstraints] The view hierarchy is not prepared for the
constraint: <NSLayoutConstraint:0x608000098100
UILabel:0x7fe35b60edc0'Scan Results'.left ==
UIView:0x7fe35b405c20.left (inactive)> When added to a view, the
constraint's items must be descendants of that view (or the view
itself). This will crash if the constraint needs to be resolved before
the view hierarchy is assembled. Break on
-[UIView(UIConstraintBasedLayout) _viewHierarchyUnpreparedForConstraint:] to debug. 2017-02-24 21:01:59.807 EOB-Reader[78109:10751346] * Assertion failure in
-[UIView _layoutEngine_didAddLayoutConstraint:roundingAdjustment:mutuallyExclusiveConstraints:],
/BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.6.21/NSLayoutConstraint_UIKitAdditions.m:649
2017-02-24 21:01:59.951 EOB-Reader[78109:10751346] * Terminating app
due to uncaught exception 'NSInternalInconsistencyException', reason:
'Impossible to set up layout with view hierarchy unprepared for
constraint.'
I've also updated my code in the original post.
The reason this is happening is because you are using frames. You calculated the frame based on the width of the screen. You do not need frames, you can do this all using auto layout. Instead, you should use constraints to pin your label to it's super view bounds, and give it a static height. for example:
lazy var titleConstraints: [NSLayoutConstraint] = [
NSLayoutConstraint(item: self.titleLabel, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.titleLabel, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.titleLabel, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.titleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 40)
]
then, in viewDidLoad()
self.view.addConstraints(titleConstraints)
You could simplify your label declaration like so. dont forget the auto resizing mask flag to get constraints to work correctly:
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.blue
label.text = "Scan Results"
label.textAlignment = .center
label.textColor = UIColor.white
return label
}()
Finally, you are doing strange math to get the top of your view controller to abut the bottom of your nav bar controller. Remove all that garbage and put the following in viewDidLoad() to get the top of your view controller right against the bottom of your UINavigationBar:
self.edgesForExtendedLayout = []
UPDATES:
The problem here is you are appending views and constraints into a View Controller which has not allocated yet.
The reason we append sub views and constraints within viewDidLoad() is because we cannot add subviews and constraints before the view....did....load into memory. Otherwise, it's not there, and you get the error above. Consider breaking out your detailViewController into a class declaration, like so:
class detailViewController: UIViewController {
let eobTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.blue
label.text = "Scan Results"
label.textAlignment = .center
label.font = UIFont.boldSystemFont(ofSize: 18)
label.textColor = UIColor.white
return label
}()
lazy var eobTitleConstraints: [NSLayoutConstraint] = [
NSLayoutConstraint(item: self.eobTitleLabel, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.eobTitleLabel, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.eobTitleLabel, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.eobTitleLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 40)
]
override func viewDidLoad() {
self.view.addSubview(eobTitleLabel)
self.view.addConstraints(self.eobTitleConstraints)
}
}
Also, not to come off as offensive, but your code is kind of a mess. Things you should avoid in the future:
adding constraints to a label which does not exist. ( rename the label of fix the constraints)
you are declaring vars in a outlet method. dont do this, declare methods and properties at the class level.
Read about OOP and how it is implemented in swift. This will help you understand the methods and patterns to complete your task :)
Related
I have a viewcontroller that holds multiple stackviews. There is a button that when pressed, the corresponding uiview wil become fullscreen inside the original view. There is a different button that is supposed to make the uiview go back to its original stackview. The uiview itself contains other views. I am having some problems doing that. The uiview does end up in the stackview, but not near the same size/place it used to be. I am not sure how to solve this, and been going at it for several hours now, looking at multiple sources.
This is the code that makes the uiview go fullscreen:
private func moveToFrontOfCardView(v: UIView) {
originalView = v.superview
if let stack = originalView as? UIStackView {
stack.removeArrangedSubview(v)
}
myCardView.addSubview(v)
let topConstraint = NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: myCardView, attribute: .top, multiplier: 1, constant: 10)
let bottomConstraint = NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: myCardView, attribute: .bottom, multiplier: 1, constant: -10)
let leftConstraint = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: myCardView, attribute: .left, multiplier: 1, constant: 10)
let rightConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: myCardView, attribute: .right, multiplier: 1, constant: -10)
myCardView.addConstraints([topConstraint, bottomConstraint, leftConstraint, rightConstraint])
}
And this is the code I use when I want it to go back:
private func moveToOriginalPosition(v: UIView) {
if let stack = originalView as? UIStackView {
stack.addArrangedSubview(v)
}
}
Does anyone have a clue how I could fix this?
EDIT
I've tried Saqib and Bilals answer, but I get this as a result:
Declare a class variable for tracking view's index
var selectedIndex = 0 // Contains Current Seleceted view's index
overrie func viewDidLoad() { ...
Before removing view from stackview get the view index like this selectedIndex = stack.subviews.index(of: v)
keep reference to all the constraints.
Before adding it back disable all the constraints topConstraint.isActive = false
Now add the view at the same index using stack.insertArrangedSubview(view, at: selectedIndex)
An other option is to create a same new view and just hide/unhide the one in stackview. StackView automatically fills the space accordingly for the hidden views.
You should deActivate the constraints you added to view when removed it from stackView, at the time you want add the view to the stackView again.
For this you should make the constraints instance of your viewController class and next, write your moveToOriginalPosition(v: UIView) method like this:
private func moveToOriginalPosition(v: UIView) {
if let stack = originalView as? UIStackView {
stack.addArrangedSubview(v)
topConstraint.isActive = false
bottomConstraint.isActive = false
leftConstraint.isActive = false
rightConstraint.isActive = false
}
}
Ofcourse, you should remove, these lines of codes from moveToFrontOfCardView(v: UIView) method:
self.topConstraint = NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: myCardView, attribute: .top, multiplier: 1, constant: 10)
self.bottomConstraint = NSLayoutConstraint(item: v, attribute: .bottom, relatedBy: .equal, toItem: myCardView, attribute: .bottom, multiplier: 1, constant: -10)
self.leftConstraint = NSLayoutConstraint(item: v, attribute: .left, relatedBy: .equal, toItem: myCardView, attribute: .left, multiplier: 1, constant: 10)
self.rightConstraint = NSLayoutConstraint(item: v, attribute: .right, relatedBy: .equal, toItem: myCardView, attribute: .right, multiplier: 1, constant: -10)
myCardView.addConstraints([topConstraint, bottomConstraint, leftConstraint, rightConstraint])
and add them where you make your view initialized. and replace below lines with above lines in moveToFrontOfCardView(v: UIView) method:
topConstraint.isActive = true
bottomConstraint.isActive = true
leftConstraint.isActive = true
rightConstraint.isActive = true
By the looks of things you don't need to remove the original view. You could make a copy of it then display the copy full screen. Then when you dismiss this copy you release the reference to it
Is there a way in swift to have a label and make constraints for it programmatically. For example on all devices to 'Pin to the top' or 'Pin to the right side' so that on all devices it just pins to whatever device is being used.
It's because I've created a label programmatically, so I want to make constraints for it.
If you need more info, just let me know. Thanks :)
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
self.view.addSubview(label)
label.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
This will bind the item to all the edges, making it as big as the screen. Anchor is used to refer to an point in game. which is either its superview or some view on the same level as your current view. You can use references as i did self.view.trailingAnchor, and you can also add offsets and insets view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true. Or you can name the direct size of something label.heightAnchor.constraint(equalToConstant: CGFloat).isActive = true. Good luck
You can easily add Constraints programatically using NSLayoutConstraints. Below is the code sample for it. I have used centering constraint , you can use leading and top constraints with height and width.
func addLabel(){
let newView =UILabel()
self.view.addSubview(newView)
newView.translatesAutoresizingMaskIntoConstraints = false
let horizontalConstraint = NSLayoutConstraint(item: newView, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerX, multiplier: 1, constant: 0)
let verticalConstraint = NSLayoutConstraint(item: newView, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerY, multiplier: 1, constant: 0)
let widthConstraint = NSLayoutConstraint(item: newView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: 100)
let heightConstraint = NSLayoutConstraint(item: newView, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: 100)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])
}
the code below, will stick your label to top-right as you want, on all d devices,
let aLabel = UILabel()
self.view.addSubview(aLabel)
aLabel.backgroundColor = UIColor.gray
aLabel.text = " i am a label"
aLabel.textAlignment = .center
aLabel.translatesAutoresizingMaskIntoConstraints = false
let horizontalConstraint = NSLayoutConstraint(item: aLabel, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: view, attribute:NSLayoutAttribute.trailing, multiplier: 1, constant: 0)
let verticalConstraint = NSLayoutConstraint(item: aLabel, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint])
self.view.layoutIfNeeded()
I'm trying to set 2 labels, one under other with different font size in the header. The function is called like this:
viewController.navigationItem.titleView = self.setHeader()
And code responsible for generating label is :
private func setHeader(agentName: String = "", isTyping: Bool = false) -> UIView {
let headerLabel: UILabel = {
let label = UILabel()
label.text = self.title
label.font = UIFont.systemFont(ofSize: 21)
label.textColor = UIColor.white
return label
}()
let subheaderLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 10)
return label
}()
let headerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(headerLabel)
view.addConstraints([
NSLayoutConstraint(item: headerLabel, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: headerLabel, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: headerLabel, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 16),
NSLayoutConstraint(item: headerLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 16)
])
return view
}()
if (!agentName.isEmpty) {
if (isTyping) {
subheaderLabel.text = agentName + " is typing ..."
} else {
subheaderLabel.text = agentName
}
headerView.addSubview(subheaderLabel)
}
return headerView
}
When I running IOS app there is nothing shown in the header. What is a reason?
I think you need to set the frame for the headerView. So in the initialization code for the headerView, use initializer with frame:
let headerView: UIView = {
// initialize the view with frame
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 35))
// you want to call this on the headerLabel, not on view
headerLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(headerLabel)
view.addConstraints([
NSLayoutConstraint(item: headerLabel, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: headerLabel, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1.0, constant: 0),
NSLayoutConstraint(item: headerLabel, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 16),
NSLayoutConstraint(item: headerLabel, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 16)
])
return view
}()
Moreover, I believe headerLabel you want to set translatesAutoresizingMaskIntoConstraints on headerLabel rather than on headerView (headerView is positioned through frame and not Autolayout).
Also, notice that you add subheaderLabel to the view, but you never position it, don't forget about it either (although this should cause only subheaderLabel not to be rendered properly).
I have a UITableViewCell subclass, which I setup the views all in code and than add NSLayoutConstraints to. Everything is working, except my UITabbleViewCell is not calculating its height correctly.
Here is the code of the UITableViewCell
override func updateConstraints() {
setupThumbnailImages()
super.updateConstraints()
}
func setupThumbnailImages() {
var imageViewXOrigin : CGFloat = 5.0
var imageViewYOrigin : CGFloat = 0.0
for thumbnailUrl in self.thumbnailsUrlArray {
let miniPictureView = UIImageView()
miniPictureView.backgroundColor = UIColor.blueColor()
miniPictureView.contentMode = UIViewContentMode.ScaleAspectFill
miniPictureView.clipsToBounds = true
miniPictureView.frame = CGRectMake(0, 0, 0, 0)
miniPictureView.translatesAutoresizingMaskIntoConstraints = false
if((imageViewXOrigin + 50) > frame.size.width){
imageViewYOrigin += 50
imageViewXOrigin = 5
}
contentView.addSubview(miniPictureView)
contentView.addConstraint(NSLayoutConstraint(item: miniPictureView, attribute: .Leading, relatedBy: .Equal, toItem: contentView, attribute: .Leading, multiplier: 1, constant: imageViewXOrigin))
contentView.addConstraint(NSLayoutConstraint(item: miniPictureView, attribute: .Top, relatedBy: .Equal, toItem: contentView, attribute: .Top, multiplier: 1, constant: imageViewYOrigin))
contentView.addConstraint(NSLayoutConstraint(item: miniPictureView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .Height, multiplier: 1, constant: 50))
contentView.addConstraint(NSLayoutConstraint(item: miniPictureView, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .Width, multiplier: 1, constant: 50))
let lastImage = thumbnailsUrlArray.last
if (lastImage == thumbnailUrl){
contentView.addConstraint(NSLayoutConstraint(item: miniPictureView, attribute: .Bottom, relatedBy: .Equal, toItem: contentView, attribute: .Bottom, multiplier: 1, constant: 0))
}
imageViewXOrigin += 50
}
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraint(NSLayoutConstraint(item: contentView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .Height, multiplier: 1, constant: imageViewYOrigin))
}
What happens is the miniPictureView displays as it should, however its displayed outside of the UITableViewCell's bounds and the cell stays at a height of 44.
If you want the cell to size itself based on its contents, make sure you set the following on your UITableView:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44.0 // Or any other estimate you want, just make sure to set this to some value
A couple more notes:
Your miniPictureView constraints should be added to miniPictureView itself, since they don't involve contentView or a sibling view
In order for self-sizing to work, you also have to have a complete chain of constraints from the top of the contentView to the bottom. So you should add an equality constraint between the bottom of miniPictureView and contentView so there is a constraint that will actually push the bottom of the content view.
I suspect your last two lines aren't helping. The contentView's height should be established by its top and bottom edges being constrained to its contents. And the autoresizing mask is used to scale the cell and its contentView together. If you set that to false, you should at least replace it with code that sets the cell's frame to be equal to the contentView frame AFTER the layout is calculated.
I have a UIButton that I set up with auto layout constraints in a storyboard. i also have a UIView that I initiate in the UIViewController's viewDidLoad method. I make this view have (almost) all the same properties as the UIButton but when it I run it in the simulator it doesn't "stick" to the button. here's what I have:
class ViewController: UIViewController {
#IBOutlet weak var someButton: UIButton!
func viewDidLoad() {
super.viewDidLoad()
let someView = UIView()
someView.backgroundColor = UIColor.greenColor()
someView.frame = someButton.bounds
someView.frame.origin = someButton.frame.origin
someView.autoresizingMask = someButton.autoresizingMask
someView.autoresizesSubviews = true
someView.layer.cornerRadius = someButton.layer.cornerRadius
someView.clipsToBounds = true
someView.userInteractionEnabled = false
view.insertSubview(someView, belowSubview: someButton)
}
}
I guess I’m missing the someView.auto layout constraint?
EDIT: i thought accessing the UIButton's constraints would work but they appear to be an empty array. are story board constraints are hidden?
someView.addConstraints(someButton.constraints)
thanks.
Copying the constraints that way fails because:
the constraints in the storyboard are added to the superview and not the button itself
the constraints you try to copy are referencing the button and not your new view
Instead of copying the constraints keep it simple and create new ones and reference the button:
let someView = UIView()
someView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(someView)
view.addConstraints([
NSLayoutConstraint(item: someView, attribute: .Leading, relatedBy: .Equal, toItem: someButton, attribute: .Leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: someView, attribute: .Trailing, relatedBy: .Equal, toItem: someButton, attribute: .Trailing, multiplier: 1, constant: 0),
NSLayoutConstraint(item: someView, attribute: .Top, relatedBy: .Equal, toItem: someButton, attribute: .Top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: someView, attribute: .Bottom, relatedBy: .Equal, toItem: someButton, attribute: .Bottom, multiplier: 1, constant: 0)
])