Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/ConversationView/CellViews/CVDimmableView.swift
1 views
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

protocol CVDimmableView: ManualLayoutView {
    /// Can be a dynamic color that will get resolved at rendering time.
    var dimmerColor: UIColor { get set }

    /// When set to `true` dimmer layer will be displayed on top of all subviews.
    /// Applicable when content needs to be dimmed too (eg media bubbles).
    var dimsContent: Bool { get set }

    /// If `dimsContent` is `false` the dimmer layer
    /// will be placed above the sublayer retuned by this method.
    /// If `nil` is returned, dimmerLayer will be placed on top of all sublayers.
    var backgroundLayer: CALayer? { get }

    /// - Parameter animationDuration: Duration for fade-in and fade-out animations.
    /// - Parameter dimDuration: How long the dimmer stays visible.
    func performDimmingAnimation(animationDuration: TimeInterval, dimDuration: TimeInterval)
}

extension CVDimmableView {

    private func sublayerIndexForDimmerLayer() -> UInt32 {
        if
            dimsContent == false,
            let backgroundLayer,
            let backgroundLayerIndex = layer.sublayers!.firstIndex(of: backgroundLayer)
        {
            // Just above background layer if there's one and it is part of the view hierarchy.
            return UInt32(backgroundLayerIndex) + 1
        }
        return UInt32(layer.sublayers!.count)
    }

    func performDimmingAnimation(animationDuration: TimeInterval, dimDuration: TimeInterval) {
        let dimmerLayerIndex = sublayerIndexForDimmerLayer()
        let dimmerLayer = CALayer()
        dimmerLayer.opacity = 0
        dimmerLayer.backgroundColor = dimmerColor.resolvedColor(with: traitCollection).cgColor
        dimmerLayer.frame = layer.bounds
        layer.insertSublayer(dimmerLayer, at: dimmerLayerIndex)

        // Animate fade-in.
        let fadeIn = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
        fadeIn.fromValue = 0
        fadeIn.toValue = 1
        fadeIn.duration = animationDuration
        fadeIn.fillMode = .forwards
        fadeIn.isRemovedOnCompletion = false
        dimmerLayer.add(fadeIn, forKey: "fadeIn")

        // Schedule fade-out after delay.
        DispatchQueue.main.asyncAfter(deadline: .now() + dimDuration) {
            let fadeOut = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
            fadeOut.fromValue = 1
            fadeOut.toValue = 0
            fadeOut.duration = animationDuration
            fadeOut.fillMode = .forwards
            fadeOut.isRemovedOnCompletion = false
            fadeOut.delegate = DimAnimationDelegate(layer: dimmerLayer)
            dimmerLayer.add(fadeOut, forKey: "fadeOut")
        }
    }
}

private class DimAnimationDelegate: NSObject, CAAnimationDelegate {
    private weak var layer: CALayer?

    init(layer: CALayer) {
        self.layer = layer
    }

    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        // This is necessary to ensure proper deallocation of both CALayer and DimAnimationDelegate.
        layer?.removeFromSuperlayer()
        layer?.removeAllAnimations()
    }
}