Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix potential crash when creating a shadow path in box #149

Merged
merged 4 commits into from Sep 17, 2020
Merged

Conversation

kylebshr
Copy link
Contributor

@kylebshr kylebshr commented Sep 16, 2020

The CGPath init has an assertion when creating a rounded rect that can cause a crash if the path is created with a frame width smaller than 2 * corner radius. The UIBezierPath init has no issues with this (though your path might look a little weird) and is much safer to use here. Even though UIBezierPath doesn't crash in this case, I updated the behavior to max out the radius at min(width, height) / 2, so you don't get the nonsensical diamond shape.

Copy link
Collaborator

@bencochran bencochran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like UIBezierPath essentially does cornerRadius = min(cornerRadius, bounds.width / 2, bounds.height / 2) (that is, if corner radius is too big, it makes a capsule or circle shape). This is different that the cornerRadius of the layer which makes a round-diamond-thing in this case.

So with this code:

let frame = CGRect(x: 150, y: 150, width: 100, height: 100)
let bounds = CGRect(origin: .zero, size: frame.size)
let radius: CGFloat = 80
let view = UIView(frame: frame)
view.backgroundColor = .blue
view.layer.cornerRadius = radius
view.layer.shadowPath = UIBezierPath(
    roundedRect: bounds,
    cornerRadius: radius
).cgPath
view.layer.shadowOpacity = 1
view.layer.shadowColor = UIColor.red.cgColor
view.layer.shadowRadius = 0
view.layer.shadowOffset = .zero

we get:
image

Wondering if we should mimic UIBezierPath’s behavior for our corner radius (which gives a nicer handling of this case, and makes the shadow match the contents). Generally something like:

let frame = CGRect(x: 150, y: 150, width: 100, height: 100)
let bounds = CGRect(origin: .zero, size: frame.size)
let radius: CGFloat = 80
let actualRadius = min(radius, bounds.width / 2, bounds.height / 2)
let view = UIView(frame: frame)
view.backgroundColor = UIColor.blue.withAlphaComponent(0.8)
view.layer.cornerRadius = actualRadius
view.layer.shadowPath = UIBezierPath(
    roundedRect: bounds,
    cornerRadius: actualRadius
).cgPath
view.layer.shadowOpacity = 1
view.layer.shadowColor = UIColor.red.cgColor
view.layer.shadowRadius = 0
view.layer.shadowOffset = .zero

to produce
image

@kylebshr
Copy link
Contributor Author

As weird of a use-case as it might be, I don't know that we should limit someone from creating that diamond shape; what do you think about just not setting a shadowPath when the radii is too big? It's a performance optimization I added recently, and works fine without setting it

@kylebshr
Copy link
Contributor Author

Weirdly though, I'm not seeing the same behavior you are @bencochran - check out the snapshot image. Using a UIBezierPath and it's matching the shape of the layer 🤔

@@ -87,6 +87,16 @@ class BoxTests: XCTestCase {
of: element,
size: CGSize(width: 100, height: 100))
}

func test_largeCornerRadius() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test crashes on main on iOS 11

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot looks real funky... is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the "diamond" shape without clipping - but am going to update to use the min of width/height

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right; looks way better now

@kyleve
Copy link
Collaborator

kyleve commented Sep 16, 2020

I think I agree w/ ben here; the diamond seems really unexpected to me.

@kylebshr
Copy link
Contributor Author

kylebshr commented Sep 17, 2020 via email

@@ -146,9 +146,10 @@ extension Box.CornerStyle {
case .square:
return 0
case .capsule:
return min(bounds.height, bounds.width) / 2.0
return min(bounds.midX, bounds.midY)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw, while this is less code; its also a bit only based on the fact that bounds is always at 0,0. If it was not at 0,0, this would be reporting the middle of the frame in its coordinate space, whereas the / 2 on the width and height makes this explict.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense.. just to make sure I understand, a rect of x: 10, y: 10, width: 20, height: 20 would have a midX of 15, right? Will go back to / 2 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it would

@kylebshr
Copy link
Contributor Author

Alrighty, no more diamonds! Should be all set for review 👍

@@ -87,6 +87,16 @@ class BoxTests: XCTestCase {
of: element,
size: CGSize(width: 100, height: 100))
}

func test_largeCornerRadius() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right; looks way better now

@kylebshr kylebshr merged commit d7ab362 into main Sep 17, 2020
@kylebshr kylebshr deleted the kb/box-crash branch May 22, 2021 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants