Updating a UIView with a subview's gesture handler

37 views Asked by At

I have a simple app with a single ViewController containing a very minimal view hierarchy: DrawingView (a subclass of UIView), and three ImageViews (which are each children of DrawingView).

Inside DrawingView, I've overridden draw(_:) to produce graphics which depend on the center of each ImageView (specifically, a polygon whose vertices are the image centers). Meanwhile, the ImageView positions are controlled by the user through drag gestures. The gesture handling action is a method of ViewController.

I'd like for DrawingView to update in real-time as the ImageView positions change. To accomplish this, I've called DrawingView.setNeedsDisplay() inside the gesture handler. However, this approach only updates DrawingView discretely, and seemingly not until the next gesture begins (regardless of where the call appears in the switch gesture.state statement).

simulator screengrab

My question: where/how should I call setNeedsDisplay in order to achieve a smooth (and real-time) update to DrawingView? Or is there a better approach?

Here are my class definitions:

class ViewController: UIViewController {
    
    @IBOutlet var drawingView: DrawingView!

    @IBOutlet var majorVertex1: UIImageView!
    @IBOutlet var majorVertex2: UIImageView!
    @IBOutlet var majorVertex3: UIImageView!
    
    var majorVertices: [UIImageView]!
    
    @IBOutlet var majorVertex1XConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex1YConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex2XConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex2YConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex3XConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex3YConstraint: NSLayoutConstraint!
    
    var majorVertexXConstraints: [NSLayoutConstraint]!
    var majorVertexYConstraints: [NSLayoutConstraint]!
    
    static var majorVertexXOffsets: [Double]?
    static var majorVertexYOffsets: [Double]?
        
    override func viewDidLoad() {
        
        super.viewDidLoad()
        
        majorVertices = [majorVertex1, majorVertex2, majorVertex3]
        majorVertexXConstraints = [majorVertex1XConstraint, majorVertex2XConstraint, majorVertex3XConstraint]
        majorVertexYConstraints = [majorVertex1YConstraint, majorVertex2YConstraint, majorVertex3YConstraint]
        ViewController.majorVertexXOffsets = majorVertexXConstraints.map {(constraint) -> Double in return constraint.constant}
        ViewController.majorVertexYOffsets = majorVertexYConstraints.map {(constraint) -> Double in return constraint.constant}
        
    }

    @IBAction func handlePan(_ gesture: UIPanGestureRecognizer) {
        
        guard let majorVertices = majorVertices,
              let gestureView = gesture.view
        else {return}
        
        guard let parentView = gestureView.superview,
              let gestureViewIndex = majorVertices.firstIndex(of: gestureView as! UIImageView)
        else {return}
        
        let translation = gesture.translation(in: parentView)
        
        switch gesture.state {
            
        case .began:
            ViewController.majorVertexXOffsets = majorVertexXConstraints.map {(constraint) -> Double in return constraint.constant}
            ViewController.majorVertexYOffsets = majorVertexYConstraints.map {(constraint) -> Double in return constraint.constant}
            break
            
        case .changed:
            majorVertexXConstraints[gestureViewIndex].constant = ViewController.majorVertexXOffsets![gestureViewIndex] + translation.x
            majorVertexYConstraints[gestureViewIndex].constant = ViewController.majorVertexYOffsets![gestureViewIndex] + translation.y
            drawingView.setNeedsDisplay()
            break
            
        case .ended, .cancelled:
            majorVertexXConstraints[gestureViewIndex].constant = gestureView.center.x - parentView.frame.size.width / 2.0
            majorVertexYConstraints[gestureViewIndex].constant = gestureView.center.y - parentView.frame.size.height / 2.0
            break

        default:
            break
            
        }
        
    }

}


class DrawingView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    func setupView() {
        backgroundColor = .clear
    }
        
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        drawTriangle(rect)
    }
    
    internal func drawTriangle(_ rect: CGRect) {
        
        guard let context = UIGraphicsGetCurrentContext(),
              let majorVertexXOffsets = ViewController.majorVertexXOffsets,
              let majorVertexYOffsets = ViewController.majorVertexYOffsets
        else {return}
        
        let majorVertexXCenters = majorVertexXOffsets.map {(x) -> Double in return x + rect.width / 2.0}
        let majorVertexYCenters = majorVertexYOffsets.map {(y) -> Double in return y + rect.height / 2.0}
                
        context.setStrokeColor(UIColor.lightGray.cgColor)
        context.setLineWidth(3)
        
        context.move(to: CGPoint(x: majorVertexXCenters[0], y: majorVertexYCenters[0]))
        context.addLine(to: CGPoint(x: majorVertexXCenters[1], y: majorVertexYCenters[1]))
        context.addLine(to: CGPoint(x: majorVertexXCenters[2], y: majorVertexYCenters[2]))
        context.addLine(to: CGPoint(x: majorVertexXCenters[0], y: majorVertexYCenters[0]))
        
        context.strokePath()
        
    }
    
}
1

