0

i have a view (self.view) that is masked with another view (not a layer) using the UIView.mask property. on self.view i installed a UIPanGestureRecognizer so when i pan across the screen the mask gets smaller and larger accordingly. in addition i installed on self.view a UITapGestureRecognizer which adds animatable UIImageViews to the screen and they are animating across a UIBezierPath. i am updating the mask size with constraints.

the problem is that after i tap the screen to add animatable views the changes i make on the mask constraint stop taking affect. i can see in the log that i do indeed change the constant of the constraint and that the UIPanGestureRecognizer is still working.

so i mean that the mask view constraint stop affecting its view. why is that? thanks

video illustration: https://youtu.be/UtNuc8nicgs

here is the code:

class UICircle: UIView {
    init() {
        super.init(frame: .zero)
        self.clipsToBounds = true
        self.backgroundColor = .yellow
        self.isUserInteractionEnabled = false
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    var diameterConstraint: NSLayoutConstraint?
    var animating = false

    func updateSize(_ delta: CGFloat, animated: Bool = false) {

        if animating { return }
        if animated {
            animating = true
            diameterConstraint?.constant = UIScreen.main.bounds.height * 2.1

            let duration: TimeInterval = 0.6
            let animation = CABasicAnimation(keyPath: "cornerRadius")
            animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
            animation.fromValue = self.layer.cornerRadius
            animation.toValue = UIScreen.main.bounds.height * 2.1 / 2
            animation.duration = duration
            self.layer.add(animation, forKey: nil)

            UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {
                self.superview?.layoutIfNeeded()
            }, completion: { (success) in
                if success {
                    self.animating = false
                }
            })
        } else {
            let newSize = diameterConstraint!.constant + (delta * 2.85)
            if newSize > 60 && newSize < UIScreen.main.bounds.height * 2.1 {
                diameterConstraint?.constant = newSize
            }
        }

    }

    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        if let superv = superview {
            self.makeSquare()
            self.centerHorizontallyTo(superv)
            let c = NSLayoutConstraint.init(item: self, attribute: .centerY, relatedBy: .equal, toItem: superv, attribute: .bottom, multiplier: 1, constant: -40)
            c.isActive = true
            diameterConstraint = self.constrainHeight(superv.frame.height * 2.1)
        }
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        self.layer.cornerRadius = self.frame.width / 2
    }

}


class ViewController: UIViewController, UIGestureRecognizerDelegate {

    var circle = UICircle()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.init(red: 48/255, green: 242/255, blue: 194/255, alpha: 1)
        self.view.clipsToBounds = true

        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        tap.delegate = self
        self.view.addGestureRecognizer(tap)

        setupCircle()

    }


    func setupCircle() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        panGesture.delegate = self
        self.view.addGestureRecognizer(panGesture)
        self.view.mask = circle
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }


    var panStarted = false
    func handlePan(_ pan: UIPanGestureRecognizer) {
        let delta = pan.translation(in: self.view).y
        if pan.state == .began {
            if delta > 0 {
                panStarted = true
                circle.updateSize(-delta)
            }
        } else if pan.state == .changed {
            if panStarted {
                circle.updateSize(-delta)
            }
        } else if pan.state == .ended || pan.state == .cancelled {
            if panStarted {
                circle.updateSize(self.view.frame.height * 2.1, animated: true)
            }
            panStarted = false
        }
        pan.setTranslation(.zero, in: self.view)
    }

    func handleTap() {
        let num = Int(5 + drand48() * 10)
        (1 ... num).forEach { (_) in
            addView()
        }
    }

    override var prefersStatusBarHidden: Bool {
        get {
            return true
        }
    }

    func addView() {

        var image: UIImageView!
        let dd = drand48()
        if dd < 0.5 {
            image = UIImageView(image: #imageLiteral(resourceName: "heart1"))
        } else {
            image = UIImageView(image: #imageLiteral(resourceName: "heart2"))
        }

        image.isUserInteractionEnabled = false
        image.contentMode = .scaleAspectFit
        let dim: CGFloat = 20 + CGFloat(10 * drand48())
        image.constrainHeight(dim)
        image.constrainWidth(dim)

        let animation = CAKeyframeAnimation(keyPath: "position")
        let duration = Double(1.5 * self.view.frame.width / CGFloat((60 + drand48() * 40))) // duration = way / speed
        animation.path = getPath().cgPath
        animation.duration = duration
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
        animation.fillMode = kCAFillModeForwards
        animation.isRemovedOnCompletion = false
        image.layer.add(animation, forKey: nil)

        DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + duration + 1) {
            DispatchQueue.main.async {
                image.removeFromSuperview()
            }
        }

        if drand48() < 0.3 {
            UIView.animate(withDuration: 0.2 + 0.1 * drand48() , delay: TimeInterval(drand48() * 1), options: [.curveEaseOut, .repeat, .autoreverse], animations: {
                image.transform = CGAffineTransform.init(scaleX: 1.5, y: 1.5)
            }, completion: nil)
        }

        self.view.addSubview(image)

    }


    func getPath() -> UIBezierPath {

        let path = UIBezierPath()

        let startPoint = CGPoint.init(x: -30, y: self.view.frame.height / 2)
        path.move(to: startPoint)

        let r = CGFloat(400 * drand48())
        let cp1 = CGPoint.init(x: self.view.frame.width * 0.33, y: self.view.frame.height * 0.25 - r)
        let cp2 = CGPoint.init(x: self.view.frame.width * 0.66, y: self.view.frame.height * 0.75 + r)
        let endPoint = CGPoint.init(x: self.view.frame.width + 30, y: self.view.frame.height / 2)

        path.addCurve(to: endPoint, controlPoint1: cp1, controlPoint2: cp2)

        return path

    }

}


