NSLayoutConsstraint constant not affecting view after UITapGestureRecognizer was tapped

偶尔善良 提交于 2019-12-25 07:41:06

问题


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
    }


}

回答1:


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

After tap

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




回答2:


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


来源:https://stackoverflow.com/questions/43707944/nslayoutconsstraint-constant-not-affecting-view-after-uitapgesturerecognizer-was

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!