Unable to switch between programmable constraints - swift

I am trying to toggle between two programmable view constraints, depending on the orientation of the device.
To toggle I set the old height/width constraints to isActive=False in viewWillLayoutSubviews() and the new constraints to isActive=True in viewDiDLayoutSubViews(), however I get an
“unable to simultaneously satisfy constraints”
error in the debug, and it blocks the newConstraints.
Any advice on where to block the old constraints?
The IBOutlet for my View is set to ‘Strong’. I have also tried applying .layoutIfNeeded after changing the old constraint to isActive=False, but it still seems to be active.
Please see pertinent code below. The layout works well initially however after device rotation the new constraints in the updateViewLayouts method get blocked. I believe the new constraints are correct, I just need to disable the old constraints at the correct time.
override func viewWillLayoutSubviews() {
super.viewDidLayoutSubviews()
if view.orientationHasChanged(&isInPortrait) {
orientationWillChange()
}
if isInPortrait { //Disable Landscape constraints
imageView1.heightAnchor.constraint(equalTo: frameView.heightAnchor, multiplier: 1.0).isActive = false
imageView1.widthAnchor.constraint(equalTo: frameView.widthAnchor, multiplier: 0.5).isActive = false
} else { //Disable Portrait constraints
imageView1.heightAnchor.constraint(equalTo: frameView.heightAnchor, multiplier: 0.5).isActive = false
imageView1.widthAnchor.constraint(equalTo: frameView.widthAnchor, multiplier: 1.0).isActive = false
}
imageView1.layoutIfNeeded()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if orientationDidChange {
updateViewLayouts()
orientationDidChange = false
}
}
func updateViewLayouts() {
imageView1.topAnchor.constraint(equalTo: frameView.topAnchor).isActive = true
imageView1.leadingAnchor.constraint(equalTo: frameView.leadingAnchor).isActive = true
if isInPortrait {
imageView1.heightAnchor.constraint(equalTo: frameView.heightAnchor, multiplier: 0.5).isActive = true
imageView1.widthAnchor.constraint(equalTo: frameView.widthAnchor, multiplier: 1.0).isActive = true
} else { //LANDSCAPE
imageView1.heightAnchor.constraint(equalTo: frameView.heightAnchor, multiplier: 1.0).isActive = true
imageView1.widthAnchor.constraint(equalTo: frameView.widthAnchor, multiplier: 0.5).isActive = true
}
}

This is not how it works
imageView1.heightAnchor.constraint(equalTo: frameView.heightAnchor, multiplier: 1.0).isActive = false
Because every time you call .constraint(..., it creates new constraint and return it back to you. So setting it to false is none sense because it's will create and destroy at the same time.
For disabling that, you need to take a reference to the constraint, and deactivate that later:
//Persistant variable
var portraitHeightConstraint: NSLayoutConstraint?
// If portrait
portraitHeightConstraint = imageView1.heightAnchor.constraint(equalTo: frameView.heightAnchor, multiplier: 1.0)
portraitHeightConstraint?.isActive = true
// And later if needed
portraitHeightConstraint?.isActive = false
// And same logic for horizontal and other constraints
Please note that I write this without compiler and may have some syntax errors. Don't panic.

Related

Add constraints to a UIButton class without creating new view or knowing specific parent view

I have a bunch of buttons in my app and I would like to use the same class for all of them (UIButton). I need to be able to change the constraints for all of them at the same time in the class, some of them will be nested in different parent-views. All of them were created in Interface Builder nested in their own UIView in a few horizontal and vertical UIStackViews.
Is this possible?
I was hoping to do something like this, but I don't know how to go further:
class myButtonClass: UIButton {
required init(coder aDecoder:NSCoder) { super.init(coder: aDecoder)!}
override init(frame:CGRect) {super.init(frame: frame)
setConstraints()
}
func setConstraints() {
let topMarginConstraint = NSLayoutConstraint(item: .self, attribute: NSLayoutConstraint.Attribute.topMargin, relatedBy: NSLayoutConstraint.Relation, toItem: .superview, attribute: NSLayoutConstraint.Attribute.topMargin, multiplier: 1, constant: 2)
//Additional Constraints
self.addConstraints([topMarginConstraint])
}
}
This doesn't work for a variety of reasons, some of the errors are because I don't know how to assign the above to an undisclosed superview(parent-view)?
I managed to solve it myself by using .superview:
func setConstraints() {
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.8).isActive = true
self.heightAnchor.constraint(equalTo: superview!.heightAnchor, multiplier: 0.8).isActive = true
self.centerYAnchor.constraint(equalTo: superview!.centerYAnchor).isActive = true
self.centerXAnchor.constraint(equalTo: superview!.centerXAnchor).isActive = true
}

