Creating a UIButton programmatically on iPhone X but safe area issue - swift

I created a UIButton in the middle of a TabBarController but it is not correctly displayed on the iPhone X because of its safe areas in the bottom of the phone.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Frame mic button
micButton.frame = CGRect.init(x: self.tabBar.center.x - 62, y: self.view.bounds.height - 94, width: 124, height: 124)
micButton.layer.cornerRadius = 62
}
What is the correct way for drawing this UIButton without touching the safe areas.

Safe zones, specifically a safeAreaLayoutGuide, relates to constraints.
There are various ways to code auto layout constraints (Visual Format Layout or VFL, explicit - and wordy - NSLayoutConstraints) but my preference is to use "layout anchors".
The basic idea for any layout is position and size. Give something a width/height, and x/y axis value and you've got it. Pretty much like frames.
So the basic "anchor" way of laying out this:
micButton.frame = CGRect.init(x: self.tabBar.center.x - 62, y: self.view.bounds.height - 94, width: 124, height: 124)
Would be this:
let micButton = UIButton()
micButton.translatesAutoresizingMaskIntoConstraints = false
micButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -94).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 124).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 124).isActive = true
micButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -62).isActive = true
Some notes:
Yes, you do not dictate a frame. In fact, you just initialize things when possible.
Since you aren't using IB, you need to set the auto resize mask flag to false. This is something even the most experienced developers sometimes forget. (Doing so usually means you have "unexpected" results ranging from not seeing something to seeing it misplaced.)
I've set the bottom or Y axis, the width and height, and finally the X axis.
While it's more code than using a frame, what you gain is consistency across screen sizes.
But you want more - you need to code for iPhone X safe zones. You have two tools Apple has given you: layoutMarginsGuide and safeAreaLayoutGuide. The former was introduced in iOS 9 (along with the more simple layoutGuide and layout anchors) while the latter was introduced in iOS 11.
[My assumption, probably a safe one, is that all iPhone X devices will run iOS 11 or later. Since a "safe area" is really only needed for this device, the following is what you need.]
Margins work with all devices for leading/trailing (or horizontal) edges. They also work for top/bottom (or vertical) edges too. But for iPhone X you need to be concerned with a different top/bottom, thus the "safe area".
let layoutGuideTop = UILayoutGuide()
let layoutGuideBottom = UILayoutGuide()
view.addLayoutGuide(layoutGuideTop)
view.addLayoutGuide(layoutGuideBottom)
let margins = view.layoutMarginsGuide
view.addLayoutGuide(margins)
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
layoutGuideTop.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0).isActive = true
layoutGuideBottom.bottomAnchor.constraintEqualToSystemSpacingBelow(guide.bottomAnchor, multiplier: 1.0).isActive = true
} else {
layoutGuideTop.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
layoutGuideBottom.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
}
The code snippet above will yield the correct top/bottom margin variables (layoutGuideTop and layoutGuideBottom) based on the iOS version. From there you can adjust your bottom anchor to:
micButton.bottomAnchor.constraint(equalTo: laytouGuideBottom, constant: -94).isActive = true
Which should set micButton to be 94 points above the bottom safe area.
Here's a few links that should help you with layout anchors and guides:
Layout Anchors
Layout Guides
Safe Area Layout Guides
EDIT:
One last note about constraints. Since you aren't relying on frame values, all of this code is best placed in viewDidLoad, because the layout engine will determine everything appropriately (and viewDidLayoutSubviews may be called more than once).

Try this :
Add in UITabViewController's class
micButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(micButton)
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
micButton.centerXAnchor.constraint(equalTo: guide.centerXAnchor).isActive = true
micButton.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 64).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 64).isActive = true
} else {
NSLayoutConstraint(item: micButton, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: micButton, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 64).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 64).isActive = true
}

Thanks to #dfd guidance this was the way i did it as a work around. I just checked if the phone using the app has the same screen size as iPhoneX and i just changed in the constraints.
override func viewDidLoad() {
super.viewDidLoad()
//Frame mic button
micButton.frame = CGRect.init(x: self.tabBar.center.x - 62, y: self.view.bounds.height - 94, width: 124, height: 124)
micButton.layer.cornerRadius = 62
micButton.translatesAutoresizingMaskIntoConstraints = false
micButton.setBackgroundImage(#imageLiteral(resourceName: "micIcon"), for: .normal)
//Add to tabbar view
self.view.insertSubview(micButton, aboveSubview: self.tabBar)
if UIDevice().userInterfaceIdiom == .phone {
if UIScreen.main.nativeBounds.height == 2436 {
//iPhoneX Device
micButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 124).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 124).isActive = true
micButton.centerXAnchor.constraint(equalTo: self.tabBar.centerXAnchor, constant: 0).isActive = true
} else {
//Not an iPhoneX Device
micButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 25).isActive = true
micButton.widthAnchor.constraint(equalToConstant: 124).isActive = true
micButton.heightAnchor.constraint(equalToConstant: 124).isActive = true
micButton.centerXAnchor.constraint(equalTo: self.tabBar.centerXAnchor, constant: 0).isActive = true
}
}
// Do any additional setup after loading the view.
}

