5

I'm reimplementig some kind of UISplitViewController. Now I need to draw a separator line between the master and the detail view. Now I have some questions for this:

  • Should the separator line be on the view controller itself or should it be a separate view?
  • What about Auto Layout? Setting a frame is not allowed.

I saw solutions for CALayer/CAShapeLayer, drawRect (CoreGraphics) or using the background color of an UIView/UILabel. The last one should cost too much performance according to the folks there.

On the one side it is comfortable to draw the line in the UITableViewController itself. Or should a separate UIView be created? If I embed a separate UIView there will be much more constraints and it will complicate things (would have to set separate widths) ... Also it should adapt to orientation changes (e.g. the size of the UITableViewController changes -> the separation line should also resize).

How can I add such a dividing rule? Such a dividing rule can be seen here: Separator line divides master and detail view controller

testing
  • 19,681
  • 50
  • 236
  • 417
  • BTW it would be best if you include an image of what you're trying to do. Also, the comments about performance are not sensible .. adding a view that is "a black line" is absolutely no problem. – Fattie Oct 23 '14 at 09:50
  • Everyone should know the split view controller. But I'm to lazy to include an image for that ;-) I think drawing a line costs less performance than creating a `UIView`. If you want you can read the linked answers of the SO threads in my answer, where they talk about a little bit more. – testing Oct 23 '14 at 11:39
  • 1
    Regarding the image, I don't understand where you want a line and why it's causing such a problem. Regarding the performance comment, it is **totally, absolutely, completely, nonsensical**. Regarding the linked QA, b1234 mentions performance once in passing, and is totally wrong. Note that the person who pointed out that it is wrong, got 24 uproots :) – Fattie Oct 23 '14 at 12:03
  • err 26. i also added a quick explanation there. – Fattie Oct 23 '14 at 12:04
  • 1
    Sounds like you found the solution you need, cheers! – Fattie Oct 23 '14 at 12:05
  • I added an example image (not from my app). Here you can see the line between the left view controller (master) and the right view controller (detail). I didn't need the line in the navigation bar. Furthermore, I didn't know how to do that with auto layout but in genereal it is not a problem. Hmm, that means I could use a simple `UIView` and set a background color together with my constraints. Would be much easier :) – testing Oct 23 '14 at 12:51
  • do you mean the UP AND DOWN line? vertical line ? – Fattie Oct 23 '14 at 12:55
  • dude it is **surprisingly difficult** to draw a true single-pixel line. carefully note all the comments and discussion here on this 500-bounty question .. http://stackoverflow.com/a/22694062/294884 – Fattie Oct 23 '14 at 12:56
  • Yes, I mean the vertical line. Seems your solution is working fine! – testing Oct 23 '14 at 13:19

5 Answers5

7

If you need to add a true one pixel line, don't fool with an image. It's almost impossible. Just use this:

@interface UILine : UIView
@end

@implementation UILine

- (void)awakeFromNib {

    CGFloat sortaPixel = 1 / [UIScreen mainScreen].scale;

    // recall, the following...
    // CGFloat sortaPixel = 1 / self.contentScaleFactor;
    // ...does NOT work when loading from storyboard!

    UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, sortaPixel)];

    line.userInteractionEnabled = NO;
    line.backgroundColor = self.backgroundColor;

    line.autoresizingMask = UIViewAutoresizingFlexibleWidth;

    [self addSubview:line];

    self.backgroundColor = [UIColor clearColor];
    self.userInteractionEnabled = NO;
}

@end

How to use:

Actually in storyboard, simply make a UIView that is in the exact place, and exact width, you want. (Feel free to use constraints/autolayout as normal.)

Make the view say five pixels high, simply so you can see it clearly, while working.

Make the top of the UIView exactly where you want the single-pixel line. Make the UIView the desired color of the line.

Change the class to UILine. At run time, it will draw a perfect single-pixel line in the exact location on all devices.

(For a vertical line class, simply modify the CGRectMake.)

Hope it helps!

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Thanks for your answer. I'm doing nearly everything in code and I can't test out your solution. Now I found an apporach which seems to work. I'll post it as an answer. Hopefully your solution help others! – testing Oct 23 '14 at 11:37
3

I took @joe-blow's excellent answer a step further and created a view that is not only rendered in IB but also whose line width and line color can be changed via the inspector in IB. Simply add a UIView to your storyboard, size appropriately, and change the class to LineView.

import UIKit

@IBDesignable
class LineView: UIView {

