ephread/Instructions

View on GitHub
Sources/Instructions/Extra/Default Views/CoachMarkBodyDefaultView.swift

Summary

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

import UIKit

// MARK: - Main Class
/// A concrete implementation of the coach mark body view and the
/// default one provided by the library.
public class CoachMarkBodyDefaultView: UIControl,
                                       CoachMarkBodyView {
    // MARK: Overridden Properties
    public override var isHighlighted: Bool {
        didSet {
            bodyBackground.isHighlighted = isHighlighted
            highlightArrowDelegate?.highlightArrow(isHighlighted)
        }
    }

    // MARK: Public Properties
    public var nextControl: UIControl? {
        return self
    }

    public lazy var nextLabel: UILabel = makeNextLabel()
    public lazy var hintLabel: UITextView = makeHintTextView()
    public lazy var separator: UIView = makeSeparator()
    private var nextLabelPosition: CoachMarkNextLabelPosition = .trailing
    private let spacing: CGFloat = 18

    public var background: CoachMarkBodyBackgroundStyle {
        get { return bodyBackground }
        set { bodyBackground.updateValues(from: newValue) }
    }

    // MARK: Delegates
    public weak var highlightArrowDelegate: CoachMarkBodyHighlightArrowDelegate?

    // MARK: Private Properties
    private lazy var labelStackView: UIStackView = makeStackView()

    private var bodyBackground = CoachMarkBodyBackgroundView().preparedForAutoLayout()

    // MARK: - Initialization
    override public init(frame: CGRect) {
        super.init(frame: frame)
        initializeViewHierarchy()
    }

    public init(frame: CGRect, hintText: String, nextText: String?, nextLabelPosition: CoachMarkNextLabelPosition) {
        self.nextLabelPosition = nextLabelPosition

        super.init(frame: frame)
        initializeViewHierarchy()

        separator.isHidden = (nextText == nil)
        nextLabel.isHidden = (nextText == nil)

        nextLabel.text = nextText
        hintLabel.text = hintText
    }

    public init(frame: CGRect, nextLabelPosition: CoachMarkNextLabelPosition) {
        self.nextLabelPosition = nextLabelPosition

        super.init(frame: frame)
        initializeViewHierarchy()
    }

    convenience public init(hintText: String, nextText: String?, nextLabelPosition: CoachMarkNextLabelPosition) {
        self.init(frame: CGRect.zero, hintText: hintText, nextText: nextText, nextLabelPosition: nextLabelPosition)
    }

    convenience public init(nextLabelPosition: CoachMarkNextLabelPosition) {
        self.init(frame: CGRect.zero, nextLabelPosition: nextLabelPosition)
    }

    required public init?(coder aDecoder: NSCoder) {
        fatalError(ErrorMessage.Fatal.doesNotSupportNSCoding)
    }
}

// MARK: - Private Methods
private extension CoachMarkBodyDefaultView {
    func initializeViewHierarchy() {
        backgroundColor = .clear
        translatesAutoresizingMaskIntoConstraints = false

        initializeAccessibilityIdentifier()

        addSubview(bodyBackground)
        addSubview(labelStackView)

        bodyBackground.fillSuperview()
        labelStackView.fillSuperview(insets: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15))

