skip to Main Content

I’m using a a bezier path to draw any shape on Image view. When the touch ends , i’ve closed the path to get the shape .. now i want to achieve the blank area that is inside the bezier path so that i can replace it with another image later on. Rest of the image remains same.

By "Replace with another image " I mean ,I want to assign a new image to the cropped portion or the black hole whatever,.. The result will be like a sticker over an imageView with the path drawn by the user.

I want to crop the content inside the closed path so that when I assign a new image to the path, it’ll resize and adjust into the closed path. Please do not use masking in the new assigned image. I’ll have to create a method to resize it’s frame, but that too inside the bezierpath. I am attaching the image of desired result too.

The background image

1st User draws a bezier path

When path ends a new image gets assigned to the closed path with the exact frame as closed path.

2nd grass image gets assigned to path and adjusts its frame

The frame can then be resized by the user. But the grass image shouldn’t move outside the path.

3rd resizable frame of grass image inside path

I hope I’m making sense here.

2

Answers


  1. We’re missing a lot of detail about your ultimate goal, but this may get you on your way…

    Instead of trying to "cut a hole and fill it" we can:

    • overlay the "grass" image in an image view on top of the "street" image
    • start with an empty path for the grass "mask" so it is completely clear
    • draw the path on the street image
    • when the user stops drawing, close the path and set it as the mask-layer path on the grass image view

    So, the view hierarchy looks like this (with no mask set on the grass):

    enter image description here

    when we set a path for the grass layer mask, it looks like this:

    enter image description here

    When running and the user is drawing:

    enter image description here

    The user stops drawing, we close the path, and apply it to the grass view:

    enter image description here

    Sample code to see that in action…

    // FillImageView class – this will hold the "grass" image, and will apply a layer mask:

    class FillImageView: UIImageView {
        
        public var userPath: UIBezierPath = UIBezierPath() {
            didSet {
                maskLayer.path = userPath.cgPath
            }
        }
        private let maskLayer: CAShapeLayer = CAShapeLayer()
        
        init() {
            super.init(frame: .zero)
            commonInit()
        }
        override init(image: UIImage?) {
            super.init(image: image)
            commonInit()
        }
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            layer.mask = maskLayer
        }
        
    }
    

    // DrawImageView class – this will hold the "street" image, and handle drawing the dashed line:

    class DrawImageView: UIImageView {
        
        // closure used when drawing stops
        public var drawEnded: ((UIBezierPath) -> ())?
        
        // adjust drawing-line-width as desired
        public var lineWidth: CGFloat = 3.0 {
            didSet {
                dashedLineLayer.lineWidth = lineWidth
            }
        }
        
        public var userPath: UIBezierPath = UIBezierPath() {
            didSet {
                dashedLineLayer.path = userPath.cgPath
            }
        }
        private let dashedLineLayer: CAShapeLayer = CAShapeLayer()
        
        init() {
            super.init(frame: .zero)
            commonInit()
        }
        override init(image: UIImage?) {
            super.init(image: image)
            commonInit()
        }
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            
            isUserInteractionEnabled = true
            
            dashedLineLayer.fillColor = UIColor.clear.cgColor
            dashedLineLayer.strokeColor = UIColor.white.cgColor
            dashedLineLayer.lineWidth = lineWidth
            dashedLineLayer.lineDashPattern = [16, 16]
            
            layer.addSublayer(dashedLineLayer)
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            let currentPoint = touch.location(in: self)
            userPath.move(to: currentPoint)
        }
        
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            let currentPoint = touch.location(in: self)
            // add line to our maskPath
            userPath.addLine(to: currentPoint)
            // update the mask layer path
            dashedLineLayer.path = userPath.cgPath
        }
        
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            // close the path
            userPath.close()
            // update the mask
            dashedLineLayer.path = userPath.cgPath
            // closure
            drawEnded?(userPath)
        }
        
    }
    

    // DrawMaskView class – manages the DrawImageView and the FillImageView:

    class DrawMaskView: UIView {
        
        public var drawImage: UIImage? {
            didSet {
                drawImageView.image = drawImage
            }
        }
        public var fillImage: UIImage? {
            didSet {
                fillImageView.image = fillImage
            }
        }
        
        private let drawImageView = DrawImageView()
        private let fillImageView = FillImageView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            [drawImageView, fillImageView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                addSubview(v)
            }
            NSLayoutConstraint.activate([
                drawImageView.topAnchor.constraint(equalTo: topAnchor),
                drawImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
                drawImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
                drawImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
    
                fillImageView.topAnchor.constraint(equalTo: topAnchor),
                fillImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
                fillImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
                fillImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
            
            drawImageView.drawEnded = { [weak self] pth in
                guard let self = self else { return }
                fillImageView.userPath = pth
            }
        }
        
        public func reset() {
            drawImageView.userPath = UIBezierPath()
            fillImageView.userPath = UIBezierPath()
        }
        
    }
    

    // simple example view controller – uses images named "street" and "grass":

    class ExampleVC: UIViewController {
        
        let testView = DrawMaskView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            guard let drawImg = UIImage(named: "street"),
                  let fillImg = UIImage(named: "grass")
            else {
                fatalError("Could not load images!!!")
            }
            
            testView.drawImage = drawImg
            testView.fillImage = fillImg
            
            testView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(testView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                testView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -120.0),
            ])
            
            // let's add a Reset button at the bottom
            let btn = UIButton()
            btn.backgroundColor = .systemBlue
            btn.setTitleColor(.white, for: .normal)
            btn.setTitleColor(.lightGray, for: .highlighted)
            btn.setTitle("Reset", for: [])
            btn.layer.cornerRadius = 6
            
            btn.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(btn)
    
            NSLayoutConstraint.activate([
                btn.topAnchor.constraint(equalTo: testView.bottomAnchor, constant: 20.0),
                btn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
                btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
                btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            ])
    
            btn.addTarget(self, action: #selector(reset(_:)), for: .touchUpInside)
        }
        
        @objc func reset(_ sender: Any?) {
            testView.reset()
        }
        
    }
    
    Login or Signup to reply.
  2. To achieve this, you can follow these steps:

    Create a Mask Image from Bezier Path:
    After the user completes drawing the path, you can create a mask image from the Bezier path. This mask image will have a transparent area inside the path and opaque outside.

    // Assuming `bezierPath` is your completed UIBezierPath
    let maskLayer = CAShapeLayer()
    maskLayer.path = bezierPath.cgPath
    
    let maskImage = UIImage.image(fromLayer: maskLayer)
    
    

    Apply the Mask to the Original Image:
    Apply the mask image to the original image so that you have the content of the original image inside the user-drawn path and the rest outside the path.

    let maskedImage = originalImage.applyMask(maskImage: maskImage)
    

    Overlay the Result with Another Image:
    Now you can overlay the maskedImage with another image (your sticker) to achieve the desired effect.

    let overlayedImage = maskedImage.overlayWith(image: stickerImage)
    
    

    Here are the helper extensions and methods you can use:

    extension UIImage {
        func applyMask(maskImage: UIImage) -> UIImage {
            UIGraphicsBeginImageContextWithOptions(size, false, scale)
            let maskRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
            
            guard let context = UIGraphicsGetCurrentContext() else {
                return self
            }
            
            context.translateBy(x: 0, y: size.height)
            context.scaleBy(x: 1.0, y: -1.0)
            
            context.clip(to: maskRect, mask: maskImage.cgImage!)
            draw(in: maskRect)
            
            let maskedImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            return maskedImage ?? self
        }
        
        func overlayWith(image: UIImage) -> UIImage {
            UIGraphicsBeginImageContextWithOptions(size, false, scale)
            
            draw(in: CGRect(origin: CGPoint.zero, size: size))
            image.draw(in: CGRect(origin: CGPoint.zero, size: size))
            
            let overlayedImage = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            return overlayedImage ?? self
        }
    }
    
    extension UIImage {
        static func image(fromLayer layer: CALayer) -> UIImage? {
            UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0)
            guard let context = UIGraphicsGetCurrentContext() else {
                return nil
            }
            
            layer.render(in: context)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            return image
        }
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search