How can I add the safe area to the view height? - ios

I am creating a top "banner" and I want it to cover the safe area and some more.
I can't seem to find how to cover the Safe area.
The first picture is the desired effect, the second is the effect on notched iPhones.
Desired Effect
What's Happening
How can I add the safe area to the desired value of height?
Code:
let shadowView = UIView( frame: CGRect( x: -10, y: -20, width: (view.frame.width+20), height: 90 ) )
view.addSubview( shadowView )
shadowView.backgroundColor = .clear
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.3
shadowView.layer.shadowOffset = CGSize(width: 5, height: 3)
shadowView.layer.shadowRadius = 4.0
let titleView = UIView( frame: CGRect( x: 0, y: 0, width: ( shadowView.frame.width ), height: 90 ) )
shadowView.addSubview( titleView )
titleView.backgroundColor = .systemGreen
titleView.layer.cornerRadius = 45
titleView.layer.masksToBounds = true

The problem is here:
let shadowView = UIView( frame: CGRect( x: -10, y: -20, width: (view.frame.width+20), height: 90 ) )
You are hardcoding the frame - don't do this! Well, a height of 90 is fine. But the x: -10, y: -20, width: (view.frame.width+20) is terrible. Not all phones are the same size.
Technically, you could calculate the safe area inset height as NoeOnJupiter commented, but this is still pretty bad. What happens when the user rotates their device, and the notch moves? It sounds like a lot of work.
What you want is Auto Layout and the Safe Area. With Auto Layout, simply define some constraints, then watch your UIViews look great on all screen sizes. Then, the Safe Area defines what parts of the screen are "safe," meaning "not covered by notches or rounded screen corners."
So, you can pin shadowView to the edges of the screen (beyond the notch/safe area), and add the .systemGreen background color. Then, make titleView 90 points high, pinning it vertically to shadowView. Just note how titleView.topAnchor is pinned to shadowView.safeAreaLayoutGuide.topAnchor — this makes it stay clear of the notch.
let shadowView = UIView() /// green background with shadow and corner radius
shadowView.translatesAutoresizingMaskIntoConstraints = false
shadowView.backgroundColor = .systemGreen /// put the background color here instead of on `titleView`, because this view stretches beyond the notch
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.3
shadowView.layer.shadowOffset = CGSize(width: 5, height: 3)
shadowView.layer.shadowRadius = 4.0
shadowView.layer.cornerRadius = 45
shadowView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] /// only round the bottom corners
view.addSubview(shadowView)
let titleView = UIView() /// container for title label.
titleView.translatesAutoresizingMaskIntoConstraints = false
shadowView.addSubview(titleView)
let titleLabel = UILabel() /// the title label itself
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleView.addSubview(titleLabel)
titleLabel.text = "Catalogues"
titleLabel.font = UIFont.systemFont(ofSize: 36, weight: .medium)
NSLayoutConstraint.activate([
/// constrain `shadowView`
shadowView.topAnchor.constraint(equalTo: view.topAnchor),
shadowView.rightAnchor.constraint(equalTo: view.rightAnchor),
shadowView.leftAnchor.constraint(equalTo: view.leftAnchor),
/// constrain `titleView`
titleView.topAnchor.constraint(equalTo: shadowView.safeAreaLayoutGuide.topAnchor), /// most important part!
titleView.heightAnchor.constraint(equalToConstant: 90), /// will also stretch `shadowView` vertically
titleView.rightAnchor.constraint(equalTo: shadowView.rightAnchor),
titleView.leftAnchor.constraint(equalTo: shadowView.leftAnchor),
titleView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor),
/// constrain `titleLabel`
titleLabel.centerXAnchor.constraint(equalTo: titleView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: titleView.centerYAnchor)
])
Result:
iPhone 8
iPhone 12
For further reading, I have a blog post on this topic.

Related

Auto-sizing a UILabel without setting an explicit height