extension UIView {

    @discardableResult
    func makeSquare() -> NSLayoutConstraint {
        self.turnOffMaskResizing()
        let constraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self, attribute: NSLayoutAttribute.height, multiplier: 1.0, constant: 0)
        NSLayoutConstraint.activate([constraint])
        return constraint
    }


    @discardableResult
    func centerHorizontallyTo(_ toItem: UIView, padding: CGFloat) -> NSLayoutConstraint {
        self.turnOffMaskResizing()
        let constraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: toItem, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: padding)
        NSLayoutConstraint.activate([constraint])
        return constraint
    }


    @discardableResult
    func constrainHeight(_ height: CGFloat, priority: UILayoutPriority = 1000) -> NSLayoutConstraint {
        self.turnOffMaskResizing()
        let constraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.height, multiplier: 0, constant: height)
        constraint.priority = priority
        NSLayoutConstraint.activate([constraint])
        return constraint
    }



    @discardableResult
    func constrainWidth(_ width: CGFloat) -> [NSLayoutConstraint] {
        self.turnOffMaskResizing()
        let constraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[item(width)]", metrics: ["width" : width], views: ["item" : self])
        NSLayoutConstraint.activate(constraints)
        return constraints
    }


    func turnOffMaskResizing() {
        self.translatesAutoresizingMaskIntoConstraints = false
    }


}
user1974368
  • 474
  • 5
  • 13
  • Your code references some custom autolayout methods that aren't in the listing provided (e.g. `image.constrainHeight(dim)`, `makeSquare()`) - any chance you could add them? – Rich Tolley May 01 '17 at 00:13
  • @richt i can see that `updateSize` is indeed being called and i do indeed change the value of the constraint's constant. plus, i added the extension you asked for. – user1974368 May 01 '17 at 06:13

2 Answers2

0

I think it is because you add new objects to that view which will affect the constraints, and they break. What I propose is to add circle as a subview so it is not related to the other objects.

This is what I tried and it worked

    override func viewDidLoad() {
    super.viewDidLoad()

    let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    tap.delegate = self
    self.view.addGestureRecognizer(tap)

    setupCircle()

}


func setupCircle() {

    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
    panGesture.delegate = self

    self.view.addSubview(circle)
    self.circle.backgroundColor = UIColor.init(red: 48/255, green: 242/255, blue: 194/255, alpha: 1)
    self.circle.clipsToBounds = true
    self.view.addGestureRecognizer(panGesture)

}

EDIT:Added images of change what will happen in your hierarchy

Before tap enter image description here

After tap enter image description here

Your mask seems removed after the tap - But I am not sure how to fix that, still do not see reason why can't you add subview

Luzo
  • 1,336
  • 10
  • 15
  • thanks but i checked it now and it's not masking the view, which was the initial propose. also the constraint don't break, there is no breaking logs in the console. i don't see how the `UIImageView`s are related to the `self.view.mask` view by any relation constraints-wise. – user1974368 May 01 '17 at 06:40
  • Look at the edit, you will see what is happening. You can do it with mask, but I do not help you with that probably. My approach works so you can keep it as a backup. – Luzo May 01 '17 at 06:58
  • thank you, you helped me realize that the `mask` view is being released somehow, still don't know why. i don't add it as a subview because it defeats the whole propose of what I'm trying to achieve, which is exactly like the camera in WhatsApp, which can be dismissed with a pan gesture to the bottom of the screen and it masks out everything outside of the circle. – user1974368 May 01 '17 at 07:07
  • Okay there might be a hack for you. You will need 2 things, first one is view which will be in front and will serve as a cover, second one is your circle which should be mask of what should have alpha 0. Just make sure this view is in front. You might consider to add one subview where you add your moving objects and other subview which will be your mask view, that way you ensure it is still over all objects. That is what I suggest to you – Luzo May 01 '17 at 07:19
  • i think that in that case the `mask` view will only mask out the `coverView` and will leave `self.view` visible behind it. `mask` only applies to the view it's assigned to, no? – user1974368 May 01 '17 at 07:37
  • Yes it does and it is correct for suggested approach. In this approach, mask has to be inverted. You might get an inspiration here, but I never implemented it http://stackoverflow.com/questions/28448698/how-do-i-create-a-uiview-with-a-transparent-circle-inside-in-swift – Luzo May 01 '17 at 07:56
  • yeah i saw this post and i had trouble to achieve this behavior with layers because i had to achieve animation with `cornerRadius` in my app. i did some additional testings and i realized that no matter which other view i add to `self.view` it removes the mask view from it. – user1974368 May 01 '17 at 08:57
  • check my answer below, I added working code for you, it hides your moving objects – Luzo May 01 '17 at 10:40