Updating constraints when device orientation change

I want to change a button height constraint according to device orientation. I am creating with height constraint. And then I am setting height constraint 60 for landscape mode, 40 for portrait mode. But when I change device orientation, height is not becoming bigger. Where is the problem. Here is my code
lazy var nextEpisodeButton: CustomPlayerButton = {
let nextEpisode = CustomPlayerButton(type: .nextEpisode, backgroundImage: nil)
nextEpisode.addTarget(self, action: #selector(nextEpisodeTapped), for: .touchUpInside)
nextEpisode.translatesAutoresizingMaskIntoConstraints = false
nextEpisode.adjustsImageWhenHighlighted = false
return nextEpisode
}()
func addNextEpisodeButton() {
view.addSubview(nextEpisodeButton)
NSLayoutConstraint.activate([
nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40),
nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120),
nextEpisodeButton.rightAnchor.constraint(equalTo: view.safeRightAnchor, constant: -20),
nextEpisodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -60)
])
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
nextEpisodeButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
} else {
nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
nextEpisodeButton.layoutIfNeeded()
}
You should have a reference to nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40) constraint somewhere in your ViewController and in willTransition callback just change its constant value. With your code you are creating and activating a new constraint every time you rotate rather than changing the existing one.
The constraints are persistent. When you are creating a second constraint for the same attribute (in your case the height of a view) and you are activating it, the 2 constraints are conflicting with each other.
So, you need to keep a reference for both of them and activate/deactivate them accordingly:
lazy var nextEpisodeButton: CustomPlayerButton = {
let nextEpisode = CustomPlayerButton(type: .nextEpisode, backgroundImage: nil)
nextEpisode.addTarget(self, action: #selector(nextEpisodeTapped), for: .touchUpInside)
nextEpisode.translatesAutoresizingMaskIntoConstraints = false
nextEpisode.adjustsImageWhenHighlighted = false
return nextEpisode
}()
private var buttonLandscapeHeightContraint: NSLayoutConstraint?
private var buttonPortraitHeightContraint: NSLayoutConstraint?
func addNextEpisodeButton() {
view.addSubview(nextEpisodeButton)
buttonLandscapeHeightContraint = nextEpisodeButton.heightAnchor.constraint(equalToConstant: 60)
buttonPortraitHeightContraint = nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40)
NSLayoutConstraint.activate([
nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120),
nextEpisodeButton.rightAnchor.constraint(equalTo: view.safeRightAnchor, constant: -20),
nextEpisodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -60)
])
updateButtonHeightConstraint()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: size, with: coordinator)
updateButtonHeightConstraint()
nextEpisodeButton.layoutIfNeeded()
}
private func updateButtonHeightConstraint() {
if UIDevice.current.orientation.isLandscape {
buttonPortraitHeightContraint?.isActive = false
buttonLandscapeHeightContraint?.isActive = true
} else {
buttonLandscapeHeightContraint?.isActive = false
buttonPortraitHeightContraint?.isActive = true
}
}
My way is to do two things:
Keep the constraints you wish to change in arrays to be activated and deactivated.
Detect device orientation through a means other than traits.
Let's start with the first. And you are off to a good start - the constraints are (a) in code and (b) using anchors. (If by chance you are using IB - Storyboards for others - you'll need to set the changing constraints as #IBOutlets.)
It looks like you are wanting this button to be in the right bottom, so let's make those constraints active:
nextEpisodeButton.rightAnchor.constraint(equalTo: view.safeRightAnchor, constant: -20).isActive = true
nextEpisodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -60).isActive = true
This will pin things properly no matter what the orientation.
Now, let's say you want to change size. You need to put these into two arrays:
var portraitLayout = [NSLayoutConstraint]()
var landscapeLayout = [NSLayoutConstraint]()
portraitLayout.append(nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120))
portraitLayout.append(nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40))
landscapeLayout.append(nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120))
landscapeLayout.append(nextEpisodeButton.heightAnchor.constraint(equalToConstant: 60))
This sets up a 40x120 button in portrait and a 60x120 button in landscape. This can (should?) be done in viewDidLoad. Now it's time to activate/deactivate....
Only one array should be active, and you'll need to do one at the time the view is initialized. I'll get to that, but first, let me show two lines of code that is necessary:
NSLayoutConstraint.deactivate(landscapeLayout)
NSLayoutConstraint.activate(portraitLayout)
You can try to add/delete constraints, but this is not only risky and not as easy to maintain, it's not even needed. Simply set all constraints the are constant as isActive = true, put the ones that change into arrays, and activate/deactive.
(If you want to animate such changes - I wouldn't for you - then do this and add UIView.animate(withDuration:) at the end.)
Now, the rough piece, detecting the orientation.
Apple decided to add trait collections a few years ago. They work well (and this year I finally get why they did it the way they did). But they have one serious issue - iPads in full screen mode always have a normal size. (I'm writing an iPad only app this year and in split screen mode it may be compact.)
Your question stressed orientation, so I'd recommend not use trait collection changes. Instead, use viewWillLayoutSubviews. For me, this seems to be more reliable - it's the earliest in the view controller lifecycle that I've found. You'll need to do two things... set the initial orientation and detect changes.
Here's my setup. In a UIView extension:
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)
}
}
p and l are portrait and landscape respectively. All I do is simply check the bounds and active/deactive appropriately.
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)
}
}
}
This is likely overkill for your needs. I'm basically tracking two things (initial and current orientation) for changes and calling things when needed - viewWillLayoutSubviews can be called more than once during a load(?) and for other reasons than an orientation change.
Conclusion:
You are close, but you have a few changes. First, set your constant constraints to isActive = true and activate/deactivate the remaining ones as arrays. Second, unless your app is iPhone only (and even then it will still be available for iPad) do not use trait collections, but instead use the view controller lifecycle and the screen bounds.