How do I get a multi-line label to size itself? I don't want to set an explicit height for it but I do need to place it in view.
The way my app is built, we explicitly set frames and origins rather than using NSLayoutConstraints. It's a mature app so this isn't up for discussion.
I'd like to be able to give my UILabel an origin and a width and let it figure its own height out.
How can I do this? This is my playground code:
let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 180))
view.backgroundColor = .white
let l = UILabel()
l.text = "this is a really long label that should wrap around and stuff. it should maybe wrap 2 or three times i dunno"
l.textColor = .black
l.lineBreakMode = .byWordWrapping
l.numberOfLines = 0
l.textAlignment = .center
l.sizeToFit()
let margin: CGFloat = 60
view
view.addSubview(l)
l.frame = CGRect(x: margin, y: 0, width: view.bounds.width - (margin * 2), height: 100)
// I don't want to do this ^^
This may do what you want...
As requested, you want to set the .origin and .width of a UILabel and have it set its own .height based on the text.
class ZackLabel: UILabel {
override public func layoutSubviews() {
super.layoutSubviews()
let h = sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
self.frame.size.height = h.height
}
}
class ViewController: UIViewController {
var testLabel: ZackLabel!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// instantiate a 300 x 180 UIView at 20, 80
let myView = UIView(frame: CGRect(x: 20, y: 80, width: 300, height: 180))
myView.backgroundColor = .white
// instantiate a ZackLabel
testLabel = ZackLabel()
testLabel.text = "this is a really long label that should wrap around and stuff. it should maybe wrap 2 or three times i dunno"
testLabel.textColor = .black
testLabel.lineBreakMode = .byWordWrapping
testLabel.numberOfLines = 0
testLabel.textAlignment = .center
// set background color so we can see its frame
testLabel.backgroundColor = .cyan
let margin: CGFloat = 60
// set label's origin
testLabel.frame.origin = CGPoint(x: margin, y: 0)
// set label's width (label will set its own height)
testLabel.frame.size.width = myView.bounds.width - margin * 2
// add the view
view.addSubview(myView)
// add the label to the view
myView.addSubview(testLabel)
// add a tap recognizer so we can change the label's text at run-time
let rec = UITapGestureRecognizer(target: self, action: #selector(tapFunc(_:)))
view.addGestureRecognizer(rec)
}
#objc func tapFunc(_ sender: UITapGestureRecognizer) -> Void {
testLabel.text = "This is dynamic text being set."
}
}
Result (on an iPhone 8):
and, after tapping on the (yellow) view, dynamically changing the text:
label.sizeThatFits(CGSize(width: <your required width>, height: CGFloat.greatestFiniteMagnitude))
This returns the labels needed size, growing infinitely in height, but fitted to your required width. I've occasionally noticed minor inaccuracies with this function (rounding error?), so I tend to bump the width and height by 1 just to be safe.
UILabel comes with an intrinsic size that should be calculated based on the text and the label's .font property. You may need to add a margin to it...
var height = l.intrinsicContentSize.height
height += margin
l.frame = CGRect(x: margin, y: 0, width: view.bounds.width - (margin * 2), height: height)
Failing that, maybe you can try something like:
let size = CGSize(width: view.bounds.width - (margin * 2), height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
var estimatedFrame = CGRect()
if let font = l.font {
estimatedFrame = NSString(string: l.text).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: font], context: nil)
}
//if you need a margin:
estimatedFrame.height += margin
l.frame = estimatedFrame
Give your UILabel as a UIScrollview or UITableView cell subview.
Then you setup UILabel leading, tralling, top, bottom constrain.
If you give UITableview then set table view hight auto dynamic. If you give UIScrollview
just set UILabel bottom constrain priority low

Swift iOS UITabBar Customization

