iosswiftcore-graphicscalayermasking

how to draw border to a masked image using swift?


I have created below masked image and I have masked image like this

image imag2

Here is my code

//
//  Collage_Layout_1.swift
//  Snapcial
//
//  Created by Jecky Modi on 15/02/25.
//


class Collage_Layout_1: CollageLayoutBaseView {
    var maskedImage: String = ""
    var originalImage = UIImage()
    private let maskImageView = UIImageView()
    var viewFrame : CGRect = .zero
    //    private var originalImageCenter: CGPoint?
    
    init(frame: CGRect, maskedImage: String, originalImage: UIImage) {
        super.init(frame: frame)
        self.maskedImage = maskedImage
        self.originalImage = originalImage
        self.viewFrame = frame
    }
    
    override func setUpCollage() {
        let maskedContainer = self.arrViews[0].containerView
        maskedContainer.tag = 100
                
        let maskImage = UIImage(named: maskedImage)?.withRenderingMode(.alwaysTemplate)
        maskImageView.translatesAutoresizingMaskIntoConstraints = false
        maskImageView.image = maskImage
        maskImageView.contentMode = .scaleAspectFit
        maskImageView.tintColor = .clear
        addSubview(maskImageView)       // Keep the mask image visible
        
        // Create a mask layer from the black shape
        let maskLayer = CALayer()
        maskLayer.contents = maskImage?.cgImage
        maskLayer.frame = self.viewFrame
        maskLayer.contentsGravity = .resizeAspect
        
        maskedContainer.layer.mask = maskLayer
        maskedContainer.clipsToBounds = true
        maskedContainer.backgroundColor = .clear
        self.backgroundColor = .clear
        
        addSubview(maskedContainer)     // Add container with mask
        
        
        NSLayoutConstraint.activate([
            maskImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            maskImageView.topAnchor.constraint(equalTo: self.topAnchor),
            maskImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            maskImageView.bottomAnchor.constraint(equalTo: self.bottomAnchor),

            maskedContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            maskedContainer.topAnchor.constraint(equalTo: self.topAnchor),
            maskedContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            maskedContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor),
        ])
        

        self.layoutIfNeeded()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

I want to add borders to my masked image that border should take shape of mask.

Any help would be appreciated.


Solution

  • Based on this answer: How do I get a Path from an Image( )?

    Code stripped-down a bit so we can see how we're using a UIImage (the black leaf with transparent background), instead of a generating a SF Symbol image.

    enum ContourError: Error {
        case cgImageFailed
        case noContour
    }
    
    extension CGPath {
        static func imgContourPath(image: UIImage) throws -> CGPath {
            guard let cgImage = image.cgImage else { throw ContourError.cgImageFailed }
            return try cgImage.contourPath(size: image.size)
        }
    }
    
    extension CGImage {
        func contourPath(size: CGSize? = nil, contrastAdjustment: Float = 2, maximumImageDimension: Int? = nil) throws -> CGPath {
            let size = size ?? CGSize(width: width, height: height)
            
            let contourRequest = VNDetectContoursRequest()
            contourRequest.maximumImageDimension = maximumImageDimension ?? Int(max(size.width, size.height))
            contourRequest.contrastAdjustment = contrastAdjustment
            let requestHandler = VNImageRequestHandler(cgImage: self, options: [:])
            try requestHandler.perform([contourRequest])
            var transform = CGAffineTransform(translationX: 0, y: CGFloat(size.height))
                .scaledBy(x: CGFloat(size.width), y: -CGFloat(size.height))
            guard let path = contourRequest.results?.first?.normalizedPath.mutableCopy(using: &transform) else {
                throw ContourError.noContour
            }
            return path
        }
    }
    
    extension UIImage {
        func withBackground(_ background: UIColor) -> UIImage? {
            let format = UIGraphicsImageRendererFormat()
            format.scale = scale
            let rect = CGRect(origin: .zero, size: size)
            return UIGraphicsImageRenderer(bounds: rect, format: format).image { _ in
                background.setFill()
                UIBezierPath(rect: rect).fill()
                draw(in: rect)
            }
        }
    }
    

    Using this 1200x1200 "image to mask":

    and this 1200x1200 "mask image" (with transparent background):

    We can generate a Path to use as both the Mask and the Border / Outline:

    class ExampleVC: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            
            guard let img = UIImage(named: "maskImage"),
                  let origImage = UIImage(named: "imageToMask")
            else { fatalError("Could not load images") }
            
            // let's use a 300x300 image view
            let r: CGRect = .init(origin: .zero, size: .init(width: 300, height: 300)).offsetBy(dx: 20, dy: 50)
            
            let imgView = UIImageView(image: origImage)
            imgView.frame = r
            view.addSubview(imgView)
            
            let szOrig = img.size
            
            // for Contour detection, we want a black shape on a white background
            //  NOT transparent background
            guard let wImage = img.withBackground(.white) else { fatalError("Failed to create white background image") }
            
            if let cp = try? CGPath.imgContourPath(image: wImage) {
    
                // the Contour Path matches the image size...
                //  we want to scale the path to fit the view size
                let bz = UIBezierPath(cgPath: cp)
                let scaleX = r.width / szOrig.width
                let scaleY = r.height / szOrig.height
                bz.apply(CGAffineTransform(scaleX: scaleX, y: scaleY))
    
                // pathed shape layer to mask the image view
                let mskLayer = CAShapeLayer()
                mskLayer.path = bz.cgPath
    
                // pathed shape layer for the border / outline
                let sl = CAShapeLayer()
                sl.strokeColor = UIColor.red.cgColor
                sl.fillColor = nil
                sl.lineWidth = 3
                sl.path = bz.cgPath
    
                // mask the image view
                imgView.layer.mask = mskLayer
                
                // add the border / outline layer
                imgView.layer.addSublayer(sl)
            }
            
        }
        
    }
    

    Giving us this output:

    output image