How do you get the safe area coordinates of a view programmatically in iOS 14? - ios

Put simply, I want to get the bounds of this blue rectangle using Swift code:
The coordinates of this rectangle are visible in the Size inspector:
In an empty App project in Xcode, I've tried using view.safeAreaLayoutGuide.layoutFrame to get the CGRect, but this property seems to contain the bounds of the entire view, not the safe area. Specifically, it returns 0.0, 0.0, 896.0 and 414.0 for minX, minY, width and height respectively.
I've also tried to get the top, bottom, left and right properties of view.safeAreaInsets, but this returns 0 for each of them.
I've also tried to get the view.superview in case there was another view on top, but it returned nil.
All these values come from the iPhone 11. I'm using Xcode 12.

First, get the safe area insets. (Wait until they are known before you get them.) Okay, what are they inset from? The view controller's view. So simply apply those insets to the view controller's view's bounds and you have the rect of your rectangle (in view coordinates).
So, I believe this is the rectangle you were looking for?
How I did that:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for v in self.view.subviews {
v.removeFromSuperview()
}
let v = UIView()
v.backgroundColor = .blue
self.view.addSubview(v)
// okay, ready? here we go ...
let r = self.view.bounds.inset(by:self.view.safeAreaInsets) // *
v.frame = r
}
Remember, that rect, r, is in view coordinates. If you need the rect in some other coordinate system, there are coordinate conversion methods you can call.

Related

Is a view's frame always set by the time `draw(_:)` is called?

I use the below code to curve the edge of a custom view.
/// Curves `self's` trailing edge.
override func draw(_ rect: CGRect) {
super.draw(rect)
let path=UIBezierPath(ovalIn: .init( // The path defining `self's` curved edge.
x: rect.width - Self.ovalWidth, // Align the oval's trailing edge with `self's`.
y: (rect.height - Self.ovalHeight) / 2, // Center oval vertically within `self`.
width: Self.ovalWidth,
height: Self.ovalHeight
))
path.stroke() // Draws the border of the curved edge.
self.layer.mask={ $0.path=path.cgPath; return $0 }(CAShapeLayer())
}
My concern is that as far as I can see there is no guarantee that rect will be equal to self.frame; which would result in undefined behavior as far as drawing the mask goes. Is it possible that if I use self.frame instead of rect that draw(_:) could be called while self.frame has yet to be set, or has a value that is not up-to-date? — this view is laid out using AutoLayout.
Your concerns are only half correct. According to the docs:
The portion of the view’s bounds that needs to be updated. The first
time your view is drawn, this rectangle is typically the entire
visible bounds of your view. However, during subsequent drawing
operations, the rectangle may specify only part of your view.
Also in the docs:
You should limit any drawing to the rectangle specified in the rect
parameter
draw(_:) is typically only called when the view is first displayed (not when it is added as a subview) and when setNeedsDisplay() or setNeedsDisplay(_:) is called.
As far as I know there are only two causes for concern. The first is that your view is displayed before all of you constraints are set. The second is if you call setNeedsDisplay(_:) with a specific rectangle.

Put a mask layer on a UIView of varying size inside UITableViewCell

