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.
Related
I have simple ViewController which displays images using UIScrollView which has constraints attaching it to the (top, leading, trailing) of the superView, and a UIPageControl, it works fine on iPhoneX simulator
When I run it on iPad Pro 9.7" simulator, the output is
After changing the View as attribute in the storyboard from iPhoneX to iPadPro 9.7" it worked well
This is the logic I use to calculate scrollviewContentSize & slidesSize
override internal func viewDidLoad() {
super.viewDidLoad()
tutorialScrollView.delegate = self
viewModel = TutorialViewModel()
configurePages()
setupSlideScrollView(slides: slides)
configurePageControl()
}
private func configurePages() {
if let viewModel = viewModel {
createSlides(tutotialPages: viewModel.getTutorialPages())
}
}
private func createSlides(tutotialPages: [TutorialPage]) {
for page in tutotialPages {
if let slide = Bundle.main.loadNibNamed(BUNDLE_ID, owner: self, options: nil)?.first as? TutorialSlideView {
slide.configure(title: page.title, detail: page.details, image: page.image)
slides.append(slide)
}
}
}
private func setupSlideScrollView(slides: [TutorialSlideView]) {
tutorialScrollView.contentSize = CGSize(width: view.frame.width * (CGFloat(slides.count)), height: tutorialScrollView.frame.height)
tutorialScrollView.isPagingEnabled = true
for i in 0 ..< slides.count {
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: tutorialScrollView.frame.height)
tutorialScrollView.addSubview(slides[i])
}
}
Can anyone find the problem?
Did you try to print view's frame in setupSlideScrollView method to ensure it is correct? There is no guarantee that it will be correct in the viewDidLoad method if you use AutoLayout. Sometimes it will be, sometimes not. I assume in this particular case, it happened to be correct on iPhone X, but incorrect on iPad.
If that's the problem, you should set contentSize and slides' frames in viewDidLayoutSubviews. Adding slides as subviews should stay in viewDidLoad/setupSlideScrollView because viewDidLayoutSubviews usually gets called multiple times.
I've got the following structure for example:
I want to rotate my label by 270degrees to achieve this:
via CGAffineTransform.rotated next way:
credentialsView.text = "Developed in EVNE Developers"
credentialsView.transform = credentialsView.transform.rotated(by: CGFloat(Double.pi / 2 * 3))
but instead of expected result i've got the following:
So, what is the correct way to rotate view without changing it's bounds to square or whatever it does, and keep leading 16px from edge of screen ?
I tried a lot of ways, including extending of UILabel to see rotation directly in storyboard, putted dat view in stackview with leading and it also doesn't helps, and etc.
Here is the solution which will rotate your label in an appropriate way forth and back to vertical-horizontal state. Before running the code, set constraints for your label in storyboard: leading to 16 and vertically centered.
Now check it out:
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
// Your leading constraint from storyboard, initially set to 16
#IBOutlet weak var leadingConstraint: NSLayoutConstraint!
var isHorizontal: Bool = true
var defaultLeftInset: CGFloat = 16.0
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
label.text = "This is my label"
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapAction)))
}
#objc func tapAction() {
if self.isHorizontal {
// Here goes some magic
// constraints do not depend on transform matrix,
// so we have to adjust a leading one to fit our requirements
leadingConstraint.constant = defaultLeftInset - label.frame.width/2 + label.frame.height/2
self.label.transform = CGAffineTransform(rotationAngle: .pi/2*3)
}
else {
leadingConstraint.constant = defaultLeftInset
self.label.transform = .identity
}
self.isHorizontal = !self.isHorizontal
}
}
I have an question about how can I fix the issue with the constraints in the landscape mode with view?
I set the constraint height for the view which I created in the bottom of the view controller = 80
but when the device enter in the landscape mode I need to set the height constraint for this view to 40 , but in that case I have an issue with the constraint the issue is (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.
(
"<NSLayoutConstraint:0x7d175100 UIView:0x7d5eeef0.height == 40 (active)>",
"<NSLayoutConstraint:0x7d5f8ef0 UIView:0x7d5eeef0.height == 80 (active)>"
)
)
the following is my code :
var toolBarView:UIView = {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 80))
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor(red: 23 / 255.0, green: 120 / 255.0, blue: 104 / 255.0, alpha: 1.0)
return view
}()
func setupToolBarViewInPortrait() {
toolBarView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
toolBarView.rightAnchor.constraint(equalTo:view.rightAnchor).isActive = true
toolBarView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
toolBarView.heightAnchor.constraint(equalToConstant: 80).isActive = true
}
func setupToolBarViewInLandScape() {
toolBarView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
toolBarView.rightAnchor.constraint(equalTo:view.rightAnchor).isActive = true
toolBarView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
toolBarView.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
override func viewWillLayoutSubviews() {
let oriantation = UIDevice.current.orientation
if oriantation.isLandscape {
setupNavigationBarWithObjectsInLandScape()
setupToolBarViewInLandScape()
}else if oriantation.isPortrait {
setupToolBarViewInPortrait()
}
}
As i am always late in community so posting answer is late.
I have handled your case like this way. you can try.
At first my View controller is just like this.
And for iphone 7plus height constraint is 128 for portrait and in landscape i want 55 for height constraint.
What i did.
Step 1:
pull the height constraint outlet so that i can change it programmatically.
Step 2:
I defined some predefined value for calculation.
var SCREEN_HEIGHT = UIScreen.main.bounds.size.height
var BASE_SCREEN_HEIGHT:CGFloat = 736.0 //My base layout is iphone 7 plus and 128(redViewheight) according to this.
var HEIGHT_RATIO = SCREEN_HEIGHT/BASE_SCREEN_HEIGHT
Step 3:
made a function and called it from viewDidLoad for initial layout constant.
func updateConstraint() {
self.redViewHeight.constant = 128 * HEIGHT_RATIO
}
Step 4:
As this method is called whenever rotation change so i called this function.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { context in
//This function will do the trick
let orientation = UIDevice.current.orientation
if orientation == .portrait {
self.redViewHeight.constant = 128 * HEIGHT_RATIO //Just a value
}
else {
self.redViewHeight.constant = 55 * HEIGHT_RATIO //Just a value
}
self.view.layoutIfNeeded()
}, completion: {
_ in
})
}
Hope you will find this useful.
You have both height constraints turned on at the same time, and they conflict, hence the issue. When you make the landscape constraints active you need to make the portrait ones inactive, and vice versa.
A solution might be to set all the constraints up at once in a view lifecycle method (viewDidLoad or viewWillAppear), store them in properties or instance variables in your class or struct, and then just turn them on or off when the orientation changes.
Alternatively, if you are sure that the toolBarView has no constraints other than the ones changed in this code, remove all constraints from the view before calling your setup methods (less elegant, and probably not as performant).
In portrait mode I have four views on top of one another from the top to the bottom of the view controller (see image).
I then want to change the position of the views relative to one another when the device transitions to landscape (see image two).
I want view 4 to move alongside views 2 and 3 and them all the sit below view 1.
Some of the layout conditions:
view 1 is attached to the top of the view controller in both landscape and portrait.
view 4 is attached to the left, right and bottom margins of the view controller in portrait mode.
Views 2, 3 & 4 are centered horizontally within the portrait view.
What is the best method to achieve the different layouts?
Would the most elegant solution be to make a reference to the constraints in the view controllers code and activate and deactivate them in viewWillTransition? Or is there a way to use vary for traits to achieve this (I can imagine views 2, 3 & 4 being centered horizontally would make this hard to achieve, as well as adding the new constraints for view 4 in landscape mode)?
We used to set up different sets of constraints and activate/deactivate them based upon orientation change. But nowadays can use size classes and "vary for traits".
For example, I start with a simple view and choose a compact width size class and then choose "Vary for traits":
I then add the appropriate constraints and click on "Done varying":
I then choose a "regular width size class" and repeat the process ("Vary for traits", add the constraints, click on "Done varying":
You then end up with a scene that will have a completely different set of constraints active for compact width size classes and regular width size classes. I.e. when I run the app, if the device rotates, the new constraints are activated:
For more information, see WWDC 2016 videos on adaptive layouts:
https://developer.apple.com/videos/play/wwdc2016/222
https://developer.apple.com/videos/play/wwdc2016/233
I use arrays of constraints and activate/deactivate according to orientation.
var p = [NSLayoutConstraint]()
var l = [NSLayoutConstraint]()
var initialOrientation = true
var isInPortrait = false
override func viewDidLoad() {
super.viewDidLoad()
// add any subviews here
view.turnOffAutoResizing()
// add constraints here....
// common constraints you can set their isActive = true
// otherwise, add in to portrait(p) and landscape(l) arrays
}
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)
}
}
}
extension UIView {
public func turnOffAutoResizing() {
self.translatesAutoresizingMaskIntoConstraints = false
for view in self.subviews as [UIView] {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
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)
}
}
}
My needs to distinguish portrait or landscape require using something other than size classes, as my app is universal and iPad (unless using split or slide out view) is always normal size. Also, you may get use viewWillTransistion(toSize:) or viewDidLoadSubviews() instead of 'viewWillLoadSubviews()` - but always test, as these may be executed more than once on an orientation change!
I noticed that when I update my autolayout constraints programmatically, all changes are reverted when I rotate the screen.
Reproduce the issue:
Basic Storyboard interface with UIView and 2 constraints:
width equal superview.width (multiplier 1) active
width equal superview.width (multiplier 1/2) disabled
Create and link these 2 constraints with IBOutlet
Programmatically disable the first constraint and enable the second one.
Rotate the device, the first constraint is active and the second one disabled.
Seems like a bug to me.
What do you think ?
Screenshots:
Storyboard:
Constraint #1:
Constraint #2:
Size Classes
Installed refers to Size Classes installation, not to active/inactive.
You must create another constraint programmatically, and activate/deactivate that one. This is because you cannot change the multiplier of a constraint (Can i change multiplier property for NSLayoutConstraint?), nor can you tinker with Size Classes (activateConstraints: and deactivateConstraints: not persisting after rotation for constraints created in IB).
There are a few ways to do so. In the example below, I create a copy of your x1 constraint, with a multiplier or 1/2. I then toggle between the two:
#IBOutlet var fullWidthConstraint: NSLayoutConstraint!
var halfWidthConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
halfWidthConstraint = NSLayoutConstraint(item: fullWidthConstraint.firstItem,
attribute: fullWidthConstraint.firstAttribute,
relatedBy: fullWidthConstraint.relation,
toItem: fullWidthConstraint.secondItem,
attribute: fullWidthConstraint.secondAttribute,
multiplier: 0.5,
constant: fullWidthConstraint.constant)
halfWidthConstraint.priority = fullWidthConstraint.priority
}
#IBAction func changeConstraintAction(sender: UISwitch) {
if sender.on {
NSLayoutConstraint.deactivateConstraints([fullWidthConstraint])
NSLayoutConstraint.activateConstraints([halfWidthConstraint])
} else {
NSLayoutConstraint.deactivateConstraints([halfWidthConstraint])
NSLayoutConstraint.activateConstraints([fullWidthConstraint])
}
}
Tested on iOS 9+, Xcode 7+.
I will describe how to simple switch between the two layouts.
When the screen rotates, Autolayout apply the installed default layout with the higher priority.
It does not matter whether Constraint is Active or not. Because, when rotating, the high priority layout on the storyboard is reinstalled and active = true.
Therefore, even if you change active, the default layout is applied when you rotate and you can not keep any layout.
Instead of switching active state, switching two layouts.
When switching between two layouts, use "priority" rather than "active".
This way does not need to worry about the state of active.
It's very simple.
First, create two layouts to switch, on Storyboard. Check both for Installed.
A conflict error occurs because two layouts have priority = 1000 (required).
Set the priority of the layout to be displayed first to High. And the priority of the other layout is set to Low, conflict error will be resolved.
Associate constraints of those layouts as IBOutlet of class.
Finally, just switch the priority between high and low at the timing you want to change the layout.
Note, please do not change priority to "required". Layout with priority set to required can not be changed it after that.
class RootViewController: UIViewController {
#IBOutlet var widthEqualToSuperView: NSLayoutConstraint!
#IBOutlet var halfWidthOfSuperview: NSLayoutConstraint!
override func viewDidLayoutSubviews() {
changeWidth()
}
func changeWidth() {
let orientation = UIApplication.shared.statusBarOrientation
if (orientation == .portrait || orientation == .portraitUpsideDown) {
widthEqualToSuperView.priority = UILayoutPriorityDefaultHigh;
halfWidthOfSuperview.priority = UILayoutPriorityDefaultLow;
}
else {
widthEqualToSuperView.priority = UILayoutPriorityDefaultLow;
halfWidthOfSuperview.priority = UILayoutPriorityDefaultHigh;
}
}
}
portrait with full width
landscape with half width
You can do the necessary switch with the constraints created via IB for size classes. The trick is to keep the collapsed state in a variable and update constraints as on your button's event as also on trait collection change event.
var collapsed: Bool {
didSet {
view.setNeedsUpdateConstraints()
}
}
#IBAction func onButtonClick(sender: UISwitch) {
view.layoutIfNeeded()
collapsed = !collapsed
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
view.setNeedsUpdateConstraints()
}
override func updateViewConstraints() {
constraint1.isActive = !collapsed
constraint2.isActive = collapsed
super.updateViewConstraints()
}