ephread/Instructions

View on GitHub
Sources/Instructions/Managers/Internal/OverlayStyleManager/TranslucentOverlayStyleManager.swift

Summary

Maintainability
A
45 mins
Test Coverage
A
100%
// Copyright (c) 2017-present Frédéric Maquin <fred@ephread.com> and contributors.
// Licensed under the terms of the MIT License.

import UIKit

class TranslucentOverlayStyleManager: OverlayStyleManager {
    // MARK: Properties
    weak var overlayView: OverlayView?

    // MARK: Private Properties
    private var onGoingTransition = false
    private let color: UIColor

    // MARK: Layer Mask related properties
    private var cutoutMaskLayer = CAShapeLayer()
    private var fullMaskLayer = CAShapeLayer()
    private lazy var overlayLayer: CALayer = {
        return self.createSublayer()
    }()

    // MARK: Initialization
    init(color: UIColor) {
        self.color = color
    }

    // MARK: OverlayStyleManager
    func viewWillTransition() {
        // Basically removes everything except the overlay itself.
        // Background color duty, handled by the sublayer, it transferred to
        // the overlay itself.
        guard let overlay = overlayView else { return }

        onGoingTransition = true
        self.overlayLayer.removeFromSuperlayer()
        overlay.backgroundColor = color
    }

    func viewDidTransition() {
        // Back to business, recreating the sublayer.
        guard let overlay = overlayView else { return }

        onGoingTransition = false

        overlayLayer = createSublayer()
        overlayLayer.frame = overlay.bounds
        overlayLayer.backgroundColor = self.color.cgColor

        overlay.holder.layer.addSublayer(overlayLayer)
        updateCutoutPath()

        overlay.backgroundColor = UIColor.clear
    }

    func showOverlay(_ show: Bool, withDuration duration: TimeInterval,
                     completion: ((Bool) -> Void)?) {
        guard let overlay = overlayView else { return }

        overlay.isHidden = false
        overlay.alpha = show ? 0.0 : overlay.alpha
        overlay.backgroundColor = .clear
        overlay.holder.backgroundColor = color

        if !show { self.overlayLayer.removeFromSuperlayer() }

        UIView.animate(withDuration: duration, animations: {
            overlay.alpha = show ? 1.0 : 0.0
        }, completion: { success in
            if show {
                self.overlayLayer.removeFromSuperlayer()
                self.overlayLayer.frame = overlay.bounds
                self.overlayLayer.backgroundColor = self.color.cgColor
                overlay.holder.layer.addSublayer(self.overlayLayer)
                overlay.holder.backgroundColor = UIColor.clear
            } else {
                self.overlayLayer.removeFromSuperlayer()
            }
            completion?(success)
        })
    }

    func showCutout(_ show: Bool, withDuration duration: TimeInterval,
                    completion: ((Bool) -> Void)?) {
        if show { updateCutoutPath() }

        CATransaction.begin()

        fullMaskLayer.opacity = show ? 0.0 : 1.0

        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = show ? 1.0 : 0.0
        animation.toValue = show ? 0.0 : 1.0
        animation.duration = duration
        animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        animation.isRemovedOnCompletion = true

        CATransaction.setCompletionBlock {
            completion?(true)
        }

        fullMaskLayer.add(animation, forKey: "opacityAnimationFade")

        CATransaction.commit()
    }

    func updateStyle(with traitCollection: UITraitCollection) {
        overlayLayer.backgroundColor = self.color.cgColor
        overlayView?.setNeedsDisplay()
    }

    // MARK: Private methods
    private func updateCutoutPath() {
        cutoutMaskLayer.removeFromSuperlayer()
        fullMaskLayer.removeFromSuperlayer()

        guard let cutoutPath = overlayView?.cutoutPath else {
            overlayLayer.mask = nil
            return
        }

        configureCutoutMask(usingCutoutPath: cutoutPath)
        configureFullMask()

        let maskLayer = CALayer()
        maskLayer.frame = overlayLayer.bounds
        maskLayer.addSublayer(self.cutoutMaskLayer)
        maskLayer.addSublayer(self.fullMaskLayer)

        overlayLayer.mask = maskLayer
    }

    private func configureCutoutMask(usingCutoutPath cutoutPath: UIBezierPath) {
        cutoutMaskLayer = CAShapeLayer()
        cutoutMaskLayer.name = "cutoutMaskLayer"
        cutoutMaskLayer.fillRule = .evenOdd
        cutoutMaskLayer.frame = overlayLayer.frame

        let cutoutMaskLayerPath = UIBezierPath()
        cutoutMaskLayerPath.append(UIBezierPath(rect: overlayLayer.bounds))
        cutoutMaskLayerPath.append(cutoutPath)

        cutoutMaskLayer.path = cutoutMaskLayerPath.cgPath
    }

    private func configureFullMask() {
        fullMaskLayer = CAShapeLayer()
        fullMaskLayer.name = "fullMaskLayer"
        fullMaskLayer.fillRule = .evenOdd
        fullMaskLayer.frame = overlayLayer.frame
        fullMaskLayer.opacity = 1.0

        let fullMaskLayerPath = UIBezierPath()
        fullMaskLayerPath.append(UIBezierPath(rect: overlayLayer.bounds))

        fullMaskLayer.path = fullMaskLayerPath.cgPath
    }

    private func createSublayer() -> CALayer {
        let layer = CALayer()
        layer.name = OverlayView.sublayerName

        return layer
    }
}