Adding Constraints to ViewController with Swift - ios

I want to add container view to the main view (rootViewController.view), but the following gives exception. I know it about the constraints but not able to find out why.
import UIKit
class rootViewController : UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
setupLoginView()
}
func setupLoginView() {
// User ID label
let userIDLabel:UILabel = UILabel()
userIDLabel.text = "User ID"
// Password label
let passwordLabel:UILabel = UILabel()
passwordLabel.text = "Password"
// User ID text
let userIDText:UITextField = UITextField()
// Password text
let passwordText:UITextField = UITextField()
// Login button
let loginBtn:UIButton = UIButton()
loginBtn.setTitle("Login", for: .normal)
// Container view
let container:UIView = UIView()
container.addSubview(userIDLabel)
container.addSubview(userIDText)
container.addSubview(passwordLabel)
container.addSubview(passwordText)
container.addSubview(loginBtn)
view.addSubview(container)
// Add constraints
let heightConstraint = NSLayoutConstraint(item: container, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 300)
let widthConstraint = NSLayoutConstraint(item: container, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 200)
let centerXConstraint = NSLayoutConstraint(item: container, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0)
let centerYConstraint = NSLayoutConstraint(item: container, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)
container.addConstraint(heightConstraint)
container.addConstraint(widthConstraint)
container.addConstraint(centerXConstraint)
container.addConstraint(centerYConstraint)
}
}
Gives the following exception, with hint "Does the constraint reference something from outside the subtree of the view? That's illegal"
'NSGenericException', reason: 'Unable to install constraint on view.
Does the constraint reference something from outside the subtree of
the view? That's illegal. constraint: NSLayoutConstraint:0x170089830
UIView:0x12de138c0.centerX == UIView:0x12de0e650.centerX (active)>

Replace your code for adding the center constraints with these lines:
view.addConstraint(centerXConstraint)
view.addConstraint(centerYConstraint)
You can't add a constaint on the container with a reference to it's superview.
And move the setupLoginView() to viewDidLoad instead of init().
Don't forget to remove the warning in the console to set translatesAutoresizingMaskIntoConstraints to false for all the created views (container, labels, buttons and textviews).

1. Move setupLoginView() to viewDidLoad()
override func viewDidLoad()
{
super.viewDidLoad()
setupLoginView()
}
2. Set translatesAutoresizingMaskIntoConstraints of container
container.translatesAutoresizingMaskIntoConstraints = false
3. Add constraints to relevant views:
container.addConstraint(heightConstraint)
container.addConstraint(widthConstraint)
view.addConstraint(centerXConstraint)
view.addConstraint(centerYConstraint)

The ViewController's view does not exist yet. Move the setupLoginView() call from the init() method to viewDidLoad().
Also - as #Pranav Kasetti suggests - you have to add the center constraints to the view instead of the container.
Last but not least set translatesAutoresizingMaskIntoConstraints to false for all the created views (including the container).

