1

as the title says, i have self.view which i add to its mask (link) property another view, but when i add more views to self.view using addSubview the mask is removed. why is that? thanks

i originally had this problem and realized that the mask view is being released.

in my example i add animatable UIImageViews that animate across a UIBezierPath when i tap the screen in addView.

here is the code:

protocol UICircleMaskDelegate {

    func circleMaskCompletion()

}

class UICircleMask: UIView {

    var delegate: UICircleMaskDelegate?
    var gestureDelegate: UIGestureRecognizerDelegate?

    init(gestureDelegate: UIGestureRecognizerDelegate? = nil) {
        super.init(frame: .zero)
        self.gestureDelegate = gestureDelegate
        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 = Double((UIScreen.main.bounds.height - self.frame.height / 2.1) / 600)// duration = way / speed
            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
                    self.delegate?.circleMaskCompletion()
                }
            })
        } else {
            let newSize = diameterConstraint!.constant + (delta * 2.85)
            if newSize > 60 && newSize < UIScreen.main.bounds.height * 2.1 {
                diameterConstraint?.constant = newSize
            }
        }

    }

    var panStarted = false

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

    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)
            //
            let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
            panGesture.delegate = gestureDelegate
            self.superview?.addGestureRecognizer(panGesture)
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.layer.cornerRadius = self.frame.width / 2
    }

}


class ViewController: UIViewController, UIGestureRecognizerDelegate, UICircleMaskDelegate {

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

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

    func circleMaskCompletion() {
//        print("nana")
    }

    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)

        let circleMask = UICircleMask(gestureDelegate: self)
        circleMask.delegate = self
        self.view.mask = circleMask

    }

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

    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)
        self.view.sendSubview(toBack: 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 {

    func turnOffMaskResizing() {
        self.translatesAutoresizingMaskIntoConstraints = false
    }


    @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
    }


}
Community
  • 1
  • 1
user1974368
  • 474
  • 5
  • 13

1 Answers1

0

Your mask is there, it's just that it is 2.1 times the size of your view, so it always covers the entire thing so you can see everything. You have a bug in your code:

if pan.state == .began {
        if delta > 0 {
            panStarted = true
            circle.updateSize(-delta)
        }

At .began there will never be a translation, so this code is never getting hit, which means nothing else in your handler is processed. This code should just set the panStarted flag.

You have to drag quite a long way for the mask to take effect, and when you release, it springs back out to 2.1 times the size of the view again.

You can see this quite easily by adding the circle as a subview instead of a mask. Replacing all the 2.1 values with 0.5 gives you this effect:

enter image description here

So in summary, there's nothing wrong with your mask, but with the surrounding code.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • have you seen [here](https://youtu.be/UtNuc8nicgs) that i managed to get the desired behavior before adding views to `self.view`? i linked this in my previous post i linked above. I'm using 2.1 because i have a circle that its center is at the bottom of `self.view` and if i want it to cover the entire view i need the radius to be as tall as the view. when i look at my view hierarchy before and after adding `UIImageView`s to the screen the mask is there before and gone after. – user1974368 May 01 '17 at 10:53
  • Even leaving 2.1 in you can still see the mask take effect if you pan far enough and fix the bug I mentioned, but it always springs back when the pan is finished. You problem is not with a disappearing mask. Masks don't disappear when you add subviews. – jrturton May 01 '17 at 10:57
  • there is a translation in `.begin`. so i don't understand why you say there is a bug. and i programmed it to bounce back to full size when the pan is finished. when i look at `View UI Hierarchy` debug mode before and after adding views to the screen the mask is there before and gone after. i understand there is no logical reason for it, but it does happen for me. – user1974368 May 01 '17 at 11:03
  • I'm running it in a playground so maybe there's a difference there, but for me at began there was never a delta. – jrturton May 01 '17 at 11:05
  • Honestly I think you're better off using a layer mask than a view mask, it seems to be adding so much complication trying to use autolayout to handle this, and having the multiple animations... just use a CAShapeLayer with a circular path as the main view's layer mask and animate the path of it. – jrturton May 01 '17 at 11:07
  • ah ok, no the panning works fine until i tap the screen. the mask resizes itself and everything is fine like in the video i attached: https://www.youtube.com/watch?v=UtNuc8nicgs after the tap for unknown reason the mask view is no longer in my view hirerchy. – user1974368 May 01 '17 at 11:08
  • yeah i tried that but had a problem resizing the mask because i needed to use a scale transform to resize it and it was too complicated for me, also i had trouble keeping the layer centered at the bottom. – user1974368 May 01 '17 at 11:10
  • Well, is a mask really part of the view hierarchy? I got it working after the tap by forcing a layout of the circle during the resize method, but that made other things a bit strange. I don't think autolayout plays very nicely with masks, hence my suggestion of using layers. – jrturton May 01 '17 at 11:17
  • yes, when you assign `view.mask = maskView` then `maskView` becomes a direct subview of `view`. what do you mean forcing a layout? in the resizing method you added `self.layoutIfNeeded()`? – user1974368 May 01 '17 at 11:21
  • well apparently the mask view isn't apart of the view hierarchy as i said even though in some cases it does append to the `self.view` subviews. yeah I'm going with the layer approach instead, thanks for you help :) – user1974368 May 02 '17 at 15:51