  @IBInspectable var lineWidth: CGFloat = 1.0
  @IBInspectable var lineColor: UIColor? {
    didSet {
      lineCGColor = lineColor?.CGColor
    }
  }
  var lineCGColor: CGColorRef?

    override func drawRect(rect: CGRect) {
      // Draw a line from the left to the right at the midpoint of the view's rect height.
      let midpoint = self.bounds.size.height / 2.0
      let context = UIGraphicsGetCurrentContext()
      CGContextSetLineWidth(context, lineWidth)
      if let lineCGColor = self.lineCGColor {
        CGContextSetStrokeColorWithColor(context, lineCGColor)
      }
      else {
        CGContextSetStrokeColorWithColor(context, UIColor.blackColor().CGColor)
      }
      CGContextMoveToPoint(context, 0.0, midpoint)
      CGContextAddLineToPoint(context, self.bounds.size.width, midpoint)
      CGContextStrokePath(context)
    }
}

Inspector for LineView

LineView rendered in storybard

mharper
  • 3,212
  • 1
  • 23
  • 23
1

Updating the @mharper answer to Swift 3.x

import UIKit

@IBDesignable
class LineView: UIView {

    @IBInspectable var lineWidth: CGFloat = 1.0
    @IBInspectable var lineColor: UIColor? {
        didSet {
            lineCGColor = lineColor?.cgColor
        }
    }
    var lineCGColor: CGColor?

    override func draw(_ rect: CGRect) {
        // Draw a line from the left to the right at the midpoint of the view's rect height.
        let midpoint = self.bounds.size.height / 2.0
        if let context = UIGraphicsGetCurrentContext() {
            context.setLineWidth(lineWidth)
            if let lineCGColor = self.lineCGColor {
                context.setStrokeColor(lineCGColor)
            }
            else {
                context.setStrokeColor(UIColor.black.cgColor)
            }
            context.move(to: CGPoint(x: 0.0, y: midpoint))
            context.addLine(to: CGPoint(x: self.bounds.size.width, y: midpoint))
            context.strokePath()
        }
    }
}
YMonnier
  • 1,384
  • 2
  • 19
  • 30
0

New code

This should work for horizontal and vertical lines:

/// <summary>
/// Used as separator line between UIView elements with a given color.
/// </summary>
public class DividerView : UIView
{
    private UIColor color;

    public DividerView ()
    {
        this.color = UIColor.Black;
    }

    public DividerView (UIColor color)
    {
        this.color = color;
    }

    public override void Draw (CGRect rect)
    {
        base.Draw (rect);

        // get graphics context
        CGContext context = UIGraphics.GetCurrentContext ();

        // set up drawing attributes
        color.SetStroke ();
        color.SetFill ();

        // assumption: we can determine if the line is horizontal/vertical based on it's size
        nfloat lineWidth = 0;
        nfloat xStartPosition = 0;
        nfloat yStartPosition = 0;
        nfloat xEndPosition = 0;
        nfloat yEndPosition = 0;

        if (rect.Width > rect.Height) {
            // horizontal line
            lineWidth = rect.Height;
            xStartPosition = rect.X;
            // Move the path down by half of the line width so it doesn't straddle pixels.
            yStartPosition = rect.Y + lineWidth * 0.5f;
            xEndPosition = rect.X + rect.Width;
            yEndPosition = yStartPosition;

        } else {
            // vertical line
            lineWidth = rect.Width;
            // Move the path down by half of the line width so it doesn't straddle pixels.
            xStartPosition = rect.X + lineWidth * 0.5f;
            yStartPosition = rect.Y;
            xEndPosition = xStartPosition;
            yEndPosition = rect.Y + rect.Height;

        }

        // start point
        context.MoveTo (xStartPosition, yStartPosition);

        // end point
        context.AddLineToPoint (xEndPosition, yEndPosition);

        context.SetLineWidth (lineWidth);

        // draw the path
        context.DrawPath (CGPathDrawingMode.Stroke);
    }
}

Original answer

Using frames in an Auto Layout project seems not suitable for me. Also I'd need the actual frames after auto layout has been applied and than I'd have to draw another view on top of it. In a fixed layout based on frames it is no problem, but not here. That's why I chose the following apporach for now:

I created a subclass of UIView and overwrote drawRect like in Draw line in UIView or how do you draw a line programmatically from a view controller?. Here is another option.

Because I'm using C# I give the code samples in that programming language. In the links I posted you can get the Objective-C version if you want.

DividerView:

using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using MonoTouch.CoreGraphics;
using System.CodeDom.Compiler;
using System.Drawing;

namespace ContainerProject
{
    public class DividerView : UIView
    {
        public DividerView ()
        {
        }