The problem is that you define properties in the init method, but it's not the good entry point for a ViewController. You should take a look here for some details.
Then, replace
init() {
super.init(nibName: nil, bundle: nil)
by this:
override func viewDidLoad() {
super.viewDidLoad()
and it should works.

Related

Best approach to access IBOutlets after calling NSBundle (with loadNibName) for a custom view

I am looking for good practice how to initialize subviews (e.g. text of labels, or buttons) of a custom view that are connected via IBOutlets.
The custom view's view controller is calling the xib file on init like this:
final class MenuDiscoveryListView : NSView, MenuDiscoveryListViewProtocol {
let C_NAME_XIB = "MenuDiscoveryList"
#IBOutlet weak var labelStatus: NSTextField!
#IBOutlet weak var stackList: NSStackView!
var presenter : MenuDiscoveryListPresenter?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
xibInit(autoXibLoading: true)
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
xibInit(autoXibLoading: false)
}
/// Routine for initializating view
///
/// - Parameter loadXib: Select if auto-load from related xib file into the view container is desired. Select TRUE, if function is called from NSView's `init(frame frameRect: NSRect)`.
func xibInit(autoXibLoading loadXib : Bool = true) {
// Load xib item
if loadXib {
var topLevelObjects : NSArray?
if Bundle(for: type(of: self)).loadNibNamed(C_NAME_XIB, owner: self, topLevelObjects: &topLevelObjects) {
if let contentView = topLevelObjects!.first(where: { $0 is NSView }) as? NSView {
// Add loaded view from xib file into container as subview
self.addSubview(contentView)
// Transfer dimensions
self.frame = contentView.frame
// Define constraints
self.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
}
}
}
}
}
The init of the view controller is called from another view controller's presenter module in a very classy way:
let view = MenuHeaderItemView()
However, after initializing the view controller, as expected, the IBOutlets found nil. Nevertheless, I wanted to set a string value of labelStatus right after initializing the view (e.g. standard string) through NSBundle's (or Bundle's) loadNibName without waiting for awakeFromNib.
What is a good practice or approach to do this synchronously and access the IBOutlets right after the init?
EDIT:
I have realized that labelStatus and stackList are successfully loaded in contentView:
Is there any elegant way to copy their content/instantiation over to the IBOutlets?
with the statement
let view = MenuHeaderItemView()
The view controller has not yet loaded its view hierarchy/ Subviews.
From my understanding you may use:
let customView = Bundle.main.loadNibNamed("MenuDiscoveryList", owner: nil,
options: nil)?[0] as? MenuDiscoveryListView
if let menuView = customView {
menuView.labelStatus.text = "You label string"
}
Thanks, have a try with this.

Programatic constraints collapsing cell view

