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.
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:Suppose you want to use
DrawingView
inSomeOtherController
? You have to edit those two lines:and now
DrawingView
no longer works in the originalViewController
.What you want to do instead is to add var properties to
DrawingView
and set/update them as needed.So, in
ViewController
remove thestatic
keyword: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-Pattern
for more in-depth discussion.