I have a tab bar. I changed its colour, gave it a corner radius, set its border width and colour, and everything is working good. Until now, everything is done in a storyboard.
Now I want to give it left and right margins, as by default it is sticking to the screen's edges.
This is the tab bar's current look:
The black arrows point to the lines that stick to the screen's edges. I want space between this line and the edges.
You can create this placeholder view inside a custom UITabBar as below,
class CustomTabBar: UITabBar {
let roundedView = UIView(frame: .zero)
override func awakeFromNib() {
super.awakeFromNib()
roundedView.layer.masksToBounds = true
roundedView.layer.cornerRadius = 12.0
roundedView.layer.borderWidth = 2.0
roundedView.isUserInteractionEnabled = false
roundedView.layer.borderColor = UIColor.black.cgColor
self.addSubview(roundedView)
}
override func layoutSubviews() {
super.layoutSubviews()
let margin: CGFloat = 12.0
let position = CGPoint(x: margin, y: 0)
let size = CGSize(width: self.frame.width - margin * 2, height: self.frame.height)
roundedView.frame = CGRect(origin: position, size: size)
}
}
Set this class for TabBar in storyboard/xib. It should give you the following,

Programmatically adding UILabel is not visible

I'm trying to practice here, I have a SearchBar in my view and i've used
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
Once the user starts writing, I hide on of my views, and make another one programmatically
Here's my concept, Once i start writing, my 2nd view comes in:
But once i get the second view properly added, i'd like to add a UILabel to it. Here's my code:
self.word_of_the_day_view.isHidden = true // main view
self.popularView.frame = CGRect(x: self.searcy_bar_view.frame.minX, y: self.searcy_bar_view.frame.maxY + 15, width: self.searcy_bar_view.frame.width, height: self.searcy_bar_view.frame.height) // don't mind the height and width
self.popularView.backgroundColor = .white
self.popularView.layer.cornerRadius = self.searcy_bar_view.layer.cornerRadius
self.popularView.popIn() // just an animation
is_popular_added = true
self.view.addSubview(popularView)
let label_1 = UILabel()
label_1.frame = CGRect(x: self.popularView.frame.minX + 3, y: self.popularView.frame.minY, width: self.popularView.frame.width, height: 20)
label_1.font = UIFont(name: "avenirnext-regular", size: 13)
label_1.text = "Hello World!"
label_1.layer.borderColor = UIColor.red.cgColor
popularView.addSubview(label_1)
But on runtime, the UILabel isn't even added.
Thank you so much for even reading the question!
You need to understand the difference between Frame and Bounds.
The Frame of the view is the position of the view in it's super view, so his origin can have any value of a CGPoint.
The Bounds are just the view itself, so it always have an origin of (0,0)
I think the problem is
label_1.frame = CGRect(x: self.popularView.frame.minX + 3, y: self.popularView.frame.minY, width: self.popularView.frame.width, height: 20)
try to change in
label_1.frame = CGRect(x: self.popularView.bounds.minX + 3, y: self.popularView.bounds.minY, width: self.popularView.frame.width, height: 20)
or
label_1.frame = CGRect(x: 3, y: 0, width: self.popularView.frame.width, height: 20)
In your code
label_1.frame = CGRect(x: self.popularView.frame.minX + 3, y: self.popularView.frame.minY, width: self.popularView.frame.width, height: 20)
This is because self.popularView.frame.minX and self.popularView.frame.minY have some +value that crosses the boundary of the current view on which you are trying to add label_1.
you should use self.popularView.bounds.origin.x and self.popularView.bounds.origin.y
Difference between bounds and frame
frame = a view's location and size with respect to the parent view's coordinate system
bounds = a view's size using its own coordinate system
This issues is due to confusion between frames and bounds. I also got similar issue in on of my app.
The bounds of an UIView is the rectangle, expressed as a location (x,y) and size (width,height) relative to its own coordinate system (0,0).
The frame of an UIView is the rectangle, expressed as a location (x,y) and size (width,height) relative to the superview it is contained within.
you should use self.popularView.bounds.origin.x and self.popularView.bounds.origin.y in place of self.popularView.frame.minX and self.popularView.frame.minY

How to use preservesSuperviewLayoutMargins in nested views

