Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/DebugUI/LineWrappingStackViewTestController.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

#if USE_DEBUG_UI

import Foundation
import SignalUI
public import UIKit

public class LineWrappingStackViewTestController: UIViewController {

    override public func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .white

        let numLabelsLabel = UILabel()
        numLabelsLabel.textAlignment = .center
        numLabelsLabel.text = "# of labels"
        let numLinesLabel = UILabel()
        numLinesLabel.textAlignment = .center
        numLinesLabel.text = "# of lines per label"
        let numCharactersLabel = UILabel()
        numCharactersLabel.textAlignment = .center
        numCharactersLabel.adjustsFontSizeToFitWidth = true
        numCharactersLabel.text = "# of characters per label"
        let labelWidthConstraintLabel = UILabel()
        labelWidthConstraintLabel.textAlignment = .center
        labelWidthConstraintLabel.adjustsFontSizeToFitWidth = true
        labelWidthConstraintLabel.text = "Constrained label width"
        let leadingIconLabel = UILabel()
        leadingIconLabel.textAlignment = .center
        leadingIconLabel.adjustsFontSizeToFitWidth = true
        leadingIconLabel.text = "Show leading icon"
        let trailingIconLabel = UILabel()
        trailingIconLabel.textAlignment = .center
        trailingIconLabel.adjustsFontSizeToFitWidth = true
        trailingIconLabel.text = "Show trailing icon"
        let overflowLeadingIconLabel = UILabel()
        overflowLeadingIconLabel.textAlignment = .center
        overflowLeadingIconLabel.adjustsFontSizeToFitWidth = true
        overflowLeadingIconLabel.text = "Overflow leading icon"
        let overflowTrailingIconLabel = UILabel()
        overflowTrailingIconLabel.textAlignment = .center
        overflowTrailingIconLabel.adjustsFontSizeToFitWidth = true
        overflowTrailingIconLabel.text = "Overflow trailing icon"

        let slidersStack = UIStackView()
        slidersStack.axis = .vertical
        slidersStack.spacing = 12
        slidersStack.alignment = .fill
        slidersStack.distribution = .equalSpacing

        for (hViews, distribution) in [
            ([numLabelsLabel, numLinesLabel], UIStackView.Distribution.fillEqually),
            ([numLabelsSlider, numLinesSlider], UIStackView.Distribution.fillEqually),
            ([numCharactersLabel, labelWidthConstraintLabel], UIStackView.Distribution.fillEqually),
            ([numCharactersSlider, labelWidthConstraintSlider], UIStackView.Distribution.fillEqually),
            ([leadingIconLabel, trailingIconLabel], UIStackView.Distribution.fillEqually),
            ([showLeadingIconSwitch, showTrailingIconSwitch], UIStackView.Distribution.equalCentering),
            ([overflowLeadingIconLabel, overflowTrailingIconLabel], UIStackView.Distribution.fillEqually),
            ([overflowLeadingIconSwitch, overflowTrailingIconSwitch], UIStackView.Distribution.equalCentering),
        ] {
            slidersStack.addArrangedSubview({
                let hStack = UIStackView()
                hStack.axis = .horizontal
                hStack.spacing = 12
                hStack.alignment = .center
                hStack.distribution = distribution
                hViews.forEach { hStack.addArrangedSubview($0) }
                return hStack
            }())
        }

        view.addSubview(slidersStack)
        view.addSubview(outerStackView)

        slidersStack.autoPinEdge(toSuperviewEdge: .left, withInset: 12)
        slidersStack.autoPinEdge(toSuperviewEdge: .right, withInset: 12)
        slidersStack.autoPinEdge(toSuperviewEdge: .top, withInset: 20)

        outerStackView.axis = .horizontal
        outerStackView.distribution = .fill
        outerStackView.spacing = 8
        outerStackView.alignment = .fill

        outerStackView.autoPinEdge(.left, to: .left, of: view, withOffset: 12)
        outerStackView.autoPinEdge(.right, to: .right, of: view, withOffset: -12)
        outerStackView.autoPinEdge(.top, to: .bottom, of: slidersStack, withOffset: 20)

        outerStackView.addArrangedSubview(overflowStackView)
        overflowStackView.layer.borderWidth = 1
        overflowStackView.layer.borderColor = UIColor.blue.cgColor
        overflowStackView.addArrangedSubview(leadingIcon)
        overflowStackView.addArrangedSubview(trailingIcon)