There are 1 answers

2
DonMag On BEST ANSWER

You're doing some rather funky things with static vars, which require direct referencing to the specific class... and, it's not entirely clear how you've setup your constraints relative to the views, but...

The reason you are not seeing "real-time" draw updates is because you change the constraint constants without updating the X & Y offsets arrays.

    case .changed:
        // modify constraint constants to "move" the view
        majorVertexXConstraints[gestureViewIndex].constant = ViewController.majorVertexXOffsets![gestureViewIndex] + translation.x
        majorVertexYConstraints[gestureViewIndex].constant = ViewController.majorVertexYOffsets![gestureViewIndex] + translation.y

        // update the arrays of X/Y offsets
        ViewController.majorVertexXOffsets = majorVertexXConstraints.map {(constraint) -> Double in return constraint.constant}
        ViewController.majorVertexYOffsets = majorVertexYConstraints.map {(constraint) -> Double in return constraint.constant}

        // NOW call setNeedsDisplay()
        drawingView.setNeedsDisplay()

        // because we're now updating the constraint constants on .changed
        // we need to reset the gesture translation
        gesture.setTranslation(.zero, in: parentView)
        
        break
        

Edit - in response to comment...

The approach you've taken with static vars results in what's referred to as "Tight Coupling" -- where two or more classes are heavily dependent on each other, which can make it difficult to change and/or reuse the classes.

The issue has too much depth to fully discuss here, but a quick example:

In your DrawingView class, you have these two lines:

let majorVertexXOffsets = ViewController.majorVertexXOffsets
let majorVertexYOffsets = ViewController.majorVertexYOffsets

Suppose you want to use DrawingView in SomeOtherController? You have to edit those two lines:

let majorVertexXOffsets = SomeOtherController.majorVertexXOffsets
let majorVertexYOffsets = SomeOtherController.majorVertexYOffsets

and now DrawingView no longer works in the original ViewController.

What you want to do instead is to add var properties to DrawingView and set/update them as needed.

So, in ViewController remove the static keyword:

//static var majorVertexXOffsets: [Double]?
//static var majorVertexYOffsets: [Double]?
var majorVertexXOffsets: [Double]?
var majorVertexYOffsets: [Double]?

then remove all occurrences of ViewController. in your code:

//ViewController.majorVertexXOffsets
majorVertexXOffsets

Next we add two properties to DrawingView:

var majorVertexXOffsets: [Double]?
var majorVertexYOffsets: [Double]?

and, change the first part of drawTriangle():

//guard let context = UIGraphicsGetCurrentContext(),
//      let majorVertexXOffsets = ViewController.majorVertexXOffsets,
//      let majorVertexYOffsets = ViewController.majorVertexYOffsets
//else {return}
    
guard let context = UIGraphicsGetCurrentContext(),
      let majorVertexXOffsets = majorVertexXOffsets,
      let majorVertexYOffsets = majorVertexYOffsets
else {return}

The last step is back in ViewController:

    case .changed:
        // modify constraint constants to "move" the view
        majorVertexXConstraints[gestureViewIndex].constant = majorVertexXOffsets![gestureViewIndex] + translation.x
        majorVertexYConstraints[gestureViewIndex].constant = majorVertexYOffsets![gestureViewIndex] + translation.y
        
        // update the arrays of X/Y offsets
        majorVertexXOffsets = majorVertexXConstraints.map {(constraint) -> Double in return constraint.constant}
        majorVertexYOffsets = majorVertexYConstraints.map {(constraint) -> Double in return constraint.constant}

        // NEW \/           
        // update the X/Y offset arrays in drawingView
        drawingView.majorVertexXOffsets = majorVertexXOffsets
        drawingView.majorVertexYOffsets = majorVertexYOffsets
        // NEW /\           
        
        // NOW call setNeedsDisplay()
        drawingView.setNeedsDisplay()
        
        // reset the gesture translation
        gesture.setTranslation(.zero, in: parentView)
        
        break
        

Search for Tight Coupling Anti-Pattern for more in-depth discussion.