Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/UIKitExtensions/UIKit+Image.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

public import SignalServiceKit

public extension UIImageView {

    func setImage(imageName: String) {
        guard let image = UIImage(named: imageName) else {
            owsFailDebug("Couldn't load image: \(imageName)")
            return
        }
        self.image = image
    }

    func setTemplateImage(_ templateImage: UIImage?, tintColor: UIColor) {
        guard let templateImage else {
            owsFailDebug("Missing image")
            return
        }
        self.image = templateImage.withRenderingMode(.alwaysTemplate)
        self.tintColor = tintColor
    }

    func setTemplateImageName(_ imageName: String, tintColor: UIColor) {
        guard let image = UIImage(named: imageName) else {
            owsFailDebug("Couldn't load image: \(imageName)")
            return
        }
        setTemplateImage(image, tintColor: tintColor)
    }

    class func withTemplateImage(_ templateImage: UIImage?, tintColor: UIColor) -> UIImageView {
        let imageView = UIImageView()
        imageView.setTemplateImage(templateImage, tintColor: tintColor)
        return imageView
    }

    class func withTemplateImageName(_ imageName: String, tintColor: UIColor) -> UIImageView {
        let imageView = UIImageView()
        imageView.setTemplateImageName(imageName, tintColor: tintColor)
        return imageView
    }

    /// Creates an image view with the given theme icon, tinted with the given
    /// color, and constrained to the given size if present.
    /// - Parameters:
    ///   - icon: The ``ThemeIcon`` to display.
    ///   - tintColor: The color to tint the icon
    ///   - size: The size to constrain the image to.
    ///   When `nil`, no constraints are added.
    /// - Returns: A `UIImageView` of the icon.
    class func withTemplateIcon(
        _ icon: ThemeIcon,
        tintColor: UIColor,
        constrainedTo size: CGSize? = nil,
    ) -> UIImageView {
        let imageView = UIImageView()
        imageView.setTemplateImage(Theme.iconImage(icon), tintColor: tintColor)
        if let size {
            imageView.autoSetDimensions(to: size)
        }
        return imageView
    }
}

// MARK: -

public extension UIImage {
    /// Redraw the image into a new image, with an added background color, and inset the
    /// original image by the provided insets.
    func withBackgroundColor(_ color: UIColor, insets: UIEdgeInsets = .zero) -> UIImage? {
        let bounds = CGRect(origin: .zero, size: size)
        return UIGraphicsImageRenderer(bounds: bounds).image { context in
            color.setFill()
            context.fill(bounds)
            draw(in: bounds.inset(by: insets))
        }
    }

    func normalized() -> UIImage {
        guard imageOrientation != .up else {
            return self
        }

        let format = UIGraphicsImageRendererFormat()
        format.scale = scale
        format.opaque = false
        let renderer = UIGraphicsImageRenderer(size: size, format: format)
        return renderer.image { context in
            self.draw(in: CGRect(origin: CGPoint.zero, size: size))
        }
    }

    func withTitle(
        _ title: String,
        font: UIFont,
        color: UIColor,
        maxTitleWidth: CGFloat,
        minimumScaleFactor: CGFloat,
        spacing: CGFloat,
    ) -> UIImage? {
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.font = font
        titleLabel.adjustsFontSizeToFitWidth = true
        titleLabel.minimumScaleFactor = minimumScaleFactor
        titleLabel.textAlignment = .center
        titleLabel.textColor = color
        titleLabel.numberOfLines = title.components(separatedBy: " ").count > 1 ? 2 : 1
        titleLabel.lineBreakMode = .byTruncatingTail

        let titleSize = titleLabel.textRect(forBounds: CGRect(
            origin: .zero,
            size: CGSize(
                width: maxTitleWidth,
                height: .greatestFiniteMagnitude,
            ),
        ), limitedToNumberOfLines: titleLabel.numberOfLines).size
        let additionalWidth = size.width >= titleSize.width ? 0 : titleSize.width - size.width

        var newSize = size
        newSize.height += spacing + titleSize.height
        newSize.width = max(titleSize.width, size.width)

        UIGraphicsBeginImageContextWithOptions(newSize, false, max(scale, UIScreen.main.scale))

        // Draw the image into the new image
        draw(in: CGRect(origin: CGPoint(x: additionalWidth / 2, y: 0), size: size))

        // Draw the title label into the new image
        titleLabel.drawText(in: CGRect(origin: CGPoint(
            x: size.width > titleSize.width ? (size.width - titleSize.width) / 2 : 0,
            y: size.height + spacing,
        ), size: titleSize))

        let newImage = UIGraphicsGetImageFromCurrentImageContext()

        UIGraphicsEndImageContext()

        return newImage
    }

    func withBadge(
        color: UIColor,
        badgeSize: CGSize = .square(8.5),
    ) -> UIImage {
        let newSize = CGSize(
            width: size.width + (badgeSize.width / 2.0),
            height: size.height + (badgeSize.height / 2.0),
        )

        let renderer = UIGraphicsImageRenderer(size: newSize)
        return renderer.image { context in
            // Draw the image
            draw(at: .zero)

            // Draw the badge in the top-right corner, over the image
            let badgeOrigin = CGPoint(x: size.width - badgeSize.width, y: 0)
            let badgeRect = CGRect(origin: badgeOrigin, size: badgeSize)
            let badgePath = UIBezierPath(ovalIn: badgeRect)
            color.setFill()
            badgePath.fill()
        }
        .withRenderingMode(.alwaysOriginal)
    }
}

// MARK: -

public extension UIView {

    func renderAsImage() -> UIImage {
        renderAsImage(opaque: false, scale: UIScreen.main.scale)
    }

    func renderAsImage(opaque: Bool, scale: CGFloat) -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        format.scale = scale
        format.opaque = opaque
        let renderer = UIGraphicsImageRenderer(
            bounds: self.bounds,
            format: format,
        )
        return renderer.image { context in
            self.layer.render(in: context.cgContext)
        }
    }
}