This code help you to solve your problem.
micButton.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor).isActive = true

Related

UIKit: Relative autolayout constraints programmatically

I'm using swift + UIKit to autolayout a view programatically...
private func autoLayout(for child: UIView, in parent: UIView, width inset: CGFloat = 0) {
parent.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
child.topAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.topAnchor, constant: inset) .isActive = true
child.bottomAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.bottomAnchor, constant: -inset).isActive = true
child.leadingAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.leadingAnchor, constant: inset) .isActive = true
child.trailingAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.trailingAnchor, constant: -inset).isActive = true
}
In one instance I'm calling the method on a UILabel (with a set font size: 24), but I'd like to add a height constraint that adjusts the parent view to be 2.4x the child's height. Something like this inside the method...
if let label = child as? UILabel {
let value = child.frame.height * 2.4
parent.heightAnchor.constraint(equalToConstant: value).isActive = true
}
If I use the equalToConstant: parameter, then the size will be calculated when the method is called, which happens before the views are added to a UIStackView and their sizes are adjusted (essentially if I set the parent's constant equal to say child.frame.height * 2.4 it will calculate when the height is still zero).
How can I achieve this dynamically?
All you need to do is create a height constraint on the parent that is 2.4 times the height constraint of the child (label).
Here's an updated version of your code:
private func autoLayout(for child: UIView, in parent: UIView, width inset: CGFloat = 0) {
parent.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
child.leadingAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.leadingAnchor, constant: inset) .isActive = true
child.trailingAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.trailingAnchor, constant: -inset).isActive = true
if child is UILabel {
// This alternate syntax for creating a constraint allows you to set a multiplier
NSLayoutConstraint(item: parent, attribute: .height, relatedBy: .equal, toItem: child, attribute: .height, multiplier: 2.4, constant: 0).isActive = true
child.centerYAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.centerYAnchor).isActive = true
} else {
child.topAnchor .constraint(equalTo: parent.safeAreaLayoutGuide.topAnchor, constant: inset) .isActive = true
child.bottomAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.bottomAnchor, constant: -inset).isActive = true
}
}
I made an assumption that for the label you just want the label to be centered in the parent in addition to the parent's height being 2.4 times the child height. Adjust as needed.

Why is my second button behaving different when using auto layout constraints?

