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 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.