0

This is proof of my concept, took and reworked CircleMaskView from https://stackoverflow.com/a/33076583/4284508. This does what you need. It is little bit mess, so do not take it as a done thing. I use your class to get frame and radius for the other mask, so you will need to get rid of it somehow and compute radius and frame in some other manner. But it will serve

/// Apply a circle mask on a target view. You can customize radius, color and opacity of the mask.
class CircleMaskView {

    private var fillLayer = CAShapeLayer()
    var target: UIView?

    var fillColor: UIColor = UIColor.gray {
        didSet {
            self.fillLayer.fillColor = self.fillColor.cgColor
        }
    }

    var radius: CGFloat? {
        didSet {
            self.draw()
        }
    }

    var opacity: Float = 0.5 {
        didSet {
            self.fillLayer.opacity = self.opacity
        }
    }

    /**
     Constructor

     - parameter drawIn: target view

     - returns: object instance
     */
    init(drawIn: UIView) {
        self.target = drawIn
    }

    /**
     Draw a circle mask on target view
     */
    func draw() {
        guard let target = target else {
            print("target is nil")
            return
        }

        var rad: CGFloat = 0
        let size = target.frame.size
        if let r = self.radius {
            rad = r
        } else {
            rad = min(size.height, size.width)
        }

        let path = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:size.width, height:size.height), cornerRadius: 0.0)
        let circlePath = UIBezierPath(roundedRect: CGRect(x:size.width / 2.0 - rad / 2.0, y:0, width:rad, height:rad), cornerRadius: rad)
        path.append(circlePath)
        path.usesEvenOddFillRule = true

        fillLayer.path = path.cgPath
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = self.fillColor.cgColor
        fillLayer.opacity = self.opacity
        target.layer.addSublayer(fillLayer)
    }

    func redraw(withCircle circle: UICircle) {
        guard let target = target else {
            print("target is nil")
            return
        }

        var rad: CGFloat = 0
        let size = target.frame.size
        if let r = self.radius {
            rad = r
        } else {
            rad = min(size.height, size.width)
        }

        let path = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:size.width, height:size.height), cornerRadius: 0.0)
        let circlePath = UIBezierPath(roundedRect: circle.frame, cornerRadius: circle.diameterConstraint!.constant)
        path.append(circlePath)
        path.usesEvenOddFillRule = true

        fillLayer.path = path.cgPath
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = self.fillColor.cgColor
        fillLayer.opacity = self.opacity
        target.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
        target.layer.addSublayer(fillLayer)
    }

    /**
     Remove circle mask
     */


    func remove() {
        self.fillLayer.removeFromSuperlayer()
    }

}

var circle = UICircle()
var circleMask: CircleMaskView?
var subviewC = UIView()

override func viewDidLoad() {
    super.viewDidLoad()
    self.subviewC.clipsToBounds = true

    let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    tap.delegate = self
    self.view.addGestureRecognizer(tap)
    view.backgroundColor = UIColor.init(red: 48/255, green: 242/255, blue: 194/255, alpha: 1)
    subviewC.backgroundColor = .clear
    subviewC.frame = view.frame
    self.view.addSubview(subviewC)
    self.view.addSubview(circle)
    circle.backgroundColor = .clear
    setupCircle()
}

func setupCircle() {
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
    panGesture.delegate = self
    self.subviewC.addGestureRecognizer(panGesture)

    circleMask = CircleMaskView(drawIn: subviewC)
    circleMask?.opacity = 1.0
    circleMask?.draw()
}

override func viewDidLayoutSubviews() {
    circleMask?.redraw(withCircle: circle)
}

func handlePan(_ pan: UIPanGestureRecognizer) {

    let delta = pan.translation(in: self.view).y
    if pan.state == .began {
        if delta > 0 {
            panStarted = true
            circle.updateSize(-delta)
            circleMask?.redraw(withCircle: circle)
        }
    } else if pan.state == .changed {
        if panStarted {
            circle.updateSize(-delta)
            circleMask?.redraw(withCircle: circle)
        }
    } else if pan.state == .ended || pan.state == .cancelled {
        if panStarted {
            circle.updateSize(self.view.frame.height * 2.1, animated: true)
            circleMask?.redraw(withCircle: circle)
        }
        panStarted = false
    }
    pan.setTranslation(.zero, in: self.view)
}
Community
  • 1
  • 1
Luzo
  • 1,336
  • 10
  • 15