How do you draw a line programmatically from a view controller?

70,983

Solution 1

There are two common techniques.

  1. Using CAShapeLayer:

    • Create a UIBezierPath (replace the coordinates with whatever you want):

      UIBezierPath *path = [UIBezierPath bezierPath];
      [path moveToPoint:CGPointMake(10.0, 10.0)];
      [path addLineToPoint:CGPointMake(100.0, 100.0)];
      
    • Create a CAShapeLayer that uses that UIBezierPath:

      CAShapeLayer *shapeLayer = [CAShapeLayer layer];
      shapeLayer.path = [path CGPath];
      shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
      shapeLayer.lineWidth = 3.0;
      shapeLayer.fillColor = [[UIColor clearColor] CGColor];
      
    • Add that CAShapeLayer to your view's layer:

      [self.view.layer addSublayer:shapeLayer];
      

    In previous versions of Xcode, you had to manually add QuartzCore.framework to your project's "Link Binary with Libraries" and import the <QuartzCore/QuartzCore.h> header in your .m file, but that's not necessary anymore (if you have the "Enable Modules" and "Link Frameworks Automatically" build settings turned on).

  2. The other approach is to subclass UIView and then use CoreGraphics calls in the drawRect method:

    • Create a UIView subclass and define a drawRect that draws your line.

      You can do this with Core Graphics:

      - (void)drawRect:(CGRect)rect {
          CGContextRef context = UIGraphicsGetCurrentContext();
      
          CGContextSetStrokeColorWithColor(context, [[UIColor blueColor] CGColor]);
          CGContextSetLineWidth(context, 3.0);
          CGContextMoveToPoint(context, 10.0, 10.0);
          CGContextAddLineToPoint(context, 100.0, 100.0);
          CGContextDrawPath(context, kCGPathStroke);
      }
      

      Or using UIKit:

      - (void)drawRect:(CGRect)rect {
          UIBezierPath *path = [UIBezierPath bezierPath];
          [path moveToPoint:CGPointMake(10.0, 10.0)];
          [path addLineToPoint:CGPointMake(100.0, 100.0)];
          path.lineWidth = 3;
          [[UIColor blueColor] setStroke];
          [path stroke];
      }
      
    • Then you can either use this view class as the base class for your NIB/storyboard or view, or you can have your view controller programmatically add it as a subview:

      PathView *pathView = [[PathView alloc] initWithFrame:self.view.bounds];
      pathView.backgroundColor = [UIColor clearColor];
      
      [self.view addSubview: pathView];
      

The Swift renditions of the two above approaches are as follows:

  1. CAShapeLayer:

    // create path
    
    let path = UIBezierPath()
    path.move(to: CGPoint(x: 10, y: 10))
    path.addLine(to: CGPoint(x: 100, y: 100))
    
    // Create a `CAShapeLayer` that uses that `UIBezierPath`:
    
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath
    shapeLayer.strokeColor = UIColor.blue.cgColor
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.lineWidth = 3
    
    // Add that `CAShapeLayer` to your view's layer:
    
    view.layer.addSublayer(shapeLayer)
    
  2. UIView subclass:

    class PathView: UIView {
    
        var path: UIBezierPath?           { didSet { setNeedsDisplay() } }
        var pathColor: UIColor = .blue    { didSet { setNeedsDisplay() } }
    
        override func draw(_ rect: CGRect) {
            // stroke the path
    
            pathColor.setStroke()
            path?.stroke()
        }
    
    }
    

    And add it to your view hierarchy:

    let pathView = PathView()
    pathView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(pathView)
    
    NSLayoutConstraint.activate([
        pathView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        pathView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        pathView.topAnchor.constraint(equalTo: view.topAnchor),
        pathView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])
    
    pathView.backgroundColor = .clear
    
    let path = UIBezierPath()
    path.move(to: CGPoint(x: 10, y: 10))
    path.addLine(to: CGPoint(x: 100, y: 100))
    path.lineWidth = 3
    
    pathView.path = path
    

    Above, I'm adding PathView programmatically, but you can add it via IB, too, and just set its path programmatically.

Solution 2

Create a UIView and add it as a subview of your view controller's view. You can modify this subview's height or width to be very small so that it looks like a line. If you need to draw a diagonal line you can modify the subviews transform property.

e.g. draw black horizontal line. This is called from within your view controller's implementation

UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0,0, self.view.frame.size.width, 1)];
lineView.backgroundColor = [UIColor blackColor];
[self.view addSubview:lineView];

Solution 3

Swift 3:

let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 100, y: 100))

let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 3.0

view.layer.addSublayer(shapeLayer)

Solution 4

Here's a cool technique you might find useful: Using blocks for drawing to avoid subclassing in Objective-C

Include the article's general-purpose view subclass in your project, then this is the kind of code you can put in your view controller to create a view on the fly that draws a line:

DrawView* drawableView = [[[DrawView alloc] initWithFrame:CGRectMake(0,0,320,50)] autorelease];
drawableView.drawBlock = ^(UIView* v,CGContextRef context)
{
  CGPoint startPoint = CGPointMake(0,v.bounds.size.height-1);
  CGPoint endPoint = CGPointMake(v.bounds.size.width,v.bounds.size.height-1);

  CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
  CGContextSetLineWidth(context, 1);
  CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
  CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
  CGContextStrokePath(context);
};
[self.view addSubview:drawableView];

Solution 5

You can use UIImageView to draw lines on.

It however, allows to skip sub-classing. And as I am little inclined to Core Graphics still can use it. You can just put it in - ViewDidLoad

  UIGraphicsBeginImageContext(self.view.frame.size);
  [self.myImageView.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
  CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
  CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush);

  CGContextMoveToPoint(UIGraphicsGetCurrentContext(), 50, 50);
  CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), 200, 200);
  CGContextStrokePath(UIGraphicsGetCurrentContext());
  CGContextFlush(UIGraphicsGetCurrentContext());
  self.myImageView.image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();

Addition to Rob's answer, For a quicke, the third approach is to use an UIImageView - cover up with it - the view of xib. (Thats the default UIImageView appearance when dragged on xib in xcode 5)

Cheers and +1!

Share:
70,983

Related videos on Youtube

mkc842
Author by

mkc842

Updated on February 26, 2021

Comments

  • mkc842
    mkc842 about 3 years

    I have a UIViewController. How do I draw a line in one of its programmatically created views?

    • Bryan Chen
      Bryan Chen almost 11 years
      No you can't. Drawing should be done by view not controller. You have to change your design.
    • mkc842
      mkc842 almost 11 years
      @xlc You CAN do it from a VC, as Rob's answer shows. If you actually want to be helpful, explain what's the harm in the bezier technique.
    • bugmagnet
      bugmagnet almost 10 years
      Can we see some code? I agree, it's a a real question, but I (maybe we) respond best to code demonstrating what you're talking about.
  • marko
    marko almost 11 years
    I'd be inclined to agree that for a separator element (e.g. an line between UI elements) this is the path of least resistance. The alternative of either rendering into a bitmap to back a CoreAnimation layer or implementing a draw handler in a custom view is far more involved. This probably hardware accelerates better too, provided it's only one line.
  • Jeff Ames
    Jeff Ames about 10 years
    I'm curious about the reasoning of why this is is a terrible idea. I'd like to avoid any problems this solution might cause in my own code.
  • Manthan
    Manthan over 9 years
    @Rob: This works perfectly fine. I want to ask you that I have a line distance AB and an angle from AB, then how to get start and end points in that situation? Please help me.
  • Kishore Kumar
    Kishore Kumar over 8 years
    i added uigesture to my view ,and i have touch starting point and moving point if i add that in above comment straight line working properly .but when i touch at side from starting point itself its occupying the color @Rob
  • Joe Huang
    Joe Huang about 8 years
    @sangony I use this technique too in my code. I also wonder why this is a terrible idea? Please help to explain.
  • Albert Renshaw
    Albert Renshaw over 6 years
    When using CAShapeLayer method you can (and likely will want to) set the line caps to rounded so that lines look smooth as they connect, shapeLayer.lineCap = kCALineCapRound;
  • Rob
    Rob over 6 years
    Or, if you have multiple lines that you want to look smooth as they connect, you could also have a single UIBezierPath, with repeated addLineToPoint/addLine(to:) calls. Then you can pick whether you prefer shapeLayer.lineJoin = kCALineJoinRound or kCALineJoinBevel or kCALineJoinMiter.
  • Vilmir
    Vilmir over 5 years
    Thanks for the code snippet, it saved me quite some time. If you intend to draw a dynamic view, reacting to some events, you will need to clean the sublayer at the beginning of draw with this: self.layer.sublayers = nil