Obi Anachebe Obi Anachebe - 29 days ago 27
Swift Question

Create custom segmented control using bezier path (Swift)

I'm trying to create a custom segmented control that sits underneath my search controller as seen in the mock up below

Segmented Control Mockup

Problem:

I'm having difficulty with creating the pointy needle (looks like this: "^") that indicates the current index.

Attempt at a solution:

Thanks to some help from the question below, I was able to get it looking close, but I'm unable to get the pointer to show up

http://stackoverflow.com/a/37705692/5254240

enter image description here

Question:

How do I get my code to look like the mockup with what I currently have and get the pointer to move with the current index of the segmented controller? See my code below

func imageWithColor(color: UIColor) -> UIImage {

let rect = CGRectMake(0.0, 0.0, 1.0, segmentedController.frame.size.height)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, rect);
let image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image

}

func initializeSearchController() {
segmentedController.setTitleTextAttributes([NSFontAttributeName:UIFont(name: "Lato-Regular", size: 14)!,NSForegroundColorAttributeName:UIColor.init(red: 143/255, green: 142/255, blue: 148/255, alpha: 1.0)], forState:UIControlState.Normal)

segmentedController.setTitleTextAttributes([NSFontAttributeName:UIFont(name: "Lato-Bold", size: 14)!,NSForegroundColorAttributeName:UIColor.init(red: 93/255, green: 176/255, blue: 175/255, alpha: 1.0)], forState:UIControlState.Selected)

segmentedController.setDividerImage(self.imageWithColor(UIColor.clearColor()), forLeftSegmentState: UIControlState.Normal, rightSegmentState: UIControlState.Normal, barMetrics: UIBarMetrics.Default)

segmentedController.setBackgroundImage(self.imageWithColor(UIColor.clearColor()), forState:UIControlState.Normal, barMetrics:UIBarMetrics.Default)

segmentedController.setBackgroundImage(self.imageWithColor(UIColor.clearColor()), forState:UIControlState.Selected, barMetrics:UIBarMetrics.Default)

segmentedController.setTitle("Search", forSegmentAtIndex: 0)
segmentedController.setTitle("Meals", forSegmentAtIndex: 1)

for borderview in segmentedController.subviews {

let upperBorder: CALayer = CALayer()
upperBorder.backgroundColor = UIColor.init(red: 93/255, green: 176/255, blue: 175/255, alpha: 1.0).CGColor
upperBorder.frame = CGRectMake(0, borderview.frame.size.height-1, borderview.frame.size.width, 1.0);
borderview.layer.addSublayer(upperBorder);

}

}

Answer

The easiest way to do this is to cheat. Instead of drawing the line with a bezier curve, you can just draw a solid line by using a UIView with a height of 1. Make the triangle with the solid white bottom in PhotoShop as an image and put it on top of the line. Now to animate the position you can either change its transform, update its center or use autolayout constraints and it will slide over into the new position. For example:

UIView.animate(withDuration: 0.25) {
    triangeImageView.center.x = selectedLabel.center.x
}

You can, of course also do this programmatically using a CAShapeLayer. You would construct your path use something along the lines of:

let bezierPath = UIBezierPath()
bezierPath.move(to:  CGPoint(x: left, y: bottom))
bezierPath.addLine(to: CGPoint(x: centerX + triangleWidth/2, y: top))
bezierPath.addLine(to: CGPoint(x: centerX, y: top - triangleHeight))
bezierPath.addLine(to: CGPoint(x: centerX - triangleWidth/2, y: top))
bezierPath.addLine(to: CGPoint(x: left, y: top))
bezierPath.close()

Then you can use a CABasicAnimation to animate the path from the old path to the new one. Since you are constructing the points in the same order the animation system will interpolate the position and the triangle will appear to slide into position.