I am developing a UITableViewCell that starts as a xib, has views added to it programmatically, and has a dynamically sized height. However, it looks like when adding the programatic views with constraints, it is conflicting with the auto-resize constraint initially applied to the xib, and causing issues. Please see below:
Dequeuing my cells:
//Table Delegate/Datasource
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:S360SSessionMatchTableCell? = tableView.dequeueReusableCellWithIdentifier(XIBFiles.SESSIONMATCHTABLECELL + String(indexPath.row)) as? S360SSessionMatchTableCell
if ((cell == nil)){
tableView.registerNib(UINib(nibName: XIBFiles.SESSIONMATCHTABLECELL, bundle: nil), forCellReuseIdentifier: XIBFiles.SESSIONMATCHTABLECELL + String(indexPath.row))
cell = tableView.dequeueReusableCellWithIdentifier(XIBFiles.SESSIONMATCHTABLECELL + String(indexPath.row)) as? S360SSessionMatchTableCell
}
cell!.setupEvents(sessionMatches[indexPath.row]["sessions"]! as! [[String:String]])
return cell!
}
Setup Events Method in Custom UITableViewCell:
func setupEvents(events:[[String:String]]){
//Set up start and end times
self.startTimeLbl.text = events[0]["startTime"]!
self.endTimeLbl.text = events[events.count - 1]["endTime"]!
//Set up events
var pastEventView:S360SScheduledEventView? = nil
var pastEvent:[String:String]? = nil
for (index, event) in events.enumerate(){
var topAnchor:NSLayoutConstraint!
//Create event view
let eventView:S360SScheduledEventView = NSBundle.mainBundle().loadNibNamed(XIBFiles.SCHEDULEDEVENTVIEW, owner: self, options: nil)[0] as! S360SScheduledEventView
//Deal with first view added
if pastEvent == nil{
//Top anchor setup for first view
topAnchor = NSLayoutConstraint(item: eventView, attribute: .Top, relatedBy: .Equal, toItem: toLbl, attribute: .Bottom, multiplier: 1, constant: 10)
}
else{
//Check for a break
let timeFormatter:NSDateFormatter = NSDateFormatter()
timeFormatter.dateFormat = "hh:mm a"
let startTime = timeFormatter.dateFromString(pastEvent!["endTime"]!)
let endTime = timeFormatter.dateFromString(event["startTime"]!)
if startTime != endTime {
//Create break view
let breakView = NSBundle.mainBundle().loadNibNamed(XIBFiles.SCHEDULEDBREAKVIEW, owner: self, options: nil)[0] as! S360SScheduledBreakView
//Setup breakview constraints
let bTopAnchor = NSLayoutConstraint(item: breakView, attribute: .Top, relatedBy: .Equal, toItem: pastEventView, attribute: .Bottom, multiplier: 1, constant: 0)
let bLeftAnchor = NSLayoutConstraint(item: breakView, attribute: .Leading, relatedBy: .Equal, toItem: self.contentView, attribute: .LeadingMargin, multiplier: 1, constant: 0)
let bRightAnchor = NSLayoutConstraint(item: breakView, attribute: .Trailing, relatedBy: .Equal, toItem: self.contentView, attribute: .TrailingMargin, multiplier: 1, constant: 0)
let bHeightAnchor = NSLayoutConstraint(item: breakView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 30)
//Add break view and constraints
self.addSubview(breakView)
self.addConstraints([bTopAnchor, bLeftAnchor, bRightAnchor, bHeightAnchor])
//Top anchor setup for subsequent view
topAnchor = NSLayoutConstraint(item: eventView, attribute: .Top, relatedBy: .Equal, toItem: breakView, attribute: .Bottom, multiplier: 1, constant: 0)
}
else{
//Top anchor setup for subsequent views
topAnchor = NSLayoutConstraint(item: eventView, attribute: .Top, relatedBy: .Equal, toItem: pastEventView, attribute: .Bottom, multiplier: 1, constant: 0)
}
}
//Setup other anchors
let leftAnchor = NSLayoutConstraint(item: eventView, attribute: .Leading, relatedBy: .Equal, toItem: self.contentView, attribute: .LeadingMargin, multiplier: 1, constant: 0)
let rightAnchor = NSLayoutConstraint(item: eventView, attribute: .Trailing, relatedBy: .Equal, toItem: self.contentView, attribute: .TrailingMargin, multiplier: 1, constant: 0)
let heightAnchor = NSLayoutConstraint(item: eventView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 60)
//Setup event view
eventView.iconImg.image = Images.get_event_image(event["title"]!)
eventView.titleLbl.text = event["title"]!
eventView.courtLbl.text = "court" + event["court"]!
eventView.timeLbl.text = event["startTime"]! + " to " + event["endTime"]!
//Add event view and constraints
self.addSubview(eventView)
self.addConstraints([topAnchor, leftAnchor, rightAnchor, heightAnchor])
//Prepare for next iteration
pastEventView = eventView
pastEvent = event
//Set up last cell with bottom bound
if index == events.count - 1 {
let bottomAnchor = NSLayoutConstraint(item: eventView, attribute: .Bottom, relatedBy: .Equal, toItem: self.contentView, attribute: .BottomMargin, multiplier: 1, constant: 0)
self.addConstraint(bottomAnchor)
}
}
}
Constraints in xib:
This is the error I get (pasted once, but it occurs for each cell):
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
2016-07-05 15:13:01.654 Shoot360 Scheduler[32779:642808] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x7fedd85d5590 h=--& v=--& V:[UITableViewCellContentView:0x7fedda431120(44)]>",
"<NSLayoutConstraint:0x7fedda43a7e0 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda438b20(60)]>",
"<NSLayoutConstraint:0x7fedda436590 UITableViewCellContentView:0x7fedda431120.topMargin == UILabel:0x7fedda4312a0'10:00 AM'.top - 15>",
"<NSLayoutConstraint:0x7fedda436630 UILabel:0x7fedda431c00'to'.top == UILabel:0x7fedda4312a0'10:00 AM'.top>",
"<NSLayoutConstraint:0x7fedda433b60 V:[UILabel:0x7fedda431c00'to']-(10)-[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda438b20]>",
"<NSLayoutConstraint:0x7fedda445910 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda4443f0(60)]>",
"<NSLayoutConstraint:0x7fedda448310 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda438b20]-(0)-[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda4443f0]>",
"<NSLayoutConstraint:0x7fedda449a00 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda448540(60)]>",
"<NSLayoutConstraint:0x7fedda4479e0 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda4443f0]-(0)-[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda448540]>",
"<NSLayoutConstraint:0x7fedda44a100 Shoot360_Scheduler.S360SScheduledEventView:0x7fedda448540.bottom == UITableViewCellContentView:0x7fedda431120.bottomMargin>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fedda436590 UITableViewCellContentView:0x7fedda431120.topMargin == UILabel:0x7fedda4312a0'10:00 AM'.top - 15>
Row height is being set to dynamic:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Styling
showAllBtn.layer.cornerRadius = Numbers.CORNERRADIUS
sessionsTbl.rowHeight = UITableViewAutomaticDimension
sessionsTbl.estimatedRowHeight = 500
sessionsTbl.layer.borderColor = Colors.REALLIGHTGREY.CGColor
sessionsTbl.layer.borderWidth = Numbers.BORDERREG
sessionsTbl.layer.cornerRadius = Numbers.CORNERRADIUS
sessionsTbl.separatorStyle = UITableViewCellSeparatorStyle.None
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
The constraint
V:[UITableViewCellContentView:0x7fedda431120(44)
means that rowHeight in your table is set to the default value of 44pt while you want the cell height to be dynamic. You will have to set rowHeight to UITableViewAutomaticDimension and also set estimatedRowHeight.
Also note that cells are reused therefore you will have to remove all previously added views everytime you call setupEvents.
Also note you should not call tableView.registerNib(...) from inside cellForRow method. The good place to register cells is inside viewDidLoad.
It seems you've made life much more complicated for yourself than it needs to be.
If we look at what you currently have:
a table with 1 section and many rows
1 cell subclass
each row has an arbitrary number of subviews added on the fly
each subview is pinned to each other with constraints
each cell is explicitly instantiated for each row, not properly reused
cells don't have their subviews removed when they are reused
this doesn't fit well with a table view and means you're writing a lot of code and trying to cram it all into one place.
Looking at your data it would be better to have something like:
a table with multiple sections, each section having multiple rows
1 section per session
1 row per 'event'
1 section header class with date labels
2 cell subclasses, 1 for an event and one for a break
no views added on the fly
In this scenario your constraints are trivial and there are no constraints being added in code, you just set a bit of data and everything else just works. This scheme also breaks down your concerns and separates out the code for each different part into logical parts.
Rather than try to fix your existing issue you should step back and look at your approach.

App is crashing on NSLayoutConstraint when added programmatically

I am getting the following error :
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)
Basically I want to have a blue view that is 200h x 200w that is centered in the middle of the screen.
UIViewController.swift
override func loadView() {
super.loadView()
self.view = View()
}
Meanwhile in the View.swift
class View: UIView {
var blueView : UIView?
convenience init() {
// also tried UIScreen.mainScreen().bounds
self.init(frame:CGRectZero)
self.backgroundColor = UIColor.redColor()
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init:coder")
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func setupView() {
self.blueView = UIView()
self.blueView?.backgroundColor = UIColor.blueColor()
self.blueView?.translatesAutoresizingMaskIntoConstraints = false
// self.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.blueView!)
let centerXConstraint = NSLayoutConstraint(
// object that we want to constrain
item: self.blueView!,
// the attribute of the item we want to constraint
attribute: NSLayoutAttribute.CenterX,
// how we want to relate this item with another item so most likely its parent view
relatedBy: NSLayoutRelation.Equal,
// this is the item that we are setting up the relationship with
toItem: self,
attribute: NSLayoutAttribute.CenterX,
// How much I want the CenterX of BlueView to Differ from the CenterX of the self
multiplier: 1.0,
constant: 0)
let centerYConstraint = NSLayoutConstraint(
item: self.blueView!,
attribute: NSLayoutAttribute.CenterY,
relatedBy: NSLayoutRelation.Equal,
toItem: self,
attribute: NSLayoutAttribute.CenterY,
multiplier: 1.0,
constant: 0)
// These work but the previous two don't
let widthContraint = NSLayoutConstraint(
item: self.blueView!,
attribute: NSLayoutAttribute.Width,
relatedBy: NSLayoutRelation.Equal,
toItem: nil,
attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0,
constant: 200)
let heightConstraint = NSLayoutConstraint(
item: self.blueView!,
attribute: NSLayoutAttribute.Height,
relatedBy: NSLayoutRelation.Equal,
toItem: nil,
attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0,
constant: 200)
self.blueView?.addConstraints([widthContraint, heightConstraint, centerXConstraint, centerYConstraint])
}
Height and width constraints belong to the view they pertain to. Centering and positioning belong to that view's parent so you must add these two to the parent, not the view itself.

Programmatically copy existing constraints to a new object

I'm allowing the user to create a new text field when they press a button. I want to programmatically copy the leading and trailing constraints from an existing text field. My code:
#IBAction func addAnotherTextField(sender: AnyObject) {
let newTextField = UITextField.init(frame: CGRectMake(20, positionY, self.view.frame.size.width-40, 30))
newTextField.delegate = self
newTextField.tag = fieldCount
newTextField.placeholder = "You created this!"
newTextField.borderStyle = UITextBorderStyle.RoundedRect
newTextField.translatesAutoresizingMaskIntoConstraints = false
let leadingConstraint = NSLayoutConstraint(item: newTextField, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: nameTextField, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)
let trailingConstraint = NSLayoutConstraint(item: newTextField, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: nameTextField, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: 0)
newTextField.addConstraint(leadingConstraint)
newTextField.addConstraint(trailingConstraint)
view.addSubview(newTextField)
fieldCount++
positionY = positionY + 15 + newTextField.frame.size.height
}
}
Unfortunately, the above code crashes at run time.
You need first to addSubView, only then to add the constraints.
You can't connect constraints between UIViews that are not related...
From the docs:
Discussion:
The constraint must involve only views that are within scope of the receiving view. Specifically, any views involved must be either the receiving view itself, or a subview of the receiving view. Constraints that are added to a view are said to be held by that view. The coordinate system used when evaluating the constraint is the coordinate system of the view that holds the constraint.

