// // NotchGradientLoadingBarController.swift // GradientLoadingBar // // Created by Felix Mau on 11.06.20. // import UIKit /// Type-alias for the controller to be more similar to the pod name. public typealias NotchGradientLoadingBar = NotchGradientLoadingBarController open class NotchGradientLoadingBarController: GradientLoadingBarController { // MARK: - Config /// Values are based on /// private enum Config { /// The width of the iPhone notch. static let notchWidth: CGFloat = 209 /// The radius of the small circle on the outside of the notch. static let smallCircleRadius: CGFloat = 6 /// The radius of the large circle on the inside of the notch. static let largeCircleRadius: CGFloat = 20 } // MARK: - Public methods override open func setupConstraints(superview: UIView) { guard #available(iOS 11.0, *) else { /// The notch is only available when supporting safe area layout guides, which is starting from iOS 11. super.setupConstraints(superview: superview) return } // The `safeAreaInsets.top` always includes the status-bar and therefore will always be greater "0". // As a workaround we check the bottom inset. let hasNotch = superview.safeAreaInsets.bottom > 0 guard hasNotch else { // No special masking required for non safe area devices. super.setupConstraints(superview: superview) return } // Our view will be masked therefore the view height needs to cover the notch-height plus the given user-height. let height = 2 * Config.smallCircleRadius + Config.largeCircleRadius + self.height NSLayoutConstraint.activate([ gradientActivityIndicatorView.topAnchor.constraint(equalTo: superview.topAnchor), gradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: height), gradientActivityIndicatorView.leadingAnchor.constraint(equalTo: superview.leadingAnchor), gradientActivityIndicatorView.trailingAnchor.constraint(equalTo: superview.trailingAnchor) ]) // As we currently only support portrait mode (and no device rotation), we can safely use `bounds.size.width` here. let screenWidth = superview.bounds.size.width applyNotchMask(for: screenWidth) } // MARK: - Private methods private func applyNotchMask(for screenWidth: CGFloat) { // We always center the notch in the middle of the screen. let leftNotchPoint = (screenWidth - Config.notchWidth) / 2 let rightNotchPoint = (screenWidth + Config.notchWidth) / 2 let smallCircleDiameter: CGFloat = 2 * Config.smallCircleRadius let bezierPath = UIBezierPath() bezierPath.move(to: CGPoint(x: 0, y: 0)) // Draw line to small-circle left to `leftNotchPoint`. bezierPath.addLineTo(x: leftNotchPoint - Config.smallCircleRadius, y: 0) // Draw the small circle left to the `leftNotchPoint`. // See for the definition of the // angles in the default coordinate system. bezierPath.addArc(withCenter: CGPoint(x: leftNotchPoint - Config.smallCircleRadius, y: Config.smallCircleRadius), radius: Config.smallCircleRadius, startAngle: -CGFloat.pi / 2, endAngle: 0, clockwise: true) // We're moving the the large-circles a bit up. // This simulates the "\" and "/" line between the large and the small circles. // See: https://medium.com/tall-west/no-cutting-corners-on-the-iphone-x-97a9413b94e let verticalOffsetForLargeCircle: CGFloat = 3 // Draw the large circle right to the `leftNotchPoint`. bezierPath.addArc(withCenter: CGPoint(x: leftNotchPoint + Config.largeCircleRadius, y: smallCircleDiameter - verticalOffsetForLargeCircle), radius: Config.largeCircleRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi / 2, clockwise: false) // Draw line to large-circle underneath and left to `rightNotchPoint`. bezierPath.addLineTo(x: rightNotchPoint - Config.largeCircleRadius, y: smallCircleDiameter + Config.largeCircleRadius - verticalOffsetForLargeCircle) // Draw the large circle left to the `rightNotchPoint`. bezierPath.addArc(withCenter: CGPoint(x: rightNotchPoint - Config.largeCircleRadius, y: smallCircleDiameter - verticalOffsetForLargeCircle), radius: Config.largeCircleRadius, startAngle: CGFloat.pi / 2, endAngle: 0, clockwise: false) // Draw the small circle right to the `rightNotchPoint`. bezierPath.addArc(withCenter: CGPoint(x: rightNotchPoint + Config.smallCircleRadius, y: Config.smallCircleRadius), radius: Config.smallCircleRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi + CGFloat.pi / 2, clockwise: true) // Draw line to the end of the screen. bezierPath.addLineTo(x: screenWidth, y: 0) // And all the way back. Therefore we always have to offset the given `height` by the user. // We need to align the height with the "basic" `GradientLoadingBar`, and therefore add one point here. let height = self.height + 1 // Start by moving down at the end of the screen. bezierPath.addLineTo(x: screenWidth, y: height) // Have the small-circle at the bottom only one third of the size, produced visually better results. let bottomPathSmallCircleRadius = Config.smallCircleRadius / 3 // Draw line to small-circle right to `rightNotchPoint`. bezierPath.addLineTo(x: rightNotchPoint + bottomPathSmallCircleRadius + height, y: height) // Draw the small circle right to the `rightNotchPoint`. // We're offsetting the center-point with the given height here. bezierPath.addArc(withCenter: CGPoint(x: rightNotchPoint + bottomPathSmallCircleRadius + height, y: bottomPathSmallCircleRadius + height), radius: bottomPathSmallCircleRadius, startAngle: -CGFloat.pi / 2, endAngle: -CGFloat.pi, clockwise: false) // Moving the bottom-line just a tiny bit down here produced a visually more equal height for the gradient-view underneath // the smartphone-frame in the ears and the notch. let bottomVerticalOffsetForLargeCircle: CGFloat = 0.5 // Draw the large circle left to the `rightNotchPoint`. // We're using the same center-point as the large-circle above, but with a larger radius here. bezierPath.addArc(withCenter: CGPoint(x: rightNotchPoint - Config.largeCircleRadius, y: smallCircleDiameter - verticalOffsetForLargeCircle + bottomVerticalOffsetForLargeCircle), radius: Config.largeCircleRadius + height, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) // Draw line to large-circle underneath and right to `leftNotchPoint`. bezierPath.addLineTo(x: leftNotchPoint + Config.largeCircleRadius + height, y: smallCircleDiameter + Config.largeCircleRadius - verticalOffsetForLargeCircle + height + bottomVerticalOffsetForLargeCircle) // Draw the large circle right to the `leftNotchPoint`. // We're using the same center-point as the large-circle above, but with a larger radius here. bezierPath.addArc(withCenter: CGPoint(x: leftNotchPoint + Config.largeCircleRadius, y: smallCircleDiameter - verticalOffsetForLargeCircle + bottomVerticalOffsetForLargeCircle), radius: Config.largeCircleRadius + height, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true) // Draw the small circle left to the `leftNotchPoint`. // We're offsetting the center-point with the given height here. bezierPath.addArc(withCenter: CGPoint(x: leftNotchPoint - bottomPathSmallCircleRadius - height, y: bottomPathSmallCircleRadius + height), radius: bottomPathSmallCircleRadius, startAngle: 0, endAngle: -CGFloat.pi / 2, clockwise: false) // Draw line to the beginning of the screen. bezierPath.addLineTo(x: 0, y: height) bezierPath.close() let shapeLayer = CAShapeLayer() shapeLayer.path = bezierPath.cgPath if #available(iOS 13.0, *) { shapeLayer.cornerCurve = .continuous } gradientActivityIndicatorView.layer.mask = shapeLayer } } // MARK: - Helpers private extension UIBezierPath { // swiftlint:disable:next identifier_name func addLineTo(x: CGFloat, y: CGFloat) { addLine(to: CGPoint(x: x, y: y)) } }