R3ptor R3ptor - 29 days ago 14
Swift Question

Draw Line and fill with UIVisualEffectView

So the drawing lines part is setup:

func drawLineFrom(_ fromPoint: CGPoint, toPoint: CGPoint) {

let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)

UIGraphicsBeginImageContextWithOptions(tempImageView.frame.size, false, UIScreen.main.scale)

let context = UIGraphicsGetCurrentContext()
tempImageView.image?.draw(in: CGRect(x: 0, y: 0, width: tempImageView.frame.size.width, height: tempImageView.frame.size.height), blendMode: .normal, alpha: 1.0)

context?.move(to: CGPoint(x: fromPoint.x, y: fromPoint.y))
context?.addLine(to: CGPoint(x: toPoint.x, y: toPoint.y))

context?.setLineCap(.round)
context?.setLineWidth(brushWidth)
context?.setStrokeColor(blurEffectView(effect: blurEffect))
context?.setBlendMode(.normal)

context!.strokePath()

var img = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.image = img
tempImageView.alpha = opacity

UIGraphicsEndImageContext()

}


It is basically using touchesBegan, touchesMoved and touchesEnded and this is where
fromPoint
and
toPoint
is coming from.

And like you can already see I'm trying to fill this line up with an UIVisualEffectView instead of a Color. Obviously my method doesn't really work.
What would be the best solution to do this?

Answer

Setup

A UIVisualEffectView doesn't really have any content by itself, rather it depends on the views below it. If you want to give the effect of drawing with a UIVisualEffect you should construct your view hierarchy that you want the impression of drawing on top of. Perhaps something like view with an image view, displaying some image, then an effect view on top of that, like:

let imageView = UIImageView(image: UIImage(named: "image.jpg"))
let blurEffect = UIBlurEffect(style: .dark)
let effectView = UIVisualEffectView(effect: blurEffect)
effectView.frame = imageView.bounds
let view = UIView(frame: imageView.bounds)
view.addSubview(imageView)
view.addSubview(effectView)

Then you'll need to snapshot the view hierarchy in this state. This will act like the view if it were completely filled in by the touches. You can do that by adding an extension on UIView:

extension UIView
{
    var snapshot: UIImage?
    {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
        self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}

And

let image = view.snapshot!

We can now replace out UIVisualEffectView with an UIImageView holding our snapshotted view hierarchy:

effectView.removeFromSuperview()
let topImageView = UIImageView(image: image)
view.addSubview(topImageView)

You can then mask topImageView to drawing paths by declaring a CAShapeLayer property and setting it as the mask layer for topImageView

let shapeLayer = CAShapeLayer()

And

topImageView.layer.mask = shapeLayer

Drawing

Now you can write your drawLine function:

func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint)
{
    let path: UIBezierPath
    if let layerPath = self.shapeLayer.path
    {
        path = UIBezierPath(cgPath: layerPath)
    }
    else
    {
        path = UIBezierPath()
    }
    path.move(to: fromPoint)
    path.addLine(to: toPoint)
    self.shapeLayer.path = path.cgPath
    self.shapeLayer.lineWidth = brushWidth
    self.shapeLayer.lineCap = "round"
    self.shapeLayer.strokeColor = UIColor.black.cgColor
}

In a playground in the live view without calling drawLine my view looks like:

enter image description here

After calling:

drawLine(from: CGPoint(x: 0, y: 0), to: CGPoint(x: imageView.frame.maxX, y: imageView.frame.maxY))
drawLine(from: CGPoint(x: 0, y: imageView.frame.maxY), to: CGPoint(x: imageView.frame.maxX, y: 0))

It looks like:

enter image description here

You should be aware that this is processor intensive and may not be performant.

You can see the code I used for my playground here.

Comments