I am new to iOS development and need help from someone a bit more experienced than me. I searched the internet and couldn't find any working solution.

I need to draw to the screen like canvas in Android. Currently I have a CADisplayLink to call a function every frame. And that's working well. The problem is: How do I actually draw anything, like a rectangle, a circle or a line to the screen every frame?

This is what I have (I linked this class to the view in the storyboard):

class Canvas: UIView { override func draw(_ rect: CGRect) { let context = UIGraphicsGetCurrentContext() context?.setLineWidth(2.0) context?.setStrokeColor(UIColor.green.cgColor) context?.move(to: CGPoint(x: 30, y: 30)) context?.addLine(to: CGPoint(x: Double(xBall), y: Double(yBall))) context?.strokePath() } } 

With the following code I can actually draw a line to the screen:

let canvas = Canvas() canvas.draw(CGRect()) 

The problem is, that this works exactly ONE time. When I have canvas.draw(CGRect()) in my loop which repeats every frame, it works for the first frame (the initial values of xBall and yBall) and never again. When I print the values in the draw method, it gets called every frame and the variables have the correct values. But it does not draw it to the screen. I tried adding the line setNeedsDisplay() in the draw method, with similar results.

Any help will be appreciated! Thanks!

2

1 Answer

If you refer to the draw(_:) documentation, it says:

This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay() or setNeedsDisplay(_:) method instead.

The common approach would be to have your view controller viewDidLoad method add Canvas view:

override func viewDidLoad() { super.viewDidLoad() let canvas = Canvas() canvas.translatesAutoresizingMaskIntoConstraints = false view.addSubview(canvas) NSLayoutConstraint.activate([ canvas.leadingAnchor.constraint(equalTo: view.leadingAnchor), canvas.trailingAnchor.constraint(equalTo: view.trailingAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } 

You don’t call draw(_:) yourself, but rather the OS will do so automatically. All you need to do is add it to your view hierarchy with addSubview(_:). And you can then just have your CADisplayLink update the properties and call setNeedsDisplay (or, better, add didSet observers to those properties that calls setNeedsDisplay for you).

By the way, if you don’t want to add this programmatically, like shown above, you can add Canvas right in Interface Builder. Just drag a UIView onto your storyboard scene, add all of the appropriate constraints, go to the “identity” inspector, and set the base class name to be Canvas:

enter image description here

And if you mark your class as @IBDesignable, you can actually see your path rendered right in Interface Builder, like shown above.


A number of refinements:

  1. If you are going to implement draw(_:) yourself, instead of getting a graphics context with UIGraphicsGetCurrentContext, you might just stroke a UIBezierPath:

    override func draw(_ rect: CGRect) { let path = UIBezierPath() path.move(to: CGPoint(x: 30, y: 30)) path.addLine(to: CGPoint(x: xBall, y: yBall)) path.lineWidth = 2 UIColor.green.setStroke() path.stroke() } 

    Like your solution, this requires that after you update xBall and yBall, if you call setNeedsDisplay to have the view re-rendered with the updated path.

  2. Sometimes we wouldn’t even implement draw(_:). We would just add a CAShapeLayer as a sublayer:

    @IBDesignable class Canvas: UIView { var xBall = ... var yBall = ... let shapeLayer: CAShapeLayer = { let shapeLayer = CAShapeLayer() shapeLayer.strokeColor = UIColor.green.cgColor shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.lineWidth = 2 return shapeLayer }() override init(frame: CGRect = .zero) { super.init(frame: frame) configure() } required init?(coder: NSCoder) { super.init(coder: coder) configure() } func configure() { layer.addSublayer(shapeLayer) updatePath() } func updatePath() { let path = UIBezierPath() path.move(to: CGPoint(x: 30, y: 30)) path.addLine(to: CGPoint(x: xBall, y: yBall)) shapeLayer.path = path.cgPath } } 

    In this sort of approach, you just update the path of the shapeLayer and the OS will render your shape layer (and its path) for you.

0

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.