        render()
    }

    static let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ante augue, dapibus quis pretium nec, tincidunt ac odio. Vestibulum ullamcorper efficitur nibh, eget mollis sem blandit eget. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla ac mattis dolor, quis tempor orci. Donec quis augue rhoncus, viverra odio quis, tincidunt quam. Sed varius purus sit amet suscipit tincidunt. Integer eu lectus in odio euismod consequat."

    private lazy var outerStackView = UIStackView()
    private lazy var overflowStackView = LineWrappingStackView()

    private var numLabels = 1
    private var numLines = 1
    private var numCharacters = 10
    private lazy var labelWidthConstraint = view.bounds.width - 24
    private var labelWidthConstraints = [NSLayoutConstraint]()

    private var labels = [UILabel]()

    private lazy var leadingIcon: UIImageView = {
        let imageView = UIImageView(image: .init(named: "person-circle"))
        imageView.contentMode = .scaleAspectFit
        imageView.autoSetDimensions(to: .square(24))
        imageView.isHidden = true
        return imageView
    }()

    private lazy var trailingIcon: UIImageView = {
        let imageView = UIImageView(image: .init(named: "x-bold"))
        imageView.contentMode = .scaleAspectFit
        imageView.autoSetDimensions(to: .square(24))
        imageView.isHidden = true
        return imageView
    }()

    func render() {
        while labels.count > numLabels {
            labels.popLast().map { overflowStackView.removeArrangedSubview($0) }
            _ = labelWidthConstraints.popLast()
        }
        while labels.count < numLabels {
            let label = UILabel()
            label.layer.borderWidth = 1
            label.layer.borderColor = UIColor.red.cgColor
            overflowStackView.addArrangedSubview(label, atIndex: overflowStackView.arrangedSubviews.count - 1)
            labels.append(label)
            let constraint = label.widthAnchor.constraint(lessThanOrEqualToConstant: labelWidthConstraint)
            constraint.isActive = true
            constraint.priority = .required
            labelWidthConstraints.append(constraint)
        }

        let text = String(Self.loremIpsum.prefix(numCharacters))

        labels.forEach {
            $0.numberOfLines = numLines
            $0.text = text
        }
        labelWidthConstraints.forEach({
            $0.constant = labelWidthConstraint
            if labelWidthConstraint >= view.bounds.width - 24 {
                $0.isActive = false
            } else {
                $0.isActive = true
            }
        })
        overflowStackView.invalidateIntrinsicContentSize()
        overflowStackView.setNeedsLayout()
    }

    private lazy var numLabelsSlider: UISlider = {
        let slider = UISlider()
        slider.minimumValue = 1
        slider.maximumValue = 5
        slider.value = 1
        slider.addTarget(self, action: #selector(didChangeNumLabels), for: .valueChanged)
        return slider
    }()

    @objc
    private func didChangeNumLabels() {
        numLabels = Int(round(numLabelsSlider.value))
        render()
    }

    private lazy var numLinesSlider: UISlider = {
        let slider = UISlider()
        slider.minimumValue = 0
        slider.maximumValue = 5
        slider.value = 1
        slider.addTarget(self, action: #selector(didChangeNumLines), for: .valueChanged)
        return slider
    }()

    @objc
    private func didChangeNumLines() {
        numLines = Int(round(numLinesSlider.value))
        render()
    }

    private lazy var numCharactersSlider: UISlider = {
        let slider = UISlider()
        slider.minimumValue = 1
        slider.maximumValue = Float(Self.loremIpsum.count)
        slider.value = 10
        slider.addTarget(self, action: #selector(didChangeNumCharacters), for: .valueChanged)
        return slider
    }()

    @objc
    private func didChangeNumCharacters() {
        numCharacters = Int(round(numCharactersSlider.value))
        render()
    }

    private lazy var labelWidthConstraintSlider: UISlider = {
        let slider = UISlider()
        slider.minimumValue = 0
        slider.maximumValue = Float(view.bounds.width - 24)
        slider.value = Float(view.bounds.width - 24)
        slider.addTarget(self, action: #selector(didChangeLabelWidthConstraint), for: .valueChanged)
        return slider
    }()

    @objc
    private func didChangeLabelWidthConstraint() {
        labelWidthConstraint = CGFloat(labelWidthConstraintSlider.value)
        render()
    }

    private lazy var showLeadingIconSwitch: UISwitch = {
        let switchView = UISwitch()
        switchView.isOn = false
        switchView.addTarget(self, action: #selector(didChangeLeadingIconHidden), for: .valueChanged)
        return switchView
    }()

    @objc
    private func didChangeLeadingIconHidden() {
        leadingIcon.isHidden = !showLeadingIconSwitch.isOn
        render()
    }

    private lazy var showTrailingIconSwitch: UISwitch = {
        let switchView = UISwitch()
        switchView.isOn = false
        switchView.addTarget(self, action: #selector(didChangeTrailingIconHidden), for: .valueChanged)
        return switchView
    }()

    @objc
    private func didChangeTrailingIconHidden() {
        trailingIcon.isHidden = !showTrailingIconSwitch.isOn
        render()
    }

    private lazy var overflowLeadingIconSwitch: UISwitch = {
        let switchView = UISwitch()
        switchView.isOn = true
        switchView.addTarget(self, action: #selector(didChangeLeadingIconOverflow), for: .valueChanged)
        return switchView
    }()

    @objc
    private func didChangeLeadingIconOverflow() {
        if overflowLeadingIconSwitch.isOn {
            outerStackView.removeArrangedSubview(leadingIcon)
            overflowStackView.addArrangedSubview(leadingIcon, atIndex: 0)
        } else {
            overflowStackView.removeArrangedSubview(leadingIcon)
            outerStackView.insertArrangedSubview(leadingIcon, at: 0)
        }
        render()
    }

    private lazy var overflowTrailingIconSwitch: UISwitch = {
        let switchView = UISwitch()
        switchView.isOn = true
        switchView.addTarget(self, action: #selector(didChangeTrailingIconOverflow), for: .valueChanged)
        return switchView
    }()

    @objc
    private func didChangeTrailingIconOverflow() {
        if overflowTrailingIconSwitch.isOn {
            outerStackView.removeArrangedSubview(trailingIcon)
            overflowStackView.addArrangedSubview(trailingIcon)
        } else {
            overflowStackView.removeArrangedSubview(trailingIcon)
            outerStackView.addArrangedSubview(trailingIcon)
        }
        render()
    }
}

#endif