        public override void Draw (RectangleF rect)
        {
            base.Draw (rect);

            // get graphics context
            CGContext context = UIGraphics.GetCurrentContext ();

            // set up drawing attributes
            UIColor.Black.SetStroke ();
            UIColor.Black.SetFill ();
            context.SetLineWidth (rect.Width);

            // start point
            context.MoveTo (rect.X, 0.0f);
            // end point
            context.AddLineToPoint (rect.X, rect.Y + rect.Height);

            // draw the path
            context.DrawPath (CGPathDrawingMode.Stroke);
        }
    }
}

In viewDidLoad of my container class I instantiate the DividerView with DividerView separator = new DividerView ();.

Auto Layout:

Then I'm using Auto Layout to layout it's position (all in viewDidLoad):

separator.TranslatesAutoresizingMaskIntoConstraints = false;

separatorTop = NSLayoutConstraint.Create (separator, NSLayoutAttribute.Top, NSLayoutRelation.Equal, TopLayoutGuide, NSLayoutAttribute.Bottom, 1, 0);
separatorBottom = NSLayoutConstraint.Create (separator, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, BottomLayoutGuide, NSLayoutAttribute.Top, 1, 0);
separatorRight = NSLayoutConstraint.Create (separator, NSLayoutAttribute.Right, NSLayoutRelation.Equal, documentListController.View, NSLayoutAttribute.Left, 1, 0);
separatorWidth = NSLayoutConstraint.Create (separator, NSLayoutAttribute.Width, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1, 1);

this.View.AddSubview (separator);

Here the constraints are added:

public override void UpdateViewConstraints ()
{
    if (!hasLoadedConstraints) {
        this.View.AddConstraints (new NSLayoutConstraint[] {
            separatorTop,
            separatorBottom,
            separatorRight,
            separatorWidth
        });

        hasLoadedConstraints = true;
    }
    base.UpdateViewConstraints ();
}

Result:

This approach seems to work. Now my separator line is overlapping my detail view (only the first point). One could adapt the values or changes the constraints of the master and the detail view so that there is room between those for the separator.

Alternatives:

One could also add a subview to the content view of each UITableViewCell. Examples can be found here or here.

Community
  • 1
  • 1
testing
  • 19,681
  • 50
  • 236
  • 417
  • I've just been trying this solution in Xamarin and it only seems to draw a line halfway across the screen / view. – Jammer Nov 26 '15 at 18:13
  • So it is drawing the line which is good. Have you verified your constraints? – testing Nov 27 '15 at 07:56
  • All my constraints are working fine for everything, I ended up doing context.SetLineWidth (rect.Width * 2); – Jammer Nov 27 '15 at 10:58
  • [`SetLineWidth`](https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGContext/#//apple_ref/c/func/CGContextSetLineWidth) determines the width of the line itself, which is drawn between the two points. Are you trying to draw a vertical or a horizontal line? Perhaps the constraints have not been fully calculated at the time the line is drawn and therefore the size is wrong. In this case you could try it with a `LayoutIfNeeded()`. – testing Nov 27 '15 at 12:05
  • It's a horizontal line. I couldn't see any differences between my setup and the demo you show. Very odd indeed. – Jammer Nov 27 '15 at 12:41
  • My code was for my vertical line at first. Now I looked if I can find my project anywhere where I used this. I saw that I modified the code. If you like I can edit my answer so that you can test it out. – testing Nov 27 '15 at 12:45
  • Not had a chance to check yet. – Jammer Nov 30 '15 at 10:44
0

MHarper's code updated for Swift 5:

import UIKit

@IBDesignable
class LineView: UIView {

  @IBInspectable var lineWidth: CGFloat = 1.0
  @IBInspectable var lineColor: UIColor? {
    didSet {
        lineCGColor = lineColor?.cgColor
    }
  }
    var lineCGColor: CGColor?

    override func draw(_ rect: CGRect) {
      // Draw a line from the left to the right at the midpoint of the view's rect height.
      let midpoint = self.bounds.size.height / 2.0
      let context = UIGraphicsGetCurrentContext()
        context!.setLineWidth(lineWidth)
      if let lineCGColor = self.lineCGColor {
        context!.setStrokeColor(lineCGColor)
      }
      else {
        context!.setStrokeColor(UIColor.black.cgColor)
      }
        context!.move(to: CGPoint(x: 0.0, y: midpoint))
        context!.addLine(to: CGPoint(x: self.bounds.size.width, y: midpoint))
        context!.strokePath()
    }
}
ICL1901
  • 7,632
  • 14
  • 90
  • 138