I can not figure out why my button is behaving different when using auto layout constraints programmatically.
When setting the view that holds my cancelBtn every thing is working fine:
let cancelBtnView = TriangularView(frame: CGRect(x: 0, y: 0, width: 300, height: 150))
let cancelBtn = UIButton()
cancelBtnView.addSubview(cancelBtn)
cancelBtn.titleLabel!.font = UIFont.fontAwesome(ofSize: 35, style: .regular)
cancelBtn.setTitle(String.fontAwesomeIcon(name: .times), for: .normal)
cancelBtn.frame = CGRect(x: cancelBtnView.bounds.width / 16, y: cancelBtnView.bounds.height / 2 ,width: 55, height: 55)
I get the following layout:
When setting up the view for my doneBtn I get a different output:
let doneBtnView = TriangularView()
doneBtnView.translatesAutoresizingMaskIntoConstraints = false
doneBtnView.widthAnchor.constraint(equalToConstant: 300).isActive = true
doneBtnView.heightAnchor.constraint(equalToConstant: 150).isActive = true
doneBtnView.trailingAnchor.constraint(equalTo: actionButtonView.trailingAnchor).isActive = true
doneBtnView.bottomAnchor.constraint(equalTo: actionButtonView.bottomAnchor).isActive = true
let doneBtn = UIButton()
doneBtnView.addSubview(doneBtn)
doneBtn.titleLabel!.font = UIFont.fontAwesome(ofSize: 35, style: .regular)
doneBtn.setTitle(String.fontAwesomeIcon(name: .check), for: .normal)
doneBtn.translatesAutoresizingMaskIntoConstraints = false
doneBtn.widthAnchor.constraint(equalToConstant: 55).isActive = true
doneBtn.heightAnchor.constraint(equalToConstant: 55).isActive = true
doneBtn.leadingAnchor.constraint(equalTo: doneBtnView.leadingAnchor, constant: doneBtnView.bounds.width / 16).isActive = true
doneBtn.bottomAnchor.constraint(equalTo: doneBtnView.bottomAnchor, constant: doneBtnView.bounds.height / 2).isActive = true
Setting the constraints for my doneBtn programmatically I get the following:
The constraints for the doneBtnView are set programmatically because I want to pin it to the bottom right of its superview.
Your problem is that you are looking at the bounds of the other view when setting your constraint for your doneBtn, but those bounds will not have been set yet because layout has not happened. In general, it is a bad idea to combine frame/bounds and constraints.
This can be done, but not with anchors. Try the following NSLayoutConstraints:
NSLayoutConstraint(item: doneBtn, attribute: .leading, relatedBy: .equal,
toItem: doneBtnView, attribute: .trailing, multiplier: 1/16, constant: 0).isActive = true
NSLayoutConstraint(item: doneBtn, attribute: .top, relatedBy: .equal,
toItem: doneBtnView, attribute: .bottom, multiplier: 1/2, constant: 0).isActive = true
and then set the width and height using anchors:
doneBtn.widthAnchor.constraint(equalToConstant: 55).isActive = true
doneBtn.heightAnchor.constraint(equalToConstant: 55).isActive = true
Note: Because we're using the .trailing and .bottom constraints to represent width and height, it might be necessary to put doneBtnView into a container view of the same size because the values will be in the coordinate system of the parent view. By making the parent view the exact same size, width will be equal to the trailing constraint, and height will be equal to the bottom constraint.

object not being constraint to bottom anchor of center y anchor

My swift code is trying to constraint a object to the bottom of the center y anchor. When the code is built and run on a iPad. That is not doing it. You can see the what is being displayed in the image below. Clearly the object is not touching the center of the screen. The red marks on the image is what I am looking for.
NSLayoutConstraint.activate([
box.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.20),
box.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
box.centerXAnchor.constraint(equalTo: view.centerXAnchor),
box.bottomAnchor.constraint(equalTo: view.centerYAnchor),
])
in order to use constraints programmatically remember to set translatesAutoresizingMaskIntoConstraints to false in your object
let imageAlert: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleToFill
iv.backgroundColor = .white
iv.image = UIImage(named: "soup")
iv.translatesAutoresizingMaskIntoConstraints = false //here
return iv
}()
another common mistake is not adding the components to View
func addComponents() {
self.addSubview(container)
container.addSubview(quit)
container.addSubview(imageAlert)
container.addSubview(alertLabel)
container.addSubview(alertButtonAccept)
container.addSubview(alertButtonCancel)
}
also if you're not using a custom View to insert your Object use self
NSLayoutConstraint.activate([
box.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.20),
box.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.2),
box.centerXAnchor.constraint(equalTo: self.centerXAnchor),
box.bottomAnchor.constraint(equalTo: self.centerYAnchor)
])

Label Not Resizing Within Container Despite Several Methods