        switch nextLabelPosition {
        case .trailing:
            labelStackView.addArrangedSubview(hintLabel)
            labelStackView.addArrangedSubview(separator)
            labelStackView.addArrangedSubview(nextLabel)
            separator.heightAnchor.constraint(equalTo: labelStackView.heightAnchor,
                                              constant: -10).isActive = true
        case .leading:
            labelStackView.addArrangedSubview(nextLabel)
            labelStackView.addArrangedSubview(separator)
            labelStackView.addArrangedSubview(hintLabel)
            separator.heightAnchor.constraint(equalTo: labelStackView.heightAnchor,
                                              constant: -10).isActive = true
        case .topTrailing:
            labelStackView.addSubview(hintLabel)
            labelStackView.addSubview(nextLabel)

            NSLayoutConstraint.activate([
                nextLabel.topAnchor.constraint(equalTo: topAnchor, constant: spacing),
                nextLabel.bottomAnchor.constraint(equalTo: hintLabel.topAnchor, constant: -spacing),
                nextLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -spacing),
                hintLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -spacing),
                hintLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: spacing),
                hintLabel.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -spacing)
            ])
        case .topLeading:
            labelStackView.addSubview(hintLabel)
            labelStackView.addSubview(nextLabel)

            NSLayoutConstraint.activate([
                nextLabel.topAnchor.constraint(equalTo: topAnchor, constant: spacing),
                nextLabel.bottomAnchor.constraint(equalTo: hintLabel.topAnchor, constant: -spacing),
                nextLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: spacing),
                hintLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -spacing),
                hintLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: spacing),
                hintLabel.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -spacing)
            ])
        case .bottomTrailing:
            labelStackView.addSubview(hintLabel)
            labelStackView.addSubview(nextLabel)

            NSLayoutConstraint.activate([
                hintLabel.topAnchor.constraint(equalTo: topAnchor, constant: spacing),
                hintLabel.bottomAnchor.constraint(equalTo: nextLabel.topAnchor, constant: -spacing),
                hintLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: spacing),
                hintLabel.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -spacing),
                nextLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -spacing),
                nextLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -spacing)
            ])
        case .bottomLeading:
            labelStackView.addSubview(hintLabel)
            labelStackView.addSubview(nextLabel)

            NSLayoutConstraint.activate([
                hintLabel.topAnchor.constraint(equalTo: topAnchor, constant: spacing),
                hintLabel.bottomAnchor.constraint(equalTo: nextLabel.topAnchor, constant: -spacing),
                hintLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: spacing),
                hintLabel.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -spacing),
                nextLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -spacing),
                nextLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: spacing)
            ])
        }
    }

    func initializeAccessibilityIdentifier() {
        accessibilityIdentifier = AccessibilityIdentifiers.coachMarkBody
        nextLabel.accessibilityIdentifier = AccessibilityIdentifiers.coachMarkNext
        hintLabel.accessibilityIdentifier = AccessibilityIdentifiers.coachMarkHint
    }

    // MARK: Builders
    func makeHintTextView() -> UITextView {
        let textView = UITextView().preparedForAutoLayout()

        textView.textAlignment = .left
        textView.textColor = InstructionsColor.coachMarkLabel
        textView.font = UIFont.systemFont(ofSize: 15.0)

        textView.backgroundColor = .clear

        textView.textContainerInset = .zero
        textView.textContainer.lineFragmentPadding = 0

        textView.isEditable = false
        textView.isScrollEnabled = false
        textView.isUserInteractionEnabled = false

        if #available(iOS 15.0, *) {
            textView.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh - 1,
                                                             for: .horizontal)
        } else {
            textView.setContentCompressionResistancePriority(UILayoutPriority.required,
                                                             for: .horizontal)
        }

        textView.setContentCompressionResistancePriority(UILayoutPriority.required,
                                                         for: .vertical)
        textView.setContentHuggingPriority(UILayoutPriority.defaultHigh,
                                           for: .horizontal)
        textView.setContentHuggingPriority(UILayoutPriority.defaultHigh,
                                           for: .vertical)

        return textView
    }

    func makeNextLabel() -> UILabel {
        let label = UILabel().preparedForAutoLayout()

        label.textAlignment = .center
        label.textColor = InstructionsColor.coachMarkLabel
        label.font = UIFont.systemFont(ofSize: 17.0)

        label.isUserInteractionEnabled = false

        if #available(iOS 15.0, *) {
            label.setContentCompressionResistancePriority(UILayoutPriority.defaultHigh,
                                                          for: .horizontal)
            label.setContentHuggingPriority(UILayoutPriority.init(765),
                                            for: .horizontal)
        } else {
            label.setContentCompressionResistancePriority(UILayoutPriority.required,
                                                          for: .horizontal)
            label.setContentHuggingPriority(UILayoutPriority.defaultLow,
                                            for: .horizontal)
        }

        label.setContentCompressionResistancePriority(UILayoutPriority.required,
                                                      for: .vertical)
        label.setContentHuggingPriority(UILayoutPriority.defaultLow,
                                        for: .vertical)

        return label
    }

    func makeSeparator() -> UIView {
        let separator = UIView().preparedForAutoLayout()

        separator.backgroundColor = InstructionsColor.coachMarkLabel

        separator.widthAnchor.constraint(equalToConstant: 1).isActive = true

        return separator
    }

    func makeStackView() -> UIStackView {
        let stackView = UIStackView().preparedForAutoLayout()
        stackView.axis = .horizontal
        stackView.spacing = 10
        stackView.isUserInteractionEnabled = false
        stackView.alignment = .center

        return stackView
    }
}