I'm trying to make used of preservesSuperviewLayoutMargins through a nested view hierarchy but hitting a lot of issues with UIKit and wondering what I am doing wrong (or whether this is a genuine bug).
I'm trying to lay out a handful of views (some by the side of each other, some above each other) like so:
// Various combinations here will create different visual results (it will either work or won't)
let i1 = ImageView()
//let i1 = LabelView()
let i2 = ImageView()
//let i2 = LabelView()
let i3 = ImageView()
//let i3 = LabelView()
let view = LeftRight(left: i1, right: TopBottom(top: i2, bottom: i3))
//let view = LeftRight(left: TopBottom(top: i2, bottom: i3), right: i1)
With some layout's I can get it to work, shown below. You can see that the images on the right are correctly indented against the green view, which is in turn correctly indented against the white view.
But with others I am struggling:
Running code similar to this in an app I can get a complete lockup and memory runaway.
The indenting is done using layoutMargins like this:
let masterView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
masterView.translatesAutoresizingMaskIntoConstraints = false
masterView.layoutMargins = UIEdgeInsets(top: 5.0, left: 5.0, bottom: 5.0, right: 5.0)
let subView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
subView.translatesAutoresizingMaskIntoConstraints = false
subView.layoutMargins = UIEdgeInsets(top: 5.0, left: 5.0, bottom: 5.0, right: 5.0)
Here's a sample of the image layout from a playground. The full playground project can be found here and can easily be loaded up and ran in XCode to see what I mean.
class ImageView: UIView {
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 1000, height: 1000))
self.preservesSuperviewLayoutMargins = true
self.layoutMargins = .zero
let imageView = UIImageView(image: UIImage(named: "jesus"))
imageView.contentMode = .center
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.clipsToBounds = true
self.clipsToBounds = true
imageView.backgroundColor = UIColor.red
self.addSubview(imageView)
imageView.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor).isActive = true
}
}
I believe that you forgot something those are required.
Implementation of intrinsicContentSize on ImageView and Label
Missing constraints that make both subviews have the same width and height in those 2 UIView subclasses (TopBottom and LeftRight)
right view of view (LeftRight) still haven't been set its translatesAutoresizingMaskIntoConstraints property to false
Add the constraint for bottom of the outmost frame view
Call layoutIfNeeded() on frame view
top.widthAnchor.constraint(equalTo: bottom.widthAnchor).isActive = true
top.heightAnchor.constraint(equalTo: bottom.heightAnchor).isActive = true
Ok. Found your issue. Your master view and your subview have both a frame of size 500x500 and a marginLayout of 5 points per edge. If you instantiate those with a frame of .zero then everything should be fine.
Update
After taking a bit more time with your Playground I also added this to ImageView:
private var imageView: UIImageView!
override var intrinsicContentSize: CGSize {
return imageView?.intrinsicContentSize ?? .zero
}
and LabelView:
private var textLabel: UILabel!
override var intrinsicContentSize: CGSize {
return textLabel?.intrinsicContentSize ?? .zero
}
Now in your init methods of both ImageView and LabelView use these instance properties instead of a local variable. After doing so it seems that all possible combinations work. If you had one specific combination that you knew it didn't please let me know so I can test it.

How to add a 10px space around a tableViewCell?

I'm trying to create a tableViewCell with a 10px spacing around it so the cell doesn't touch the edges of the screen. I tried doing something like this I saw on another stackOverflow post in my cellForRowAtIndexPath but all I got was a black screen once I ran it in simulator:
cell.contentView.backgroundColor = UIColor.white
let whiteRoundedView : UIView = UIView(frame: CGRect(x: 10, y: 10, width: self.view.frame.size.width - 20, height: 188))
whiteRoundedView.layer.backgroundColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1.0, 1.0, 1.0, 0.8])
whiteRoundedView.layer.masksToBounds = false
whiteRoundedView.layer.cornerRadius = 10
cell.contentView.addSubview(whiteRoundedView)
cell.contentView.sendSubview(toBack: whiteRoundedView)
I think that it's better assign this 10px from storyboard (or xib).
You can insert a leading space constraint in your UIView (designed in your tableViewCell).
So automatically you no longer work with frames that which often gives problem...
You should bring your whiteRoundedView to the front of the contentView. Right now the whiteRoundedView is behind the cell's contentView.
cell.contentView.bringSubview(toFront: whiteRoundedView)

Resources