Why is my UIView not displaying correctly when updating autolayout constraints?

I am trying to animate a simple autolayout constraint change, but when I call it the first time it animates but instead of just stretching and changing the height, it moves the whole view up, if I call it again it then fixes itself.
Here is how I set up the constraints:
hiddenView.addSubview(topView)
topView.translatesAutoresizingMaskIntoConstraints = false
topView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
topView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
topView.bottomAnchor.constraint(equalTo: hiddenView.bottomAnchor).isActive = true
topViewDefaultTopAnchorConstraints.append(topView.topAnchor.constraint(equalTo: hiddenView.centerYAnchor))
topViewSelectedTopAnchorConstraints.append(topView.topAnchor.constraint(equalTo: hiddenView.topAnchor))
NSLayoutConstraint.activate(topViewDefaultTopAnchorConstraints)
And here is how I am updating them:
func showTopView() {
NSLayoutConstraint.deactivate(topViewDefaultTopAnchorConstraints)
NSLayoutConstraint.activate(topViewSelectedTopAnchorConstraints)
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
Update:
Here is a gif of what happens when calling showTopView, calling it again fixes the bottom constraint:
It should just animate up like in the second image, not bringing the whole view up, as the bottomAnchor does not change, how can I fix this?
Update: I realised that I am rounding the corners of topView and bottomView, if I don't round the top corners then it works correctly, so it has something to do with this.
override func viewDidLayoutSubviews() {
topView.roundCorners(corners: [.topLeft, .topRight], radius: 100*ScreenDimensions.ASPECT_RATIO_RESPECT_OF_XMAX)
bottomView.roundCorners(corners: [.topLeft, .topRight], radius: 100*ScreenDimensions.ASPECT_RATIO_RESPECT_OF_XMAX)
topConstraint = topView.topAnchor.constraint(equalTo: hiddenView.topAnchor, constant: hiddenView.frame.height/2)
topConstraint.isActive = true
}
Seems the hiddenView's bottomAnchor is pulled up along with the topView's bottom here. Make sure the hiddenView's bottomAnchor is constrained properly. And better way to do this would be to provide a heightAnchor and increase the heightConstraint constant value to animate the view to increased height.
I think I know what your issue is. You might need to play with the constant rather than having two different constraint defined that you activate/deactivate.
First, define a constraint a the top of your ViewController like so:
var topConstraint: NSLayoutConstraint!
Then, initialize it as shown below:
hiddenView.addSubview(topView)
topView.translatesAutoresizingMaskIntoConstraints = false
topView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
topView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
topView.bottomAnchor.constraint(equalTo: hiddenView.bottomAnchor).isActive = true
topConstraint = topView.topAnchor.constraint(equalTo: hiddenView.topAnchor, constant: hiddenView.frame.height/2)
topConstraint.isActive = true
Then inside showTopView, you only need to update the constant and that should create the animation you are looking for:
func showTopView() {
topConstraint.isActive = false
topConstraint.constant = 0
topConstraint.isActive = true
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
Similarly if you want to put the view back to normal, you'd implement it as follows:
func hideTopView() {
topConstraint.isActive = false
topConstraint.constant = hiddenView.frame.height/2
topConstraint.isActive = true
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
You may have better luck with this approach:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let r = 100*ScreenDimensions.ASPECT_RATIO_RESPECT_OF_XMAX
topView.layer.cornerRadius = r
topView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
bottomView.layer.cornerRadius = r
bottomView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}

How can I set priority on constraints in Swift?

I've been struggling to make priority on constraints work programmatically in Swift.
My goal is to have the meetingFormView no more than 300 wide. Using IB I would give the width constraint a lower priority and give a higher priority to the "lessThanOrEqualToConstant". But I can't get it to work.
I've tried this:
meetingFormView.translatesAutoresizingMaskIntoConstraints = false
let constraintWidth = NSLayoutConstraint(
item: meetingFormView,
attribute: NSLayoutAttribute.width,
relatedBy: NSLayoutRelation.equal,
toItem: startView,
attribute: NSLayoutAttribute.width,
multiplier: 1,
constant: 0)
constraintWidth.priority = .defaultHigh
NSLayoutConstraint.activate([
meetingFormView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
meetingFormView.heightAnchor.constraint(equalToConstant: 170),
meetingFormView.widthAnchor.constraint(lessThanOrEqualToConstant: 300),
meetingFormView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
constraintWidth
])
It actually seems to take three lines of code to set up a prioritized "anchor-based" constraint in code:
let widthConstraint = meetingFormView.widthAnchor.constraint(equalToConstant: 170)
widthConstraint.priority = UILayoutPriority(rawValue: 500)
widthConstraint.isActive = true
It seems like if I try to set isActive with the let declare Xcode (maybe Swift?) doesn't recognize that the type is NSLayoutConstraint. And using UILayoutPriority(rawValue:) seems to be the best (only?) way to set priorities.
While this answer doesn't conform exactly with what you are doing, I believe it will work with IB. Just replace the let with creating an IBOutlet and there should be no need for the isActive line.
Obviously, to change the priority later in code you just need:
widthConstraint.priority = UILayoutPriority(rawValue: 750)
You can write a small extension like this:
extension NSLayoutConstraint
{
func withPriority(_ priority: Float) -> NSLayoutConstraint
{
self.priority = UILayoutPriority(priority)
return self
}
}
then use it like this:
NSLayoutConstraint.activate([
...
meetingFormView.widthAnchor.constraint(lessThanOrEqualToConstant: 300).withPriority(500),
...
])

Can't add constraints to video programmatically

I am trying to programmatically constraint a video into the center of the page. My AV controller is called avPlayerController .
I have already given its x and y values along with the width and height:
avPlayerController.view.frame = CGRect(x: 36 , y: 20, width: 343, height: 264)
So how do i center it?
I HAVE TRIED: Programmatically Add CenterX/CenterY Constraints
But, as you can guess it did not work :(
Here is my code:
super.viewDidLoad()
let filepath: String? = Bundle.main.path(forResource: "rockline", ofType: "mp4")
let fileURL = URL.init(fileURLWithPath: filepath!)
avPlayer = AVPlayer(url: fileURL)
let avPlayerController = AVPlayerViewController()
avPlayerController.player = avPlayer
avPlayerController.view.frame = CGRect(x: 36 , y: 20, width: 343, height: 264)
// hide/show control
avPlayerController.showsPlaybackControls = false
// play video
avPlayerController.player?.play()
self.view.addSubview(avPlayerController.view)
avPlayerController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
Here's the code, with explanation.
Always remember that if you are using auto layout constraints, do not set frames. The layout engine will walk all over them. If you are instantiating your view in code, don't set a frame, or if necessary, it communicates things best if you set the frame to CGRect.zero.
Understand the view life cycle. Specifically, you can set your constraints in viewDidLoad, where they should be created only once.
Remember to set the auto resizing mask to false. This is the most common error when you learning auto layout in code.
There are actually three ways to create constraints, and a few ways to activate them. In your question, I think the easiest way is to use anchors.
Here's an example of centering a view (any view) with a width of 343 and a height of 264:
let myView = UIView() // note, I'm not setting any frame
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
myView.translatesAutoresizingMaskIntoConstraints = false
myView.widthAnchor.constraint(equalToConstant: 343.0).isActive = true
myView.heightAnchor.constraint(equalToConstant: 264.0).isActive = true
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
That's all there is to it! BUT....
I'd suggest one more thing. Don't use constants in setting the height and width. That's not being "adaptive". Your 4 inch iPhone SE has a screen size of 568x320, where this may look centered and large enough. But on an iPhone Plus with a screen size of 736x414 it may be pretty small. (To say nothing of a 12.9 inch iPad Pro!)
Notice how my code uses the superview for the centerX/centerY anchors. (And instead of equalToConstant it's equalTo.) Do the same with the width and height. Through the use of multiplier and constant, along with UILayoutGuides, you can make your layouts adapt to whatever screen size Apple throws at you.
You can try this.
avPlayerController.view.enableAutoLayoutConstraint()
avPlayerController.view.setCenterXConstraint(.equal, constantValue: 0)
avPlayerController.view.setCenterYConstraint(.equal, constantValue: 0)
extension UIView
{
//Method to making translatesAutoresizingMaskIntoConstraints false.
func enableAutoLayoutConstraint()
{
self.translatesAutoresizingMaskIntoConstraints = false
}
//Method to set Center-X Consttraint
func setCenterXConstraint(_ relationType:NSLayoutRelation , constantValue:CGFloat)->NSLayoutConstraint
{
let constraint = NSLayoutConstraint(item: self, attribute:.centerX, relatedBy: relationType, toItem: self.superview, attribute: .centerX, multiplier: 1, constant: constantValue)
self.superview?.addConstraint(constraint)
return constraint
}
//Method to set Center-Y Consttraint
func setCenterYConstraint(_ relationType:NSLayoutRelation , constantValue:CGFloat)->NSLayoutConstraint
{
let constraint = NSLayoutConstraint(item: self, attribute:.centerY, relatedBy: relationType, toItem: self.superview, attribute: .centerY, multiplier: 1, constant: constantValue)
self.superview?.addConstraint(constraint)
return constraint
}
}