I have a UITableView whose cells contain a subview on which I need to perform three things:
change its width constraint at runtime depending on a value inside an object specific to this cell
change its background color depending on that same value
round the top left and bottom left corners of the view but keep the corners on the right hand side as they are (so layer.cornerRadius is not an option)
I use the following code inside my custom UITableViewCell subclass to achieve the rounded corner effect on one side of the view only, which I call from tableView:cellForRowAt::
func roundLeadingEdgesOfBar() {
let roundedLayer = CAShapeLayer()
roundedLayer.bounds = viewInQuestion.frame
roundedLayer.position = viewInQuestion.center
roundedLayer.path = UIBezierPath(roundedRect: viewInQuestion.bounds,
byRoundingCorners: [.topLeft, .bottomLeft],
cornerRadii: CGSize(width: 2, height: 2)).cgPath
viewInQuestion.layer.mask = roundedLayer
print("frame: \(viewInQuestion.frame)")
}
However what I see when I run this code is an effect like this:
The print statement in the code above produces the following output, indicating that viewInQuestion has the same frame every time when clearly on the screen it hasn't:
frame: (175.0, 139.5, 200.0, 5.0)
frame: (175.0, 139.5, 200.0, 5.0)
frame: (175.0, 139.5, 200.0, 5.0)
frame: (175.0, 139.5, 200.0, 5.0)
So I assume the width constraint on the view has not been rendered by the time I call this function. When I scroll the entire table view up until all cells are out of view, and then scroll them back into view, everything looks correct and the printed frames are all different, like I would expect:
frame: (136.5, 79.5, 238.5, 5.0)
frame: (169.5, 79.5, 205.5, 5.0)
frame: (226.0, 79.5, 149.0, 5.0)
frame: (247.5, 79.5, 127.5, 5.0)
I've read several times on SO to execute code that is dependent on constraints having been applied from within layoutSubviews, but that gave me the same result. I even tried calling roundLeadingEdgesOfBar from within tableView:willDisplay:forRowAt:, to no avail.
I also found this response to a similar problem which suggests putting the mask layer code inside drawRect:. This actually fixes 99% of the problem for me (leaving performance issues aside), but there are still corner cases (no pun intended) left for very long table views where I still see the wrong behavior.
My last resort was to call my rounding function via performSelector with a 0.00001 delay, which works in the sense that you see the bug for about a second on screen before it then disappears - still far from ideal behavior, let alone the awful code I had to write for it.
Is there any way to reliably apply the shape layer on the view inside a UITableViewCell using its correct runtime frame?
Instead of calling a function to "round the edges," I suggest creating a UIView subclass and let it handle the rounding.
For example:
class BulletBar: UIView {
override func layoutSubviews() {
let roundedLayer = CAShapeLayer()
roundedLayer.frame = bounds
roundedLayer.path = UIBezierPath(roundedRect: bounds,
byRoundingCorners: [.topLeft, .bottomLeft],
cornerRadii: CGSize(width: 2, height: 2)).cgPath
layer.mask = roundedLayer
}
}
Now, set the class of your "bar" subview in the cell to BulletBar. Use constraints to pin it to the right and bottom and to constrain the height and width. Create an IBOutlet for the width constraint, and then set the barWidthConstraint.constant as desired.
The class itself will handle rounding the corners.
Result:
I think what you should be doing is that,
Set the properties related to your current object in cellForRowAtIndexPath, try setting constraints in the setter of that particular value.
Call setNeedsLayout which will make a future call to layoutIfNeeded -> layoutSubviews
In your layoutSubviews, set the rounded corners.
I hope this will help you.

New view.frame.y doesn't match up with label.frame.y