Center View in superview

I'm trying to center my subview with a button in its superview. So I want the center of the subview be the center of the superview. I'm trying that with following code:
override func viewDidLoad() {
self.view.backgroundColor = UIColor.redColor()
var menuView = UIView()
var newPlayButton = UIButton()
//var newPlayImage = UIImage(named: "new_game_button_5cs")
var newPlayImageView = UIImageView(image: UIImage(named: "new_game_button_5cs"))
newPlayButton.frame = CGRectMake(0, 0, newPlayImageView.frame.width, newPlayImageView.frame.height)
newPlayButton.setImage(newPlayImage, forState: .Normal)
newPlayButton.backgroundColor = UIColor.whiteColor()
menuView.backgroundColor = UIColor.whiteColor()
menuView.addSubview(newPlayButton)
menuView.addConstraint(
NSLayoutConstraint(item: self.view,
attribute: .CenterX,
relatedBy: .Equal,
toItem: menuView,
attribute: .CenterX,
multiplier: 1, constant: 0)
)
}
Unfortunately the program breaks when I try to run it.
(Thread 1: signal SIGABRT)
Your code triggers an assertion saying:
When added to a view, the constraint's items must be descendants of
that view (or the view itself).
This means you have to add menuView as a subview to self.view before adding constraints. You should also add the constraints to self.view, not the menuView. Last but not least, remove autoresizing masks constraints that were implicitly added to menuView by calling setTranslatesAutoresizingMaskIntoConstraints(false) or autolayout will complain about conflicting constraints.
menuView.addSubview(newPlayButton)
menuView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(menuView)
self.view.addConstraint(
NSLayoutConstraint(item: self.view,
attribute: .CenterX,
relatedBy: .Equal,
toItem: menuView,
attribute: .CenterX,
multiplier: 1, constant: 0)
)

Resources