I work since 2 days on this problem without understanding the mindfuck behind it...
So to explain quickly, I create a view based on a Nib in an other storyboard like this:
let storyboard = UIStoryboard(name: "TestNib", bundle: nil)
testNib = storyboard.instantiateViewControllerWithIdentifier("TestNib") as! TestNib
testNib.view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(testNib.view)
and I add some constraints manually with constraintsWithVisualFormat like this:
constraint_V = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-(10)-[conversationMenu(300)]",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil, views: ["conversationMenu": testNib.view])
let constraint_H = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[conversationMenu]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil, views: ["conversationMenu": testNib.view])
self.view.addConstraints(constraint_V)
self.view.addConstraints(constraint_H)
No problem, everything work super fine. (I also try with a view in the storyboard by the way).
The main problem is when I want to animate this view, the constraints into "testNib" (self.view.addSubview(testNib.view)) break. And this happened when I create the constraintsWithVisualFormat in the ViewDidAppear. In the ViewDidLoad everything work perfectly...
This are my instances' variables:
var testNib: TestNib!
var constraint_V: [NSLayoutConstraint]!
var completion = false
And this is my code to animate the constraints:
if completion == false {
for constraint in constraint_V {
if constraint.firstAttribute == NSLayoutAttribute.Top {
constraint.constant = -200
}
}
} else {
for constraint in constraint_V {
if constraint.firstAttribute == NSLayoutAttribute.Top {
constraint.constant = 10
}
}
}
completion = !completion
UIView.animateWithDuration(1, animations: {
self.view.layoutIfNeeded()
}, completion: {(value: Bool) in
})
You will ask me, why if it work in the ViewDidLoad you simply do not use it?
It's because my main problem is when I "presentViewController" and dismiss it, constraints break again and the only way I found to reproduce this bug is to insert my initialization into the ViewDidAppear.
I have commit and push my code into a github if you want to test with me: https://github.com/Comanga/Bug-Constraints.
What you need to see is the green view that is not stuck anymore to the top constraint.
Oh you read this until this line? Thank you <3
iOS9 - SWIFT 2.0
Hope this would help you
viewDidAppear is called finally after UIKit laid out sub views and displayed them.
So it is not working and you can think another way to re-layout your view controller after added/removed constraints.
try setNeedsUpdateConstraints or something
Related
I've created an xib and loaded the nib in my viewDidLayOutSubviews:
I then added the subview:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if (myCustomView == nil) {
myCustomView = Bundle.main.loadNibNamed("Help", owner: self, options: nil)?.first as? HelpView
self.view.addSubview(myCustomView!)
}
}
My constraints are all set up correctly in my xib (toggling between devices look okay), however when I launch the app on a different device the autolayout is not updated. How do I fix this? Thank you!
Edit:
Toggled for iPhone 7, but launching for iPhone 7 Plus
Toggled for iPhone 7 Plus, launching for iPhone 7 Plus
Your constraints may be setup correctly in your nib, but you don't have any constraints when you call self.view.addSubview(myCustomView!), so the frame is just going to be whatever it is in the nib file. You need to constraint myCustomView to self.view. Give it equal width, center X, equal top and a fixed height (or use the intrinsic height) and it should be fine. Make sure you turn off translatesAutoresizingMaskIntoConstraints.
just add this line below
self.view.addSubview(myCustomView!)
myCustomView.translatesAutoresizingMaskIntoConstraints = false;
//Views to add constraints to
let views = Dictionary(dictionaryLiteral: ("myCustomView",myCustomView))
//Horizontal constraints
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[myCustomView]|", options: nil, metrics: nil, views: views)
self.view.addConstraints(horizontalConstraints)
//Vertical constraints
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[myCustomView(SpecifyFixedHeight)]|", options: nil, metrics: nil, views: views)
self.view.addConstraints(verticalConstraints)
I've got a custom UIView that I instantiate in a view controller with this function, displayedTimer is an iVar of the view controller:
func changeViewModeTo(mode: String){
if mode == "settings" {
addSettingsModeConstraints()
animatedLayoutIfNeeded(removeView: true)
}
if mode == "timer" {
displayedTimer = TimerView.init()
displayedTimer.frame = CGRect(x: (self.view.bounds.size.width)/2 - 50, y: (self.view.bounds.size.height)/2 - 80, width: 100, height: 160)
let colors = timer.getColorScheme()
displayedTimer.setColorScheme(colorLight: colors["lightColor"]!, colorDark: colors["darkColor"]!)
displayedTimer.setTimeRemainingLabel(timer.duration)
displayedTimer.setCountDownBarFromPercentage(1.0)
displayedTimer.layer.zPosition = 100 //make sure the timer view sits on top of the settings panel
displayedTimer.timerLabel.hidden = false
displayedTimer.translatesAutoresizingMaskIntoConstraints = false
let pinchGestureRecogniser = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchDetected(_:)))
displayedTimer.addGestureRecognizer(pinchGestureRecogniser)
self.view.addSubview(displayedTimer)
addTimerModeConstraints()
animatedLayoutIfNeeded(removeView: false)
}
}
If the mode is set to timer then it creates a subclass of UIView and sets an instance variable to it, constraints are added to make it full screen and then an animated layoutIfNeeded() is called. If the mode being set is settings then it deactivates the timerConstraints, adds new constraints to shrink the view, calls an animated layoutIfNeeded and then removes the view from the superView.
func animatedLayoutIfNeeded(removeView removeView: Bool){
UIView.animateWithDuration(0.2, delay: 0, options: [UIViewAnimationOptions.CurveEaseIn] , animations: {
self.view.layoutIfNeeded()
}) { (true) in
if removeView == true {
self.displayedTimer.removeFromSuperview()
}
}
}
The constraints are added and removed with these methods (settingsConstraints and timerConstraints are iVars of the view controller):
//MARK: - Layout Constraints
func addSettingsModeConstraints() {
let views = ["timerView": displayedTimer]
let timerHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-75-[timerView]-75-|",
options: [],
metrics: nil,
views: views)
settingsConstraints += timerHorizontalConstraints
let timerVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-105-[timerView]-85-|",
options: [],
metrics: nil,
views: views)
settingsConstraints += timerVerticalConstraints
NSLayoutConstraint.deactivateConstraints(timerConstraints)
NSLayoutConstraint.activateConstraints(settingsConstraints)
}
func addTimerModeConstraints() {
let views = ["timerView": displayedTimer]
let timerHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-0-[timerView]-0-|",
options: [],
metrics: nil,
views: views)
timerConstraints += timerHorizontalConstraints
let timerVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-0-[timerView]-0-|",
options: [],
metrics: nil,
views: views)
timerConstraints += timerVerticalConstraints
NSLayoutConstraint.activateConstraints(timerConstraints)
}
changeViewModeTo is called from a pinch gesture recogniser (negative pinch sets one mode, positive pinch sets another mode).
The first time I pinch, the view is created and goes full screen. I then reverse pinch and the view shrinks and is removed. Then when I pinch again to start the process over the app crashes, there are no console errors but there is a red error over the line of code: NSLayoutConstraint.activateConstraints(timerConstraints)
I'm guessing removing the subview has caused the reference to NSConstraints to disappear?
Any thoughts would be great as I can't figure it out.
So turns out this is a simple fix, call removeAll() on settingsConstraints and timerConstraints before recreating them and activating them solves the problem.
I'm developing a custom keyboard extension and noticed when I call self.inputView.frame.size.width in either viewDidLoad or viewWillAppear, it returns 0 or an incorrect number. Only in viewDidAppear is it correct. This is a problem because I need to update the size of elements in the keyboard based on the available width and height. Right now I am handling this by detecting the frame size in viewWillLayoutSubviews, which is called several times before it is correct. This works, but the user can see the elements changing size in the keyboard - the frame is not correct until after it has been presented to the user. This is also a problem because I need to update the scroll position once the elements are their correct size, so I'm forced to delay that until viewDidAppear. This isn't a great user experience with things jumping around after it's been presented.
Is there any way I could know the height and width of the keyboard earlier in the life cycle so I can set the size of the elements before they become visible to the user?
I am using a XIB to create the keyboard interface which utilizes Auto Layout. This is how I add it to the keyboard:
let keyboardNib = UINib(nibName: "KeyboardView", bundle: nil)
let keyboardInterface = keyboardNib.instantiateWithOwner(self, options: nil)[0] as! UIView
keyboardInterface.setTranslatesAutoresizingMaskIntoConstraints(false)
self.inputView.addSubview(keyboardInterface)
let verticalConstraints: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[keyboardInterface]-0-|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["keyboardInterface": keyboardInterface, "view": view]) as! [NSLayoutConstraint]
let horizontalConstraints: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[keyboardInterface]-0-|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["keyboardInterface": keyboardInterface]) as! [NSLayoutConstraint]
self.inputView.addConstraints(verticalConstraints)
self.inputView.addConstraints(horizontalConstraints)
How do you get the width and height of a UIView who's size and position are set using Auto Layout and Apple's Visual Format Language?
Here's the code (view is just the variable from UIViewController):
// Create and add the view
var stageView = UIView()
stageView.setTranslatesAutoresizingMaskIntoConstraints(false) // Since I'm using Auto Layout I turn this off
view.addSubview(stageView)
// Create and add constraints to the containing view
let viewsDictionary = ["stageView":stageView]
let horizontalConstraints: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("H:|-150-[stageView]-150-|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDictionary)
let verticalConstraints: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("V:|-100-[stageView]-150-|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: viewsDictionary)
view.addConstraints(horizontalConstraints)
view.addConstraints(verticalConstraints)
println("stageView.frame=\(stageView.frame)")
and got:
stageView.frame=(0.0,0.0,0.0,0.0)
so I tried:
let fittingSize = stageView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
println("fittingSize=\(fittingSize)")
and got:
fittingSize=(0.0,0.0)
I can't seem to find a way to get the size. I'm able to add subviews to stageView that place just fine using Auto Layout and Visual Format Language, but I can't get width and height for stageView which I need to further position those subviews.
Any ideas?
You have a few options:
You can force the layout engine to size the views immediately by calling setNeedsLayout and then call layoutIfNeeded. This is not recommended because it's inefficient and any manual frames required for layout might not have been set yet. You can read more about this approach on my answer to this question.
You can also wait until the subviews have been updated in the view controller:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
println("stageView.frame = \(stageView.frame)")
}
If you want to know within a UIView subclass (or more often, a UITableViewCell subclass, you can check after layoutSubviews has run:
override func layoutSubviews() {
super.layoutSubviews()
println("self.frame = \(self.frame)")
}
You need to check the frame inside viewDidLayoutSubviews.
This function run after constraint calculation
Its suppose to look something like this
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
//Print frame here
}
I need to create a view with a scroll view and a page control in it, and place 7 views inside scroll view.
To lay out subviews inside the scroll view I use pure Auto layout Approach, that is described here.
So I have my controller with XIB file (I don't use storyboards here) that is pretty simple: it's a UIScrollView and UIPageControl with all constraints set up.
And I have a XIB for a UIView subclass Slide which has 2 UIImageViews and 1 UILabel, and there's also some constraints.
To add some views to UIScrollView I use this code in viewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
var pSlide: Slide?
for var i = 0; i < 7; i++ {
var slide = Slide(frame: self.view.bounds, imageName: "slide-\(i+1)-bg", text: NSLocalizedString("slides_\(i+1)", comment: ""))
slide.setTranslatesAutoresizingMaskIntoConstraints(false)
scrollView.addSubview(slide)
var dict: [NSObject : AnyObject] = ["currentSlide" : slide]
if let previousSlide = pSlide {
dict["previousSlide"] = previousSlide
let constraintsHorizontal = NSLayoutConstraint.constraintsWithVisualFormat("H:[previousSlide][currentSlide]", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsHorizontal)
let constraintsVertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|[currentSlide]|", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsVertical)
} else {
let constraintsVertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|[currentSlide]|", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsVertical)
let constraintsLeft = NSLayoutConstraint.constraintsWithVisualFormat("H:|[currentSlide]", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsLeft)
}
if i == 6 {
let constraintsRight = NSLayoutConstraint.constraintsWithVisualFormat("H:[currentSlide]|", options: nil, metrics: nil, views: dict)
scrollView.addConstraints(constraintsRight)
}
pSlide = slide
}
pageControl.numberOfPages = numberOfSlides
view.layoutSubviews()
}
In this piece of code I create a Slide instance, and set all necessary constraints to it, according to pure Auto Layout approach.
init() method of the Slide class looks like this:
init(frame: CGRect, imageName: String, text: String) {
super.init(frame: frame)
NSBundle.mainBundle().loadNibNamed("Slide", owner: self, options: nil)
self.addSubview(self.view)
self.view.frame = frame
self.layoutIfNeeded()
println("Frame is \(frame); view.frame is \(self.view.frame)")
backgroundImage.image = UIImage(named: imageName)
textLabel.text = text
}
I hoped that
self.view.frame = frame
self.layoutIfNeeded()
will help me but no. The problem is, on 3.5 inch screen all my UIScrollView subviews have the height of 568, which is the normal height for 4 inch display, but not for 3.5 inch.
I'm checking the height in viewDidAppear(animated:) method. But, in init() method of Slide class the height appears to be ok — 480.
I'm trying to solve it for second day already, and still nothing works. I know that this may be much more simple to implement without using Auto Layout and Interface Builder, but I need to do it with these.
I used UIPageViewController instead of all this mess, and it works just fine.