Swift auto layout programmatically and dynamic - swift

I have a View where I load buttons dynamically. So I have a for loop to loop through all buttons. Since this is dynamically I want to create the auto layout programmatically. Right now I have the following code:
for var i = 0; i < data.count; i++ {
let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
button.backgroundColor = UIColor.greenColor()
button.setTitle("Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(button)
let centerXConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0)
let centerYConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: scrollView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant:15)
let widthConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 200)
let heightConstraint = NSLayoutConstraint(item: button, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant:100)
scrollView.addConstraints([centerXConstraint, centerYConstraint, widthConstraint, heightConstraint])
}
This creates the first button and places it 15px under the top bar. The problem I have is how to place the next button 15px under the first one, the third button 15px under the second one etc. Anyone got any ideas?

Thats definitely possible, but firstly, I should mention it's not required that you add constraints for the buttons' width and height since they have an intrinsic content size (like UILabel) which depends on attributes such their text and font.
Back to your problem!
Here's the code, the explanation for each step is below:
override func viewDidLoad() {
super.viewDidLoad()
// 1.
var upperView: UIView = scrollView
for i in 0..<data.count {
let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
button.backgroundColor = UIColor.greenColor()
button.setTitle("Button", forState: UIControlState.Normal)
button.setTranslatesAutoresizingMaskIntoConstraints(false)
// 2.
scrollView.addSubview(button)
// 3.
let attribute: NSLayoutAttribute = i == 0 ? .Top : .Bottom
// 4.
let topEdgeConstraint = NSLayoutConstraint(item: button,
attribute: .Top,
relatedBy: .Equal,
toItem: upperView,
attribute: attribute,
multiplier: 1.0,
constant: 15.0)
let centerXConstraint = NSLayoutConstraint(item: button,
attribute: .CenterX,
relatedBy: .Equal,
toItem: scrollView,
attribute: .CenterX,
multiplier: 1.0,
constant: 0.0)
scrollView.addConstraint(topEdgeConstraint)
scrollView.addConstraint(centerXConstraint)
// 5.
if i == data.count - 1 {
let bottomConstraint = NSLayoutConstraint(item: button,
attribute: .Bottom,
relatedBy: .Equal,
toItem: scrollView,
attribute: .Bottom,
multiplier: 1.0,
constant: -15.0)
scrollView.addConstraint(bottomConstraint)
}
upperView = button
}
}
1. upperView is used to keep track of the view 'above' the current button. For example, when the first button is created, the upperView is the UIScrollView For the second button, upperView is the first button; for the third button, upperView is the second button and so on...
2. Your buttons should be added to the UIScrollView, not the self.view. Otherwise you'll get the error:
The view hierarchy is not prepared for the constraint...
3. This line selects the attribute on the upperView that will relate the button to the upperView. Here's a picture to demonstrate what I mean:
a) The .Top of the button is related to the .Top of the UIScrollView.
b) The .Top of the button is related to the .Bottom of the previous UIButton.
4. Making the top and centre X constraints - that's all pretty self explanatory.
5. For the UIScrollView to correctly calculate its contentSize it must have constraints in an unbroken chain from it top to bottom (the top to the bottom, in this case, because it needs to scroll vertically). Therefore, if it's the last UIButton a constraint from its bottom edge is added to the UIScrollView's bottom edge.
Hope that helps!

Related

How to put a uiview back in a stackview after I took it out?

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

UIPageControl has background on tvOS

In one of my games I am using a UICollectionView as the level select menu. I recently added a UIPageControl to it programatically.
/// Page control
func setupPageControl() {
pageControl.hidesForSinglePage = true
pageControl.numberOfPages = DataSource.worlds
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.currentPageIndicatorTintColor = DataSource.pageControlColors[pageControl.currentPage]
pageControl.pageIndicatorTintColor = UIColor.white.withAlphaComponent(0.8)
pageControl.addTarget(self, action: #selector(didPressPageControl), for: .valueChanged)
view.addSubview(pageControl)
let leading = NSLayoutConstraint(item: pageControl, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0)
let trailing = NSLayoutConstraint(item: pageControl, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0)
let bottomConstant = Device.isPad ? view.frame.width / 9 : view.frame.width / 17
let bottom = NSLayoutConstraint(item: pageControl, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: -bottomConstant)
view.addConstraints([leading, trailing, bottom])
}
Everything is fine on iOS but on tvOS the PageController has a translucent background that stretches all across the screen.
How can I turn this off? I tried setting the pageControl background color but that does not seem to work.
As usual just when I post a question I find the answer a minute later.
You can remove the background on tvOS by calling this
#if os(tvOS)
for subview in pageControl.subviews {
let effectView = subview as? UIVisualEffectView
effectView?.removeFromSuperview()
}
#endif
Change background color for page control

UITableViewCell subclass with Auto Layout constraints incorrect height

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.

Adding NSLayoutConstraints to UISegmentedControl causes UISegmentedControl to disappear

I have a UISegmentedControl defined programmatically.
I am trying to add a layout constraint so that when my iPad rotates, the segmented control sizes correctly within the rotated view rather than spill off screen.
I apply the following constraint:
streamSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
let segmentedControlWidth = NSLayoutConstraint(item: streamSegmentedControl,
attribute: .Width,
relatedBy: .Equal,
toItem: self.containerView,
attribute: .Width,
multiplier: 1.0,
constant: -10.0)
containerView.addConstraint(segmentedControlWidth)
My UIsegmentControl is defined as follows:
streamSegmentedControl = UISegmentedControl(items: ["Today's Events", "Past Events"])
streamSegmentedControl.frame = CGRectMake(-10,containerView.frame.size.height*0.3,containerView.frame.width+20,40)
streamSegmentedControl.selectedSegmentIndex = 0
streamScope = "today"
streamSegmentedControl.setTitleTextAttributes(segmentedControlFont as [NSObject : AnyObject], forState: .Normal)
streamSegmentedControl.backgroundColor = UIColor.colorFromClass("background")
streamSegmentedControl.tintColor = UIColor.colorFromClass("default")
streamSegmentedControl.addTarget(self, action: "changeStreamScope:", forControlEvents: UIControlEvents.ValueChanged)
containerView.addSubview(streamTableView)
containerView.addSubview(streamSegmentedControl)
I don't get an error, but at runtime, my segmented control disappears. Not sure what I am missing here as I've only done auto layout within storyboards in the past.
I only want to be able to adjust the width of the segmented control, so I assume I only need a single layout constraint.
Can anyone give me some direction? Thanks.
You need to add three constraints at least. Leading, top and trailing.
I am weak in swift but your constraints should look like.
let segmentedControlTop = NSLayoutConstraint(item: streamSegmentedControl,
attribute: .Top,
relatedBy: .Equal,
toItem: self.containerView,
attribute: .Top,
multiplier: 1.0,
constant: 0.0)
let segmentedControlLeading = NSLayoutConstraint(item: streamSegmentedControl,
attribute: .Leading,
relatedBy: .Equal,
toItem: self.containerView,
attribute: .Leading,
multiplier: 1.0,
constant: 0.0)
let segmentedControlTrailing = NSLayoutConstraint(item: streamSegmentedControl,
attribute: .Trailing,
relatedBy: .Equal,
toItem: self.containerView,
attribute: .Trailing,
multiplier: 1.0,
constant: 0.0)
containerView.addConstraint(segmentedControlTop)
containerView.addConstraint(segmentedControlLeading)
containerView.addConstraint(segmentedControlTrailing)
If you want to keep a 5 point offset from superview, try playing with constant values of leading and trailing using 5 points.

Swift - resignFirstResponder resets contentOffset on my UITextView. How come?

So I have a pretty simple app I am trying to build. Its basically a scrollable text field that fills up the entire screen.
When the user taps the screen, the keyboard appears and you can begin to edit on the line where you tapped.
If you tap in the area where the keyboard is going to appear, the size of the textView shrinks so you aren't entering text behind the keyboard.
Everything up until here works. Ill put the code at the bottom.
When the user is done editing, there is a 'Done' button in the upper right hand of the screen, When they press that, the keyboard should disappear, and, if the amount of text is more than what fits on the screen, whichever line they were editing should be at the bottom of the screen.
Right now, no matter what I try, when I resignFirstResponder to hide the keyboard, the textView resets the contentOffset to (0,0)
When I am in the above view, I press done, and this is the result:
what I would like to happen is where I was editing to be at the bottom of the screen, like this:
Class variables so they can be accessed from anywhere in the file
var textField: UITextView = UITextView()
var withKeyboard: NSLayoutConstraint!
var withoutKeyboard: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textField)
textField.scrollEnabled = true
textField.selectable = true
textField.bounces = true
textField.contentMode = UIViewContentMode.Bottom
textField.setTranslatesAutoresizingMaskIntoConstraints(false)
self.withoutKeyboard = NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)
self.view.addConstraint(NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0))
self.view.addConstraint(self.withoutKeyboard)
self.view.addConstraint(NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Left, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Right, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 0))
}
func keyboardWillShow(notification: NSNotification){
doneButton.hidden = false
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
keyboardHeight = keyboardSize.height
self.withKeyboard = NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: -keyboardHeight)
self.view.removeConstraint(withoutKeyboard)
self.view.addConstraint(withKeyboard)
textField.layoutIfNeeded()
}
}
func keyboardWillHide(notification: NSNotification){
self.doneButton.hidden = true
self.textFieldOffset = self.textField.contentOffset.y - self.keyboardHeight
println(self.textFieldOffset)
self.view.removeConstraint(withKeyboard)
self.view.addConstraint(withoutKeyboard)
textField.layoutIfNeeded()
textField.contentOffset.y = self.textFieldOffset
}
func donePressed(){
textField.resignFirstResponder()
createRecord()
}
I made the withKeyboard and withoutKeyboard constraints so that I could take one off and add the other one whenever the keyboard appears/disappears.
Anyway, when I hit the done button, it resets the view to the very top. This isnt all of the code, its just the parts that are giving me trouble.