I am practicing auto-layout programmatically. I want to put a UIView centered in the controller whose width will be 4/5 in portrait mode but when it will go to the landscape mode, I need the height to be of 4/5 of the super view's height, rather than the width.
Something like -
So, I am deactivating and then activating the constrains required depending on the orientation but when I change rotation, it gives me conflict as if it didn't deactivated the ones, I specified to be deactivated. Here is my full code. As It is storyboard independent, one can just assign the view controller class to a view controlller and see the effect.
class MyViewController: UIViewController {
var widthSizeClass = UIUserInterfaceSizeClass.unspecified
var centeredView : UIView = {
let view = UIView()
view.backgroundColor = UIColor.systemGreen
return view
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.addSubview(centeredView)
centeredView.translatesAutoresizingMaskIntoConstraints = false
}
override func viewWillLayoutSubviews(){
super.viewWillLayoutSubviews()
widthSizeClass = self.traitCollection.horizontalSizeClass
addConstrainsToCenterView()
}
func addConstrainsToCenterView() {
centeredView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
centeredView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
let compactWidthAnchor = centeredView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 4/5)
let compactHeightAnchor = centeredView.heightAnchor.constraint(equalTo: centeredView.widthAnchor)
let regularHeightAnchor = centeredView.heightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.heightAnchor, multiplier: 4/5)
let regularWidthAnchor = centeredView.widthAnchor.constraint(equalTo: centeredView.heightAnchor)
if widthSizeClass == .compact{
NSLayoutConstraint.deactivate([regularWidthAnchor, regularHeightAnchor])
NSLayoutConstraint.activate([compactWidthAnchor, compactHeightAnchor])
}
else{
NSLayoutConstraint.deactivate([compactWidthAnchor, compactHeightAnchor])
NSLayoutConstraint.activate([regularWidthAnchor, regularHeightAnchor])
}
}
}
Can anyone please help me detect my flaw.
Couple issues...
1 - many iPhone models only have wC hR (portrait) and wC hC (landscape) size classes. So, if you're checking for the .horizontalSizeClass on those devices it will always be .compact. You likely want to be checking the .verticalSizeClass
2 - the way you have your code, you are creating NEW constraints every time you call addConstrainsToCenterView(). You're not activating / deactivating existing constraints.
Take a look at this:
class MyViewController: UIViewController {
var heightSizeClass = UIUserInterfaceSizeClass.unspecified
var centeredView : UIView = {
let view = UIView()
view.backgroundColor = UIColor.systemGreen
return view
}()
// constraints to activate/deactivate
var compactAnchor: NSLayoutConstraint!
var regularAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(centeredView)
centeredView.translatesAutoresizingMaskIntoConstraints = false
// centeredView is Always centerX and centerY
centeredView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
centeredView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor).isActive = true
// for a square (1:1 ratio) view, it doesn't matter whether we set
// height == width
// or
// width == height
// so we can set this Active all the time
centeredView.heightAnchor.constraint(equalTo: centeredView.widthAnchor).isActive = true
// create constraints to activate / deactivate
// for regular height, set the width to 4/5ths the width of the view
regularAnchor = centeredView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 4/5)
// for compact height, set the height to 4/5ths the height of the view
compactAnchor = centeredView.heightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.heightAnchor, multiplier: 4/5)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// use .verticalSizeClass
heightSizeClass = self.traitCollection.verticalSizeClass
updateCenterViewConstraints()
}
func updateCenterViewConstraints() {
if heightSizeClass == .compact {
// if height is compact
regularAnchor.isActive = false
compactAnchor.isActive = true
}
else{
// height is regular
compactAnchor.isActive = false
regularAnchor.isActive = true
}
}
}
With that approach, we create two vars for the constraints we want to activate/deactivate:
// constraints to activate/deactivate
var compactAnchor: NSLayoutConstraint!
var regularAnchor: NSLayoutConstraint!
Then, in viewDidLoad(), we add centeredView to the view, set its "non-changing" constraints - centerX, centerY, aspect-ratio - and create the two activate/deactivate constraints.
When we change the size class, we only have to deal with the two var constraints.
Possibly not an answer, but to go along with my comment, here's code I use successfully:
var p = [NSLayoutConstraint]()
var l = [NSLayoutConstraint]()
Note, p and l are arrays and stand for portrait and landscape respectively.
override func viewDidLoad() {
super.viewDidLoad()
setupConstraints()
}
Nothing much here, just showing that constraints can be set up when loading the views.
func setupConstraints() {
// for constraints that do not change, set `isActive = true`
// for constants that do change, use `p.append` and `l.append`
// for instance:
btnLibrary.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
p.append(btnLibrary.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 10))
l.append(btnLibrary.bottomAnchor.constraint(equalTo: btnCamera.topAnchor, constant: -10))
Again, nothing much here - it looks like you are doing this. Here's the difference I'm seeing in your view controller overrides:
var initialOrientation = true
var isInPortrait = false
override func viewWillLayoutSubviews() {
super.viewDidLayoutSubviews()
if initialOrientation {
initialOrientation = false
if view.frame.width > view.frame.height {
isInPortrait = false
} else {
isInPortrait = true
}
view.setOrientation(p, l)
} else {
if view.orientationHasChanged(&isInPortrait) {
view.setOrientation(p, l)
}
}
}
public func orientationHasChanged(_ isInPortrait:inout Bool) -> Bool {
if self.frame.width > self.frame.height {
if isInPortrait {
isInPortrait = false
return true
}
} else {
if !isInPortrait {
isInPortrait = true
return true
}
}
return false
}
public func setOrientation(_ p:[NSLayoutConstraint], _ l:[NSLayoutConstraint]) {
NSLayoutConstraint.deactivate(l)
NSLayoutConstraint.deactivate(p)
if self.bounds.width > self.bounds.height {
NSLayoutConstraint.activate(l)
} else {
NSLayoutConstraint.activate(p)
}
}
Some of this may be overkill for your use. But instead of size classes, I actually check the bounds, along with detecting the initial orientation. For my use? I'm actually setting either a sidebar or a bottom bar. Works in all iPhones and iPads.
Again, I'm not seeing anything major - activating/deactivating a named(?) array of constraints instead of creating the arrays, the order of doing this, the override you are using... the one thing that jumps out (for me) is looking at size classes. (Possibly finding out what the initial size class is?)
I'm currently working through documenting how a UISplitViewController decided to show either the Secondary or Compact VC. Turns out that it behaves differently in at least five groups - iPad (always Secondary), iPad split screen (Compact in all iPads except iPad Pro 12.9 in landscape when half screen), iPhone portrait (always Compact), and finally, iPhone Landscape (compact for most, but Secondary for (iPhone 8 Plus, iPhone 11, iPhone 11 Pro Max, and iPhone 12 Pro Max.)
NOTE: It's Compact for iPhone 11 Pro, iPhone 12, and iPhone 12 Pro! I was surprised at this. (Next up for me is directly testing the size classes.)
My point? Maybe you need to go at the screen bounds to determine what layout you want instead of size classes. That is more in your control than size classes. Either way, good luck!
It’s quite simple. Each time layout happens and each time you say eg:
let regularWidthAnchor = centeredView.widthAnchor.constraint(equalTo: centeredView.heightAnchor)
NSLayoutConstraint.deactivate([regularWidthAnchor, regularHeightAnchor])
you are not deactivating the existing active constraint in the interface. You are creating a completely new constraint and then deactivating it (which is pointless as it was never active) and then throwing it away.
You need to create these constraints just once and keep references to them.
I have drag and dropped the constraint in my code and can access it.
#IBOutlet weak var betweenTextTerms: NSLayoutConstraint!
Howeever it seems all I can do is set the constant.
But how do I set the value for variations such as Compact Regular in code?
To do this programmatically you override traitCollectionDidChange: method in your view controller.
You can then look at self.traitCollection.horizontalSizeClass and self.traitCollection.verticalSizeClass to decide what to do. Use the reference that you have created to the layout constraint to set the constant accordingly.
After all of your layout constraints are set, call updateConstraints on your view to trigger a layout pass.
For example:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
let hCompact = (self.traitCollection.horizontalSizeClass == .compact)
let vRegular = (self.traitCollection.verticalSizeClass == .regular)
if hCompact && vRegular {
self.betweenTextTerms.constant = 45
}
self.updateViewConstraints()
}
For simple cases, it is much more convenient to do this in the Storyboard!
Here is my tableview row/cell:
there are constraints set in place - the imageview is below the label and the button is below the imageview.
here is my code:
if(row == 1) {
imageview.hidden = false
} else {
imageview.hidden = true
//how can i change the button constraint from below imageview to below label?
Adding and removing constraints is really bad example for that. I'll make your UI more complex.
Best way of solving these auto-layout problems is adding two constraints. One from imageView to button and second from imageView to label.
Now after setting these constraints, you need to set their priority levels. So, let's say button will be below the imageView first. In this case, you need to set imageView to button constraint's priority to something like 750 or UILayoutPriorityDefaultHigh and label to button constraint's priority to 250 or UILayoutPriorityDefaultLow.
Let's start creating a custom UITableViewCell
class YourTableViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
#IBOutlet weak var buttonToLabelConstraint: NSLayoutConstraint!
#IBOutlet weak var buttonToImageViewConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func shouldHideImageView(hidden: Bool) {
if(hidden == false) {
buttonToLabelConstraint.priority = UILayoutPriorityDefaultLow
buttonToImageViewConstraint.priority = UILayoutPriorityDefaultHigh
imageView.hidden = true
} else {
buttonToLabelConstraint.priority = UILayoutPriorityDefaultHigh
buttonToImageViewConstraint.priority = UILayoutPriorityDefaultLow
imageView.hidden = false
}
self.contentView.layoutIfNeeded()
}
}
After that, in your class where tableView is placed implement a logic like that:
if(row == 1) {
cell.shouldHideImageView(true)
} else {
cell.shouldHideImageView(false)
}
You should be all set.
You can try using a StackView, when you tell something to be hidden, the imageView the stack view will adjust the StackView as if the imageView was never a part of the view and it is an easy work around to not have to worry about constraints.
You can create IBOutlet on constraint and then just simply change the value like this:
buttonConstraint.constant = newValue
But i suggest you create for this a tableView. In this case you code and logic, i think, will be more accurate.
you could to this instead of hiding.
Make an outlet from the heights constraint of the imageview, call it constraint for now.
Set constraint.constant = 0 // effectively same as hiding.
Set constraint.constant = NON_ZERO_VALUE // effectively same as show.
hope it helps!
I see a couple of options. The first is a little easier to implement but a little less flexible if you decide to change your layout later.
Make the button's constraint to be below the label. Keep a reference to this constraint (you can connect it to your code via storyboard just like you do with the button itself, if you're using storyboard). When the imageView is visible, set myConstraint.constant += myImageView.frame.height. When the imageView is hidden, set myConstraint.constant -= myImageView.frame.height. Afterwards, call view.setNeedsLayout to update your constraints.
Make two constraints: one for below the image, and one for below the label ("constraintToImage" and "constraintToLabel"). Hook them both up to your controller like in option 1, and call view.addConstraint(constraintToImage) and view.removeConstraint(constraintToLabel) when the image becomes visible (and the opposite for when it's hidden). Again, call view.setNeedsLayout after.
I have this constraint for Any-Any size class and I have unselected it for Regular-Regular with the hope that this constraint would not apply to iPad. Then I run the app on iPad simulator and get unsatisfiable constraints error on this constraint. Am I missing something? Isn't what I did supposed to disable this constraints for iPad?
#IBOutlet weak var const1Out: NSLayoutConstraint!
var const1: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
const1 = NSLayoutConstraint(item:... // Defining the complete constraint
orientationDidChange()
}
func orientationDidChange()
{
// Just iPad
if traitCollection.verticalSizeClass == .Regular && traitCollection.horizontalSizeClass == .Regular {
// Conditioning on iPad Portrait or Landscape
if(UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation))
{
// activate to deactivate constraints either defined as vars or outlets
}
else
{
// activate to deactivate constraints either defined as vars or outlets
}
}
}
I do have a UIScrollView which holds a couple of UIViewController.
For the elements inside the UIViewController I used AutoLayout.
Now I'd like to change the constants of the view constraints if the current device is a iPhone 4.
So I override updateViewConstraints() of the UIViewControllers:
override func updateViewConstraints() {
if (self.view.frame.height == 480) {
self.imageMarginLeft.constant = 40
self.imageMarginRight.constant = 40
self.textMarginTop.constant = 10
println("updateViewConstraint \(self.imageMarginRight.constant)")
}
super.updateViewConstraints()
}
But even the println logs the correct new constant the update is not visible.
What do I have to change in my implementation?
As explained here I had to integrate self.view.layoutIfNeeded() before and after changing the view contstraint constant.