From 01a4ee4cc73c5b485030e9221cd178167aa81e1b Mon Sep 17 00:00:00 2001 From: Michelle Linington Date: Thu, 8 Apr 2021 00:41:27 -0700 Subject: [PATCH] Perf improvements for CVC rendering Fixes some low-hanging expensive renders in CVC. Namely: - There are a few places where shadow paths can be trivially calculated. Explicitly setting the shadow path saves an offscreen pass. - Adopts CALayer corner masking where feasible (e.g. layers with consistent corner radii). --- .../src/Calls/UserInterface/CallButton.swift | 5 +- .../ProfileSettingsViewController.swift | 6 + .../Cells/QuotedMessageView.swift | 65 ++++--- .../ConversationScrollButton.m | 5 +- .../Views/Link Preview/LinkPreviewView.swift | 172 ++++++++---------- SignalMessaging/Views/OWSBubbleShapeView.m | 16 +- SignalMessaging/Views/OWSBubbleView.h | 6 +- SignalMessaging/Views/OWSBubbleView.m | 22 ++- .../Views/Tooltips/TooltipView.swift | 3 +- SignalMessaging/categories/UIView+OWS.swift | 9 + 10 files changed, 168 insertions(+), 141 deletions(-) diff --git a/Signal/src/Calls/UserInterface/CallButton.swift b/Signal/src/Calls/UserInterface/CallButton.swift index 692c8db248..2e6c2bd587 100644 --- a/Signal/src/Calls/UserInterface/CallButton.swift +++ b/Signal/src/Calls/UserInterface/CallButton.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -120,6 +120,9 @@ class CallButton: UIButton { currentConstraints.removeAll() currentConstraints += circleView.autoSetDimensions(to: CGSize(square: currentIconSize)) + circleView.layer.shadowPath = UIBezierPath( + ovalIn: CGRect(origin: .zero, size: .square(currentIconSize)) + ).cgPath currentConstraints += iconView.autoPinEdgesToSuperviewEdges(with: currentIconInsets) if let dropdownIconView = dropdownIconView { currentConstraints.append(dropdownIconView.autoPinEdge(.leading, to: .trailing, of: iconView, withOffset: isSmall ? 0 : 2)) diff --git a/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift index d0cd17dc98..2a045177c8 100644 --- a/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift @@ -250,6 +250,9 @@ class ProfileSettingsViewController: OWSTableViewController2 { cameraImageContainer.layer.shadowOpacity = 0.2 cameraImageContainer.layer.shadowRadius = 4 cameraImageContainer.layer.shadowOffset = CGSize(width: 0, height: 2) + cameraImageContainer.layer.shadowPath = UIBezierPath( + ovalIn: CGRect(origin: .zero, size: .square(32)) + ).cgPath cell.contentView.addSubview(cameraImageContainer) cameraImageContainer.autoPinTrailing(toEdgeOf: avatarImageView) @@ -260,6 +263,9 @@ class ProfileSettingsViewController: OWSTableViewController2 { secondaryShadowView.layer.shadowOpacity = 0.12 secondaryShadowView.layer.shadowRadius = 16 secondaryShadowView.layer.shadowOffset = CGSize(width: 0, height: 4) + secondaryShadowView.layer.shadowPath = UIBezierPath( + ovalIn: CGRect(origin: .zero, size: .square(32)) + ).cgPath cameraImageContainer.addSubview(secondaryShadowView) secondaryShadowView.autoPinEdgesToSuperviewEdges() diff --git a/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift b/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift index c251d5598d..867cd6683c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -167,6 +167,42 @@ public class QuotedMessageView: UIView { : conversationStyle.quotingSelfHighlightColor()) } + func createInnerBubbleView(sharpCorners: OWSDirectionalRectCorner) -> UIView { + let sharpCornerRadius: CGFloat = 4 + let wideCornerRadius: CGFloat = 12 + + let innerBubbleView: UIView + if sharpCorners.isEmpty || sharpCorners.contains(.allCorners) { + innerBubbleView = UIView() + innerBubbleView.layer.maskedCorners = .all + innerBubbleView.layer.cornerRadius = sharpCorners.isEmpty ? wideCornerRadius : sharpCornerRadius + + } else { + // Slow path. CA isn't optimized to handle corners of multiple radii + // Let's do it by hand with a CAShapeLayer + let maskLayer = CAShapeLayer() + innerBubbleView = OWSLayerView(frame: .zero) { (layerView: UIView) in + let layerFrame = layerView.bounds + + let bubbleLeft: CGFloat = 0 + let bubbleRight = layerFrame.width + let bubbleTop: CGFloat = 0 + let bubbleBottom = layerFrame.height + let bezierPath = OWSBubbleView.roundedBezierRect(withBubbleTop: bubbleTop, + bubbleLeft: bubbleLeft, + bubbleBottom: bubbleBottom, + bubbleRight: bubbleRight, + sharpCornerRadius: sharpCornerRadius, + wideCornerRadius: wideCornerRadius, + sharpCorners: sharpCorners) + maskLayer.path = bezierPath.cgPath + } + innerBubbleView.layer.mask = maskLayer + } + innerBubbleView.backgroundColor = conversationStyle.quotedReplyBubbleColor + return innerBubbleView + } + func createContents(rootView: UIView, quotedAuthorLabel: UILabel, quotedTextLabel: UILabel, @@ -179,32 +215,7 @@ public class QuotedMessageView: UIView { rootView.layoutMargins = .zero rootView.clipsToBounds = true - let maskLayer = CAShapeLayer() - - let innerBubbleView = OWSLayerView(frame: .zero) { (layerView: UIView) in - let layerFrame = layerView.bounds - - let bubbleLeft: CGFloat = 0 - let bubbleRight = layerFrame.width - let bubbleTop: CGFloat = 0 - let bubbleBottom = layerFrame.height - - let sharpCornerRadius: CGFloat = 4 - let wideCornerRadius: CGFloat = 12 - - let bezierPath = OWSBubbleView.roundedBezierRect(withBubbleTop: bubbleTop, - bubbleLeft: bubbleLeft, - bubbleBottom: bubbleBottom, - bubbleRight: bubbleRight, - sharpCornerRadius: sharpCornerRadius, - wideCornerRadius: wideCornerRadius, - sharpCorners: sharpCorners) - - maskLayer.path = bezierPath.cgPath - } - - innerBubbleView.layer.mask = maskLayer - innerBubbleView.backgroundColor = conversationStyle.quotedReplyBubbleColor + let innerBubbleView = createInnerBubbleView(sharpCorners: sharpCorners) rootView.addSubview(innerBubbleView) innerBubbleView.autoPinLeadingToSuperviewMargin(withInset: bubbleHMargin) innerBubbleView.autoPinTrailingToSuperviewMargin(withInset: bubbleHMargin) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m index d9610b7bb9..4871937322 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // #import "ConversationScrollButton.h" @@ -67,6 +67,7 @@ NS_ASSUME_NONNULL_BEGIN iconView.userInteractionEnabled = NO; const CGFloat circleSize = self.class.circleSize; + CGRect shadowRect = CGRectMake(0, 0, circleSize, circleSize); UIView *shadowView = [[OWSCircleView alloc] initWithDiameter:circleSize]; self.shadowView = shadowView; @@ -75,6 +76,7 @@ NS_ASSUME_NONNULL_BEGIN shadowView.layer.shadowRadius = 4; shadowView.layer.shadowOpacity = 0.05f; shadowView.layer.shadowColor = UIColor.blackColor.CGColor; + shadowView.layer.shadowPath = [UIBezierPath bezierPathWithOvalInRect:shadowRect].CGPath; UIView *circleView = [[OWSCircleView alloc] initWithDiameter:circleSize]; self.circleView = circleView; @@ -83,6 +85,7 @@ NS_ASSUME_NONNULL_BEGIN circleView.layer.shadowRadius = 12.f; circleView.layer.shadowOpacity = 0.3f; circleView.layer.shadowColor = UIColor.blackColor.CGColor; + circleView.layer.shadowPath = [UIBezierPath bezierPathWithOvalInRect:shadowRect].CGPath; UIView *unreadBadge = [UIView new]; self.unreadBadge = unreadBadge; diff --git a/SignalMessaging/Views/Link Preview/LinkPreviewView.swift b/SignalMessaging/Views/Link Preview/LinkPreviewView.swift index 649d8ee4c9..a6b9859627 100644 --- a/SignalMessaging/Views/Link Preview/LinkPreviewView.swift +++ b/SignalMessaging/Views/Link Preview/LinkPreviewView.swift @@ -339,13 +339,9 @@ public protocol LinkPreviewViewDraftDelegate { // MARK: - -@objc -public class LinkPreviewImageView: UIImageView { - private let maskLayer = CAShapeLayer() - - @objc - public enum Rounding: UInt { - case none +private class LinkPreviewImageView: UIImageView { + fileprivate enum Rounding: UInt { + case standard case asymmetrical case circular } @@ -353,34 +349,38 @@ public class LinkPreviewImageView: UIImageView { private let rounding: Rounding fileprivate var isHero = false - @objc - public init(rounding: Rounding) { - self.rounding = rounding + // We only need to use a more complicated corner mask if we're + // drawing asymmetric corners. This is an exceptional case to match + // the input toolbar curve. + private let asymmetricCornerMask = CAShapeLayer() + init(rounding: Rounding) { + self.rounding = rounding super.init(frame: .zero) - self.layer.mask = maskLayer + if rounding == .asymmetrical { + layer.mask = asymmetricCornerMask + } } - public required init?(coder aDecoder: NSCoder) { - self.rounding = .none - + required init?(coder aDecoder: NSCoder) { + self.rounding = .standard super.init(coder: aDecoder) } - public override var bounds: CGRect { + override var bounds: CGRect { didSet { updateMaskLayer() } } - public override var frame: CGRect { + override var frame: CGRect { didSet { updateMaskLayer() } } - public override var center: CGPoint { + override var center: CGPoint { didSet { updateMaskLayer() } @@ -388,62 +388,62 @@ public class LinkPreviewImageView: UIImageView { private func updateMaskLayer() { let layerBounds = self.bounds - - guard rounding != .circular else { - maskLayer.path = UIBezierPath(ovalIn: layerBounds).cgPath - return - } - - // One of the corners has assymetrical rounding to match the input toolbar border. - // This is somewhat inconvenient. - let upperLeft = CGPoint(x: 0, y: 0) - let upperRight = CGPoint(x: layerBounds.size.width, y: 0) - let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height) - let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height) - let bigRounding: CGFloat = 14 let smallRounding: CGFloat = 6 - let upperLeftRounding: CGFloat - let upperRightRounding: CGFloat - if rounding == .asymmetrical { - upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding - upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding - } else { - upperLeftRounding = smallRounding - upperRightRounding = smallRounding + switch rounding { + case .standard: + layer.cornerRadius = smallRounding + layer.maskedCorners = isHero ? .top : .all + case .circular: + layer.cornerRadius = bounds.size.smallerAxis + layer.maskedCorners = .all + case .asymmetrical: + // This uses a more expensive layer mask to clip corners + // with different radii. + // This should only be used in the input toolbar so perf is + // less of a concern here. + owsAssertDebug(!isHero, "Link preview drafts never use hero images") + + let upperLeft = CGPoint(x: 0, y: 0) + let upperRight = CGPoint(x: layerBounds.size.width, y: 0) + let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height) + let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height) + + let upperLeftRounding: CGFloat = CurrentAppContext().isRTL ? smallRounding : bigRounding + let upperRightRounding: CGFloat = CurrentAppContext().isRTL ? bigRounding : smallRounding + let lowerRightRounding = smallRounding + let lowerLeftRounding = smallRounding + + let path = UIBezierPath() + + // It's sufficient to "draw" the rounded corners and not the edges that connect them. + path.addArc(withCenter: upperLeft.offsetBy(dx: +upperLeftRounding).offsetBy(dy: +upperLeftRounding), + radius: upperLeftRounding, + startAngle: CGFloat.pi * 1.0, + endAngle: CGFloat.pi * 1.5, + clockwise: true) + + path.addArc(withCenter: upperRight.offsetBy(dx: -upperRightRounding).offsetBy(dy: +upperRightRounding), + radius: upperRightRounding, + startAngle: CGFloat.pi * 1.5, + endAngle: CGFloat.pi * 0.0, + clockwise: true) + + path.addArc(withCenter: lowerRight.offsetBy(dx: -lowerRightRounding).offsetBy(dy: -lowerRightRounding), + radius: lowerRightRounding, + startAngle: CGFloat.pi * 0.0, + endAngle: CGFloat.pi * 0.5, + clockwise: true) + + path.addArc(withCenter: lowerLeft.offsetBy(dx: +lowerLeftRounding).offsetBy(dy: -lowerLeftRounding), + radius: lowerLeftRounding, + startAngle: CGFloat.pi * 0.5, + endAngle: CGFloat.pi * 1.0, + clockwise: true) + + asymmetricCornerMask.path = path.cgPath } - let lowerRightRounding = isHero ? 0 : smallRounding - let lowerLeftRounding = isHero ? 0 : smallRounding - - let path = UIBezierPath() - - // It's sufficient to "draw" the rounded corners and not the edges that connect them. - path.addArc(withCenter: upperLeft.offsetBy(dx: +upperLeftRounding).offsetBy(dy: +upperLeftRounding), - radius: upperLeftRounding, - startAngle: CGFloat.pi * 1.0, - endAngle: CGFloat.pi * 1.5, - clockwise: true) - - path.addArc(withCenter: upperRight.offsetBy(dx: -upperRightRounding).offsetBy(dy: +upperRightRounding), - radius: upperRightRounding, - startAngle: CGFloat.pi * 1.5, - endAngle: CGFloat.pi * 0.0, - clockwise: true) - - path.addArc(withCenter: lowerRight.offsetBy(dx: -lowerRightRounding).offsetBy(dy: -lowerRightRounding), - radius: lowerRightRounding, - startAngle: CGFloat.pi * 0.0, - endAngle: CGFloat.pi * 0.5, - clockwise: true) - - path.addArc(withCenter: lowerLeft.offsetBy(dx: +lowerLeftRounding).offsetBy(dy: -lowerLeftRounding), - radius: lowerLeftRounding, - startAngle: CGFloat.pi * 0.5, - endAngle: CGFloat.pi * 1.0, - clockwise: true) - - maskLayer.path = path.cgPath } } @@ -472,6 +472,7 @@ public class LinkPreviewView: UIStackView { public var hasAsymmetricalRounding: Bool = false { didSet { AssertIsOnMainThread() + owsAssertDebug(isDraft) if hasAsymmetricalRounding != oldValue { updateContents() @@ -1016,13 +1017,7 @@ public class LinkPreviewView: UIStackView { owsFailDebug("Could not load image.") return nil } - let rounding: LinkPreviewImageView.Rounding = { - if let roundingParam = roundingParam { - return roundingParam - } - return self.hasAsymmetricalRounding ? .asymmetrical : .none - }() - let imageView = LinkPreviewImageView(rounding: rounding) + let imageView = LinkPreviewImageView(rounding: roundingParam ?? .standard) imageView.image = image imageView.isHero = Self.sentIsHero(state: state) return imageView @@ -1041,7 +1036,7 @@ public class LinkPreviewView: UIStackView { owsFailDebug("Could not load image.") return nil } - let rounding: LinkPreviewImageView.Rounding = hasAsymmetricalRounding ? .asymmetrical : .none + let rounding: LinkPreviewImageView.Rounding = hasAsymmetricalRounding ? .asymmetrical : .standard let imageView = LinkPreviewImageView(rounding: rounding) imageView.image = image return imageView @@ -1268,29 +1263,6 @@ public class LinkPreviewView: UIStackView { return CGSize(width: width, height: height) } - @objc - public func addBorderViews(bubbleView: OWSBubbleView) { - if let heroImageView = self.heroImageView { - let borderView = OWSBubbleShapeView(draw: ()) - borderView.strokeColor = Theme.primaryTextColor - borderView.strokeThickness = CGHairlineWidthFraction(1.8) - heroImageView.addSubview(borderView) - bubbleView.addPartnerView(borderView) - borderView.autoPinEdgesToSuperviewEdges() - } - if let sentBodyView = self.sentBodyView { - let borderView = OWSBubbleShapeView(draw: ()) - let borderColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray60 : UIColor.ows_gray15) - borderView.strokeColor = borderColor - borderView.strokeThickness = CGHairlineWidthFraction(1.8) - sentBodyView.addSubview(borderView) - bubbleView.addPartnerView(borderView) - borderView.autoPinEdgesToSuperviewEdges() - } else { - owsFailDebug("Missing sentBodyView") - } - } - @objc func didTapCancel(sender: UIButton) { self.draftDelegate?.linkPreviewDidCancel() } diff --git a/SignalMessaging/Views/OWSBubbleShapeView.m b/SignalMessaging/Views/OWSBubbleShapeView.m index 9727736245..3d9cf3c03d 100644 --- a/SignalMessaging/Views/OWSBubbleShapeView.m +++ b/SignalMessaging/Views/OWSBubbleShapeView.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // #import "OWSBubbleShapeView.h" @@ -255,9 +255,6 @@ typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) { self.layer.mask = self.maskLayer; break; case OWSBubbleShapeViewMode_InnerShadow: { - self.maskLayer.path = bezierPath.CGPath; - self.layer.mask = self.maskLayer; - // Inner shadow. // This should usually not be visible; it is used to distinguish // profile pics from the background if they are similar. @@ -267,16 +264,21 @@ typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) { UIBezierPath *shadowPath = [bezierPath copy]; // This can be any value large enough to cast a sufficiently large shadow. CGFloat shadowInset = -(self.innerShadowRadius * 4.f); - [shadowPath - appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(shadowBounds, shadowInset, shadowInset)]]; + + CGRect outerRect = CGRectInset(shadowBounds, shadowInset, shadowInset); + UIBezierPath *outerPath = [UIBezierPath bezierPathWithRect:outerRect]; + // -[CALayer shadowPath] uses the non-zero winding rule + // Reverse the path to make the directions line up correctly. + [shadowPath appendPath:[outerPath bezierPathByReversingPath]]; + // This can be any color since the fill should be clipped. self.shapeLayer.fillColor = UIColor.blackColor.CGColor; self.shapeLayer.path = shadowPath.CGPath; - self.shapeLayer.fillRule = kCAFillRuleEvenOdd; self.shapeLayer.shadowColor = self.innerShadowColor.CGColor; self.shapeLayer.shadowRadius = self.innerShadowRadius; self.shapeLayer.shadowOpacity = self.innerShadowOpacity; self.shapeLayer.shadowOffset = CGSizeZero; + self.shapeLayer.shadowPath = shadowPath.CGPath; break; } diff --git a/SignalMessaging/Views/OWSBubbleView.h b/SignalMessaging/Views/OWSBubbleView.h index bbd0312aec..9a6fcaf6e9 100644 --- a/SignalMessaging/Views/OWSBubbleView.h +++ b/SignalMessaging/Views/OWSBubbleView.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -12,7 +12,9 @@ typedef NS_OPTIONS(NSUInteger, OWSDirectionalRectCorner) { OWSDirectionalRectCornerTopTrailing = 1 << 1, OWSDirectionalRectCornerBottomLeading = 1 << 2, OWSDirectionalRectCornerBottomTrailing = 1 << 3, - OWSDirectionalRectCornerAllCorners = ~0UL + + OWSDirectionalRectCornerAllCorners = OWSDirectionalRectCornerTopLeading | OWSDirectionalRectCornerTopTrailing + | OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing }; @class OWSBubbleView; diff --git a/SignalMessaging/Views/OWSBubbleView.m b/SignalMessaging/Views/OWSBubbleView.m index 219eaf52d1..59b104f2ae 100644 --- a/SignalMessaging/Views/OWSBubbleView.m +++ b/SignalMessaging/Views/OWSBubbleView.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // #import "OWSBubbleView.h" @@ -73,8 +73,8 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4; [self.layer addSublayer:self.gradientLayer]; self.gradientLayer.hidden = YES; + self.layer.masksToBounds = YES; self.maskLayer = [CAShapeLayer new]; - self.layer.mask = self.maskLayer; _partnerViews = [NSMutableArray new]; @@ -211,6 +211,24 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4; self.maskLayer.path = bezierPath.CGPath; + BOOL noSharpCorners = (self.sharpCorners == 0); + BOOL allSharpCorners + = (self.sharpCorners & OWSDirectionalRectCornerAllCorners) == OWSDirectionalRectCornerAllCorners; + + if (noSharpCorners || allSharpCorners) { + // Although we have a bubble bezier path, it's just a simple rounded rect + // It's more performant to use the CA corner mask property than a layer mask + CGFloat sharpRadius = kOWSMessageCellCornerRadius_Small; + CGFloat wideRadius = kOWSMessageCellCornerRadius_Large; + + self.layer.cornerRadius = (self.sharpCorners == 0) ? wideRadius : sharpRadius; + self.layer.mask = nil; + } else { + self.layer.cornerRadius = 0; + self.layer.mask = self.maskLayer; + } + + [CATransaction commit]; } diff --git a/SignalMessaging/Views/Tooltips/TooltipView.swift b/SignalMessaging/Views/Tooltips/TooltipView.swift index 68651d144f..6b85f7bf2c 100644 --- a/SignalMessaging/Views/Tooltips/TooltipView.swift +++ b/SignalMessaging/Views/Tooltips/TooltipView.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -117,6 +117,7 @@ open class TooltipView: UIView { bezierPath.addLine(to: tailPoint) shapeLayer.path = bezierPath.cgPath + shapeLayer.shadowPath = bezierPath.cgPath shapeLayer.frame = view.bounds } diff --git a/SignalMessaging/categories/UIView+OWS.swift b/SignalMessaging/categories/UIView+OWS.swift index 089d453f89..09a18463ac 100644 --- a/SignalMessaging/categories/UIView+OWS.swift +++ b/SignalMessaging/categories/UIView+OWS.swift @@ -621,6 +621,15 @@ public extension UIBezierPath { } } +public extension CACornerMask { + static let top: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + static let bottom: CACornerMask = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + static let left: CACornerMask = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + static let right: CACornerMask = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + + static let all: CACornerMask = top.union(bottom) +} + // MARK: - @objc