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

You're doing some rather funky things with
staticvars, 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.
Edit - in response to comment...
The approach you've taken with
staticvars 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
DrawingViewclass, you have these two lines:Suppose you want to use
DrawingViewinSomeOtherController? You have to edit those two lines:and now
DrawingViewno longer works in the originalViewController.What you want to do instead is to add var properties to
DrawingViewand set/update them as needed.So, in
ViewControllerremove thestatickeyword:then remove all occurrences of
ViewController.in your code:Next we add two properties to
DrawingView:and, change the first part of
drawTriangle():The last step is back in
ViewController:Search for
Tight Coupling Anti-Patternfor more in-depth discussion.