0

I'm trying to create blur effect on a view and than add a shape which will show image on this blurred layer (custom video editing functionality)

Currently I'm able to do it only dragging mask view from the right edge:

enter image description here

but when I try to do it from the left edge, I get such a effect:

enter image description here

func configureBlurView() {
        let viewHeight: CGFloat = 60
        let padding: CGFloat = 10
        blurView = UIView()
        blurView.layer.cornerRadius = 10
        blurView.clipsToBounds = true
        blurView.isHidden = true
        blurView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(blurView)
        addConstraints([
            blurView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding),
            blurView.bottomAnchor.constraint(equalTo: stackView.topAnchor, constant: -padding),
            blurView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding),
            blurView.heightAnchor.constraint(equalToConstant: viewHeight)
        ])
        addBlurEffect(for: blurView)
    }
    
    private func addBlurEffect(for view: UIView) {
        let blurEffect = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
        blurEffect.alpha = 0.5
        blurEffect.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(blurEffect)
        addConstraints([
            blurEffect.topAnchor.constraint(equalTo: view.topAnchor),
            blurEffect.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            blurEffect.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            blurEffect.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }
    
    private func makeClearHole(rect: CGRect) {
        let maskLayer = CAShapeLayer()
        maskLayer.fillColor = UIColor.black.cgColor

        let pathToOverlay = CGMutablePath()
        pathToOverlay.addRect(blurView.bounds)
        pathToOverlay.addRect(rect)
        
        maskLayer.path = pathToOverlay
        maskLayer.fillRule = .evenOdd
        maskLayer.cornerRadius = 10
        blurView.layer.mask = maskLayer
    }

I'm using touchesMoved method to change orange view dimensions:

 override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard trimmerView.isHidden == false else { return }
        if let touch = touches.first{
            let currentTouchPoint = touch.location(in: self)
            let previousTouchPoint = touch.previousLocation(in: self)
            
            let deltaX = currentTouchPoint.x - previousTouchPoint.x
            
            if trimmerView.bounds.width >= 70 {
                if touchStartEdge.middle {

                    if trimmerViewLeadingConstraint.constant < 10 {
                        trimmerViewLeadingConstraint.constant = 10
                    } else if trimmerViewTrailingConstraint.constant > -10 {
                        trimmerViewTrailingConstraint.constant = -10
                    } else {
                        trimmerViewLeadingConstraint.constant += deltaX
                        trimmerViewTrailingConstraint.constant += deltaX
                    }
                }

                if touchStartEdge.leftEdge {
                    if trimmerViewLeadingConstraint.constant >= 10.0 {
                        trimmerViewLeadingConstraint.constant += deltaX
                    } else if trimmerViewLeadingConstraint.constant < 10.0 {
                        trimmerViewLeadingConstraint.constant = 10
                    }
                }

                if touchStartEdge.rightEdge {
                    if trimmerViewTrailingConstraint.constant <= -10 {
                        trimmerViewTrailingConstraint.constant += deltaX
                    } else if trimmerViewTrailingConstraint.constant > -10 {
                        trimmerViewTrailingConstraint.constant = -10.0
                    }
                }
            }
            
            updateProgressBarConstraints()
            makeClearHole(rect: CGRect(x: 0, y: 0, width: trimmerView.frame.width, height: trimmerView.frame.height))
            UIView.animate(withDuration: 0.10, delay: 0, options: .curveEaseIn) { [weak self] in
                self?.layoutIfNeeded()
            }
        }
    }

What I'd like to achieve is to remove blur effect only in bounds of orange view.

Any ideas ?? :)

Thanks for help!!

Tzebulion
  • 27
  • 5

1 Answers1

1

Couple ways to do this - here's one...

Add a mask to the blur effect view. As the user drags the "trimmer" update the mask.

Here's a quick example...

We'll:

  • create a stack view with 10 images
  • overlay that with a masked blur effective view
  • add a "draggable trimmer view"
  • when we drag the trimmer, we update the mask

Example View Controller

class TrimmerVC: UIViewController {
    
    var blurView: MaskedBlurView!
    let trimmerView = DragView()
    let stackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // respect safe area when we setup constraints
        let g = view.safeAreaLayoutGuide
        