I have a reusable view class that I call when I want to add a disappearing subView to another view. I have a UILabel extension to determine when there is to much text for the label's current size(this extension works), and within this closure I'm trying to expand the: contianerView(regView)'s height, the label's height, and the label's height anchor, since the label was created programatically. As you guessed, the label isn't expandng correctly.
I've tried setting the numberOfLines to 0; changing the label's frame; using .layoutSubviews; removing when the height anchor was originally set, so now it's only called when the resize view method is used.
Label extension:
extension UILabel {
var isTruncated: Bool {
guard let labelText = text else {
return false
}
let labelTextSize = (labelText as NSString).boundingRect(
with: CGSize(width: frame.size.width, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [.font: font],
context: nil).size
return labelTextSize.height > bounds.size.height
}
}
function to add reusable view(most of it is within the while loop towards the bottom):
func addDisapearingView(toview: UIView, text: String, textColor: UIColor, colorView: UIColor, alpha: CGFloat, height: CGFloat){
regView.backgroundColor = colorView
regView.alpha = alpha
toview.addSubview(regView)
regView.translatesAutoresizingMaskIntoConstraints = false
if #available(iOS 11.0, *) {
let guide = toview.safeAreaLayoutGuide
regView.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
regView.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
regView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
UIView.animate(withDuration: 5.0) {
self.regView.heightAnchor.constraint(equalToConstant: height).isActive = true
}
} else {
NSLayoutConstraint(item: regView,
attribute: .top,
relatedBy: .equal,
toItem: toview, attribute: .top,
multiplier: 1.0, constant: 0).isActive = true
NSLayoutConstraint(item: regView,
attribute: .leading,
relatedBy: .equal, toItem: toview,
attribute: .leading,
multiplier: 1.0,
constant: 0).isActive = true
NSLayoutConstraint(item: regView, attribute: .trailing,
relatedBy: .equal,
toItem: toview,
attribute: .trailing,
multiplier: 1.0,
constant: 0).isActive = true
regView.heightAnchor.constraint(equalToConstant: height).isActive = true
}
let label = UILabel(frame: CGRect(x: regView.frame.origin.x, y: regView.frame.origin.y, width: regView.bounds.width, height: regView.bounds.height))
label.numberOfLines = 0
label.adjustsFontSizeToFitWidth = true
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.translatesAutoresizingMaskIntoConstraints = false
label.center.x = newView.center.x
label.center.y = newView.center.y
label.textAlignment = .center
label.text = text
label.textColor = textColor
regView.addSubview(label)
if label.isTruncated {
print("LABEL IS TRUNCATED")
}
//test if there is more text than the label has room for
while label.isTruncated {
print("printing while truncating in the wHiLE loop")
regView.bounds.size.height += 5
label.bounds.size.height += 5
var currentLabelHeight = label.bounds.height
let amt = currentLabelHeight + 5
label.frame = CGRect(x: regView.frame.origin.x, y: regView.frame.origin.y, width: regView.bounds.width, height: CGFloat(amt))
var heighT : CGFloat = height
heighT += 5
regView.heightAnchor.constraint(equalToConstant: heighT).isActive = true
}
regView.layoutSubviews()
label.sizeToFit()
//remove
Timer.scheduledTimer(withTimeInterval: 2.8, repeats: false) { (action) in
UIView.animate(withDuration: 2.8, animations: {
self.regView.heightAnchor.constraint(equalToConstant: 0).isActive = true
label.heightAnchor.constraint(equalToConstant: 0).isActive = true
})
}
}
I've briefly done this before in storyboard where I had to expand a label within another view when the text was too long(this time it did work!), and the important part there was editing the height constraint, so I think this might have something to do with modifying the height constraint.
Any help would be greatly appreciated!
ANSWER:
I asked another question here: Programatically Created Label Within Container View Won't Expand For Text
it has the same code here and everything in the question but the answer works.
If i understand correct, you have your view and a label, and you want your view to dynamically change height depend on label content. I suggest you to break that task to chunks and resolve it step by step.
1 - You might want to add a test UIView object instead of label with fixed size. When u do this, you will see whether you parent view expand depending of test view size.
2 - If it is, you are up to create a label with height you need. All that you need to know its font, text and width. I think this link may help you. After you sure, that your label size is correct (you may want to print it out) you may add it as any other UIView object to your parent view.

Swift Programatic constraint view.bottomAnchor causing issue

I am trying to get better at all programatic constraints.
I created an interface representation of what I wanted to do to make sure everything turned blue and gave no errors when ran.
In interface builder I created a UIView, set the ratio to 1:1, top anchor 100, and bottom anchor 450, and horizontally constrained it.
Works great.
Now I deleted all of that and do it via code
I think this should be identical to the IB version...
let testView = UIView()
view.addSubview(testView)
testView.backgroundColor = .red
let testViewTopConstraint = testView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
let testViewBottomConstraint = testView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 450)
let testViewCenterXConstraint = testView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let testViewAspectConstraint = NSLayoutConstraint(item: testView,
attribute: .height,
relatedBy: .equal,
toItem: testView,
attribute: .width,
multiplier: (1.0 / 1.0),
constant: 0)
NSLayoutConstraint.activate([testViewTopConstraint, testViewBottomConstraint, testViewCenterXConstraint, testViewAspectConstraint])
testView.translatesAutoresizingMaskIntoConstraints = false
When I run it, the red square becomes a rectangle extending to the end (possible beyond).
I have played around with programatic constraints. If I were to add a leading constraint I am perfectly fine, it just seems to be my bottom constraint that always messes me up.
What is wrong with my bottom constraint?
Or am I doing something else wrong?
There are a few things but to answer your particular issue here you just have to add a - to the 450 constant
let testViewBottomConstraint = testView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -450)
You can also omit the NSLayoutConstraint by adding a few more anchors.