How can I set priority on constraints in Swift? - 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),
...
])

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
}

satisfying two auto layout constraints programmatically - Swift

I want to set a UIButton with autoLayout constraints:
Basically I want the height to be the multiplier of the container view height:
button.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.43).isActive = true
and if the height doesn't reach the constant of 44 I want to set it to 44, I added this:
button.heightAnchor(greaterThanOrEqualToConstant: 44).isActive = true
Obviously programmatically setting 2 constraints like this causes a conflict, is there a way for programmatically accomplishing these 2 prerequisites without causing a warning?
You will need to set lower priority for the "ratio" constraint
let constraint = button.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.43)
constraint.priority = .defaultHigh
constraint.isActive = true
if the engine still have troubles with figuring out the layout, you can try different priority value.
UILayoutPriority documentation
You either set a low priority of
let con = button.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.43)
con.layoutPriority //////////////
con.isActive = true
or calculate it yourself mathematically and decide with if statement

Unable to switch between programmable constraints

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.

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
}
}

Customize UIView as background

I would like to use UIView to create the dark gray curvy as a design.
However, I think I am doing it wrong when I try to set the cornerRadius and I know cornerRadius is only for the corner. Maybe I am not aware that there is actually another way to go around and would like to know if there an easy way to go around. The way I am doing this is, I created an UIView on StoryBoard and I create a Cocoa Touch Class that is Subclass of UIView and have configure the Custom Class to inherit the traits.
Any suggestion is really appreciated here.
That curve is not something you will be able to achieve using cornerRadius. There are two easy ways you can have what you want: A pre-rendered UIImage or a custom drawn CAShapeLayer.
Option 1: UIImage Background
You can create a background image in your favorite image editor, then set it as a centered background:
// create the UIImageView to display the image
let backgroundImageView = UIImageView()
backgroundImageView.image = UIImage(named: "background")
backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(backgroundImageView)
// create constraints to position the UIImageView and apply them
let horizontalConstraint = NSLayoutConstraint(item: backgroundImageView, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerX, multiplier: 1, constant: 0)
let verticalConstraint = NSLayoutConstraint(item: backgroundImageView, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 0)
view.addConstraints([horizontalConstraint, verticalConstraint])
Put that in the viewDidLoad() of the UIViewController you want to have a background.
You'll need to create your background image sized for the largest screen you're targeting and add it to your app's assets. You can experiment with setting constraints on the size to get it just the way you want it on every screen.
The advantage of this method is that if you want your background to be something more elaborate, you can just edit the image.
Option 2: CAShapeLayer Background
For your specific case (a grey curve) you can do it pretty easily with vectors:
// decide on size of ellipse and how to center it
let ellipseWidth:CGFloat = view.frame.width * 7
let ellipseHeight:CGFloat = view.frame.height * 3
let ellipseTop:CGFloat = 100
let ellipseLeft:CGFloat = -ellipseWidth / 2 + view.frame.width / 2
// create the ellipse path
let ellipseOrigin = CGPoint(x:ellipseLeft,y:ellipseTop)
let ellipseSize = CGSize(width: ellipseWidth, height: ellipseHeight)
let ellipsePath = CGPath(ellipseIn: CGRect(origin: ellipseOrigin, size: ellipseSize), transform: nil)
// create the shape layer
let shapeLayer = CAShapeLayer()
shapeLayer.frame = view.frame
shapeLayer.fillColor = UIColor.darkGray.cgColor
shapeLayer.path = ellipsePath
view.layer.addSublayer(shapeLayer)
You might have to experiment with the proportions of the ellipse to get it to look just right. 7x3 seems to look pretty close to your example.
If you want to get fancier you can add other shapes, strokes, gradients, etc., or you can use a UIBezierPath to get exactly the right curve.
Also note for this to work you'll need
import CoreGraphics
at the top of your file.