ephread/Instructions

View on GitHub
Sources/Instructions/Managers/Internal/SkipViewDisplayManager.swift

Summary

Maintainability
B
5 hrs
Test Coverage
C
73%
// Copyright (c) 2015-present Frédéric Maquin <fred@ephread.com> and contributors.
// Licensed under the terms of the MIT License.

import UIKit

/// This class deals with the layout of the "skip" view.
class SkipViewDisplayManager {
    // MARK: - Internal properties
    /// Datasource providing the constraints to use.
    weak var dataSource: CoachMarksControllerProxyDataSource?

    var presentationFashion: PresentationFashion = .window

    // MARK: - Private properties
    /// Constraints defining the position of the "Skip" view
    private var skipViewConstraints: [NSLayoutConstraint] = []

    // MARK: - Internal methods
    /// Hide the given Skip View with a fading effect.
    ///
    /// - Parameter skipView: the skip view to hide.
    /// - Parameter duration: the duration of the fade.
    func hide(skipView: (UIView & CoachMarkSkipView), duration: TimeInterval = 0) {
        if duration == 0 {
            skipView.asView?.alpha = 0.0
        } else {
            UIView.animate(withDuration: duration) { () -> Void in
                skipView.asView?.alpha = 0.0
            }
        }
    }

    /// Show the given Skip View with a fading effect.
    ///
    /// - Parameter skipView: the skip view to show.
    /// - Parameter duration: the duration of the fade.
    func show(skipView: (UIView & CoachMarkSkipView), duration: TimeInterval = 0) {
        guard let parentView = skipView.asView?.superview else {
            print(ErrorMessage.Info.skipViewNoSuperviewNotShown)
            return
        }

        let constraints = self.dataSource?.constraintsForSkipView(skipView.asView!,
                                                                  inParent: parentView)

        update(skipView: skipView, withConstraints: constraints)

        skipView.asView?.superview?.bringSubviewToFront(skipView.asView!)

        if duration == 0 {
            skipView.asView?.alpha = 1.0
        } else {
            UIView.animate(withDuration: duration) { () -> Void in
                skipView.asView?.alpha = 1.0
            }
        }
    }

    /// Update the constraints defining the location of given s view.
    ///
    /// - Parameter skipView: the skip view to position.
    /// - Parameter constraints: the constraints to use.
    func update(skipView: (UIView & CoachMarkSkipView),
                withConstraints constraints: [NSLayoutConstraint]?) {
        guard let parentView = skipView.asView?.superview else {
            print(ErrorMessage.Info.skipViewNoSuperviewNotUpdated)
            return
        }

        skipView.asView?.translatesAutoresizingMaskIntoConstraints = false
        parentView.removeConstraints(self.skipViewConstraints)

        self.skipViewConstraints = []

        if let constraints = constraints {
            self.skipViewConstraints = constraints
        } else {
            self.skipViewConstraints = defaultConstraints(for: skipView, in: parentView)
        }

        parentView.addConstraints(self.skipViewConstraints)
    }

    private func defaultConstraints(for skipView: (UIView & CoachMarkSkipView), in parentView: UIView)
    -> [NSLayoutConstraint] {
        var constraints = [NSLayoutConstraint]()

        let trailingAnchor: NSLayoutXAxisAnchor
        if #available(iOS 11.0, *) {
            trailingAnchor = parentView.safeAreaLayoutGuide.trailingAnchor
        } else {
            trailingAnchor = parentView.trailingAnchor
        }

        constraints.append(skipView.trailingAnchor.constraint(equalTo: trailingAnchor,
                                                              constant: -10))

        var topConstant: CGFloat = 0.0
        let topAnchor: NSLayoutYAxisAnchor

        switch presentationFashion {
        case .window:
            if #available(iOS 11.0, *) {
                topAnchor = parentView.safeAreaLayoutGuide.topAnchor
            } else {
                topAnchor = parentView.topAnchor
                topConstant = updateTopConstant(from: topConstant)
            }
        case .viewControllerWindow:
            if #available(iOS 11.0, *), let window = parentView.window,
               let safeAreaInsets = window.rootViewController?.view.safeAreaInsets {
                // For some reasons I don't fully understand, window.safeAreaInsets.top is correctly
                // set for the iPhone X, but not for other iPhones. That's why we have this
                // awkward "hack", whereby the top inset is added manually.
                topAnchor = parentView.topAnchor
                topConstant = safeAreaInsets.top
            } else {
                topAnchor = parentView.topAnchor
                topConstant = updateTopConstant(from: topConstant)
            }
        case .viewController:
            if #available(iOS 11.0, *) {
                topAnchor = parentView.safeAreaLayoutGuide.topAnchor
            } else {
                topAnchor = parentView.topAnchor
            }
        }

        topConstant += 2

        constraints.append(skipView.topAnchor.constraint(equalTo: topAnchor,
                                                         constant: topConstant))

        return constraints
    }

    func updateTopConstant(from original: CGFloat) -> CGFloat {
#if !INSTRUCTIONS_APP_EXTENSIONS
        if #available(iOS 13.0, *) {
            let statusBarManager = UIApplication.shared.activeScene?.statusBarManager

            if let statusBarManager = statusBarManager, !statusBarManager.isStatusBarHidden {
                return statusBarManager.statusBarFrame.size.height
            }
        } else {
            if !UIApplication.shared.isStatusBarHidden {
                return UIApplication.shared.statusBarFrame.size.height
            }
        }
#endif

        return original
    }
}