        stackView.distribution = .fillEqually
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            stackView.heightAnchor.constraint(equalToConstant: 80.0),
        ])

        // let's add 10 imageviews to the stack view
        for i in 1...10 {
            if let img = UIImage(systemName: "\(i).circle.fill") {
                let imgView = UIImageView(image: img)
                imgView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
                stackView.addArrangedSubview(imgView)
            }
        }
        
        let blurEffect = UIBlurEffect(style: .dark)
        blurView = MaskedBlurView(effect: blurEffect)
        blurView.alpha = 0.5
        blurView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(blurView)
        
        NSLayoutConstraint.activate([
            blurView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: 0.0),
            blurView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
            blurView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
            blurView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 0.0),
        ])
        
        trimmerView.backgroundColor = .systemOrange
        view.addSubview(trimmerView)
        
        trimmerView.didDrag = { [weak self] newX in
            guard let self = self else { return }
            self.blurView.clearX = newX - self.stackView.frame.origin.x
        }
        
    }
    // we'll use this to update the framing when the stack view width changes
    //  such as on device rotation
    var curStackW: CGFloat = -1
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if curStackW != stackView.frame.width {
            curStackW = stackView.frame.width
            var r = stackView.frame
            r.origin.y += r.size.height + 20.0
            r.size.width = 160
            r.size.height = 40
            trimmerView.frame = r
            blurView.clearWidth = trimmerView.frame.width
            blurView.clearX = 0
        }
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // toggle the trimmer view between
        //  below the stack view and
        //  overlaid on the stack view
        if trimmerView.frame.origin.y > stackView.frame.origin.y {
            let r = stackView.frame
            trimmerView.frame.origin.y = r.origin.y - 6.0
            trimmerView.frame.size.height = r.height + 12.0
        } else {
            let r = stackView.frame
            trimmerView.frame.origin.y = r.origin.y + r.height + 12.0
            trimmerView.frame.size.height = 60.0
        }
    }
}

Example Draggable "Trimmer" View

class DragView: UIView {
    
    var didDrag: ((CGFloat) -> ())?
    
    let maskLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        maskLayer.fillColor = UIColor.red.cgColor
        layer.mask = maskLayer
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        
        let pathToOverlay = CGMutablePath()
        pathToOverlay.addRect(bounds)
        pathToOverlay.addRect(bounds.insetBy(dx: 20.0, dy: 8.0))
        
        maskLayer.path = pathToOverlay
        maskLayer.fillRule = .evenOdd
        maskLayer.cornerRadius = 10
    }
    
    var touchStartX: CGFloat = 0
    var frameStartX: CGFloat = 0
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        touchStartX = touch.location(in: self.superview!).x
        frameStartX = self.frame.origin.x
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let loc = touch.location(in: self.superview!)
        self.frame.origin.x = frameStartX + (loc.x - touchStartX)
        didDrag?(self.frame.origin.x)
    }
}

Example Masked Blur View

class MaskedBlurView: UIVisualEffectView {
    
    public var clearWidth: CGFloat = 100 {
        didSet { updateMask() }
    }
    public var clearX: CGFloat = 0 {
        didSet { updateMask() }
    }
    
    private let maskLayer = CAShapeLayer()
    
    override init(effect: UIVisualEffect?) {
        super.init(effect: effect)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        maskLayer.fillColor = UIColor.red.cgColor
        layer.mask = maskLayer
    }
    func updateMask() {
        let leftR = CGRect(x: 0, y: 0, width: clearX, height: bounds.height)
        let rightR = CGRect(x: clearX + clearWidth, y: 0, width: bounds.width, height: bounds.height)
        let bez = UIBezierPath(rect: leftR)
        bez.append(UIBezierPath(rect: rightR))
        maskLayer.path = bez.cgPath
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        maskLayer.frame = bounds
    }

}

When running (in landscape orientation) it will start like this:

enter image description here

I placed the "trimmer" view below the stack view to make it a little more clear what's happening.

As we drag the trimmer view, the blur view's mask will be updated:

enter image description here

Tapping anywhere in an empty part of the screen will toggle the trimmer view between "under the stack view" and "overlaid on the stack view":

enter image description here

enter image description here

This was just put together quickly -- you should have no trouble restructuring the code to wrap everything into a single custom view (or however it would work best for your needs).

DonMag
  • 69,424
  • 5
  • 50
  • 86