I have a label on-screen that has its height set as 300px, set to the bottom of the screen, and I want to create a UIView that exactly overlays it. The code I use is
let theFrame = CGRect(x: myLabel.frame.minX, y: myLabel.frame.minY, width: myLabel.frame.width, height: myLabel.frame.height)
newVu = UIView(frame: theFrame)
let newColor = UIColor(colorLiteralRed: 0.5, green: 0.5, blue: 1, alpha: 0.5)
newVu.backgroundColor = newColor
self.view.addSubview(newVu)
The result that I get has the new UIView shifted about 20 points higher than the label. ((EDIT: as GeorgeGreen helped me see in Comments, this is because the label is within a Vertical StackView, which is constrained to the bottom of the Top Layout Guide -- thus the Y of the label is relative to the top of the Stack View, not the top of the screen.)) Clearly there are different frames of reference at work, but what is needed to bring everything into the same frame of reference? EDIT: Or, asked another way, how can I get the "absolute" X and Y coordinates of the label in the screen, so that no matter how many views it is embedded in, I can know where to drop a new View to exactly overlay it?
Original screen in IB:
Result of the code (new view is semi-transparent; note the yellow band at the bottom):
You can get your label position in view by
let theFrame = view.convert(myLabel.frame, from: your_stack_view_here)
and You should move it to
viewDidAppear
Because
viewWillLayoutSubviews
will call any time subview will layout. That's mind at the first time it run, your "mylabel" didn't layout with correct position.
Apple document
Regarding your question of “how can I get the "absolute" X and Y coordinates of the label in the screen”, the answer is to use UIView.convert(_:to:), passing nil as the second parameter. (This will yield coordinates relative to the containing window's origin, but for practical purposes is probably what you mean by “the screen”).

UIScrollView with Horizontal Zoom - Bezier Paths Drawn within Subviews Are Blurry

I have a scroll view that zooms only horizontally on pinch (based on this answer)
The content view has several children that are evenly spaced horizontally, and placed at different heights. Basically, I'm plotting a graph with dots.
Each marker is a custom UIView subclass that has a label as subview and draws a red circle inside drawRect(_:).
(The scroll view and its only child the content view use Autolayout, but all subviews of the content view are placed with frames calculated at runtime; no constraints except the label positioning respect to the marker)
I have modified the answer linked above, so that -when zooming- the dots get more spaced horizontally, but stay the same size.
This is part of the code for my content view:
override var transform: CGAffineTransform {
get {
return super.transform
}
set {
if let unzoomedViewHeight = unzoomedViewHeight {
// ^Initial height of the content view, captured
// on layoutSubviews() and used to calculate a
// a zoom transform that preserves the height
// (see linked answer for details)
// 1. Ignore vertical zooming for this view:
var modified = newValue
modified.d = 1.0
modified.ty = (1.0 - modified.a) * unzoomedViewHeight/2
super.transform = modified
// 2. Ignore zooming altogether in marker subviews:
var markerTransform = CGAffineTransformInvert(newValue)
markerTransform.d = 1.0 // No y-scale
for subview in subviews where subview is MarkerView {
subview.transform = markerTransform
}
}
}
}
Next, I want to connect my dots with straight line segments. The problem is, when I zoom the scroll view the segments become blurry.
In addition to the blurring, and because I am only zooming horizontally, the segments "shear" a bit (depending on their their slope), and thus the line width becomes uneven:
(Right now I am placing an intermediate view between each pair of dots, that draws a single segment - but the result is the same with a single path)
I have tried making the segment-drawing views aware of the transform that is applied to them by their parent, revert it, and instead modify their frame accordingly (and redraw the path within the new bounds); however it doesn't seem to work.
What is the best way to draw zoom-resistant bezier paths inside a scroll view?
Update:
I got rid of the pixelation by following this answer to a similar question (almost duplicate?); in my case the code translates to:
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat)
{
for segment in contentView.subviews where segment is SegmentView {
segment.layer.contentsScale = scale
segment.setNeedsDisplay()
}
}
...However, I still have the uneven line width issue due to stretching the (diagonal) segments only in the horizontal direction.
My initial thought, having not done this before exactly, is to store the graph as a bezier path rather than as a set of view line and point views, and based on a single unit axis. You could convert your data into this form just for drawing.
So, all points on the bezier path are normalised into the range 0 to 1.
Once you've done that you can apply a transform to the bezier path to translate (move) and scale (zoom) to the part you want and then draw the bezier path at full resolution.
This will work and is different to your current situation because your current code draws the views and then scales then so you get artifacts. The above option scales the path and then draws it.

issues with UIImageView.layer.cornerRadius to create rounded images on different pixel densities ios

I'm simply trying to create a perfectly round image. Here's my swift code:
myImage.layer.cornerRadius = myImage.frame.size.width/2
myImage.layer.masksToBounds = true
This works on a 4s, but is not quite round on a 5s, and appears as a rounded rectangle on a iphone 6.
I'm assuming this has to do with frame.size.width returning values in pixels not points or something like that, but I've been unable to solve this problem.
If you're putting that code in viewDidLoad, try moving it to viewDidLayoutSubviews.
I'm guessing it's an auto layout issue -- although you've used the corner radius property appropriately and are in fact making the image frame circular, after auto-layout, the corner radius stays the same, but the image stretches so that it's no longer circular.
If your code is in viewDidLoad in ViewController, try moving it to viewDidLayoutSubviews.
If your rounded imageView is in tableViewCell, try moving it to draw.
override func draw(_ rect: CGRect) {
avatarView.layer.cornerRadius = avatarView.frame.size.width / 2
avatarView.layer.masksToBounds = true
avatarView.clipsToBounds = true
avatarView.contentMode = .scaleAspectFill
}
The problem is that if you change the cornerRadius of Any view, and expect it to look like a circle, the view has to be a square.
Now, because of different devices and different device size, the size of your image view might change and it may be a rectangle.
For e.g.
If you view is a 50 * 50
myImage.layer.cornerRadius = myImage.frame.size.width/2
This would add corner radios of 25 on both sides to make it a circle.
But because of auto layout of device change, your view might be a 50 * 80
Corner radius would add a 25, but as the height is 80, it will add 25 on both sides, and the remaining 30 won't be a curve and look straight.
What you can do is try viewing the screen in various orientations in the storyboard and change auto layout Constraints (Or structs and springs) to ensure that the image view is a square in all the devices
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myImage.layer.cornerRadius = myImage.frame.size.width/2
myImage.layer.masksToBounds = true
}
This should work:
myImage.layer.cornerRadius = myImage.**bounds**.size.width/2
myImage.layer.masksToBounds = true

Resources