Dynamic Shapes in UITableView Cell (Swift) - ios

I am new to Swift and having fits with the UIView class. I have a TableView (below) with a View object (left) and Label (right). The table itself works fine and the labels appear as expected.
Where I am having trouble is that I want the View object next to the label to contain various shapes and colors depending on the values in the array that support the table...
var tArray = [["Row 1","Row 2", "Row 3", "Row 4", "Row 5"],
["Circle","Circle","Square","Square","Diamond"],
["Blue","Red","Green","Red","Purple"]]
So next to "Row 1", I want to have a blue circle, etc. I have linked the View object to a custom class. But I need an approach to dynamically create the shapes and fill with appropriate colors.
In the TableViewController, I have the following, which is calling the Symbol class, and I am getting back a black circle (I hard-coded circle for now)...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.cellLabel.text = tArray[0][indexPath.row]
cell.cellSymbol = Symbol.init()
return cell
}
In my custom Symbol class:
import UIKit
class Symbol: UIView {
var inColor: String
var inShape: String
init (in_color: String, in_shape: String) {
self.inColor = in_color
self.inShape = in_shape
super.init(frame: CGRect(x: 0, y: 0, width: 70, height: 70))
}
override func drawRect(rect: CGRect) {
let path = UIBezierPath(ovalInRect: rect)
switch self.inColor {
case "Green" : UIColor.greenColor().setFill()
case "Blue" : UIColor.blueColor().setFill()
case "Yellow" : UIColor.yellowColor().setFill()
case "Cyan" : UIColor.cyanColor().setFill()
case "Red" : UIColor.redColor().setFill()
case "Brown" : UIColor.brownColor().setFill()
case "Orange" : UIColor.orangeColor().setFill()
case "Purple" : UIColor.purpleColor().setFill()
case "Grey" : UIColor.darkGrayColor().setFill()
default: UIColor.blackColor().setFill()
}
path.fill()
}
required init?(coder aDecoder: NSCoder) {
self.inColor = ""
self.inShape = ""
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
self.inColor = ""
self.inShape = ""
super.init(frame: frame)
}
}
I may be going about this all wrong and am open to other approaches entirely. In order to compile, I had to add the required init? and override init(frame: CGRect) entires. I also had to put in the initialization of the self.inColor and .inShape to compile, but since I'm not passing in the parameters to those, I have nothing to assign.
So what I get is a black circle every time. I hard-coded circle for now to keep it simple. The switch self.inColor is nil every time, so it is going down to the default case.
Any suggestions would be greatly appreciated!!!!

When you set the cellSymbol, you are creating a new instance of the Symbol class. You never modify any of the properties of cellSymbol, so it is always black.
Try:
cell.cellSymbol.inColor = self.tArray[2][indexPath.row]

The "always black" error in your code has been addressed by https://stackoverflow.com/a/35049741/218152. Below are suggested improvements.
#IBInspectable
Less code, more features
Replace your entire Symbol class with this:
#IBDesignable class Symbol: UIView {
var color = UIColor.blackColor()
#IBInspectable var inColor: String = "Black" {
didSet {
switch inColor {
case "Green" : color = UIColor.greenColor()
case "Blue" : color = UIColor.blueColor()
case "Yellow" : color = UIColor.yellowColor()
case "Cyan" : color = UIColor.cyanColor()
case "Red" : color = UIColor.redColor()
case "Brown" : color = UIColor.brownColor()
case "Orange" : color = UIColor.orangeColor()
case "Purple" : color = UIColor.purpleColor()
case "Grey" : color = UIColor.darkGrayColor()
default: color = UIColor.blackColor()
}
}
}
override func drawRect(rect: CGRect) {
color.setFill()
let path = UIBezierPath(ovalInRect: rect)
path.fill()
}
}
Use it just the same way you would use the previous one (cell.cellSymbol.inColor = ...). It will have the advantage of also being visually editable from Interface Builder. It also does not require special initialization (no init).
This implementation has the added advantage to accept a UIColor directly, as in cell.cellSymbol.color = ...
Further improvements include using the tintColor instead of creating your own instance, case-insensitive colors, enum instead of names.

Thanks to #SwiftArchitect for most of this. I added in the shapes portion. As #SwiftArchitect noted, there are other improvements to be made, but it works perfectly!
import UIKit
#IBDesignable class Symbol: UIView {
var color = UIColor.blackColor()
#IBInspectable var inShape: String = "Circle"
#IBInspectable var inColor: String = "Black" {
didSet {
switch inColor {
case "Green" : color = UIColor.greenColor()
case "Blue" : color = UIColor.blueColor()
case "Yellow" : color = UIColor.yellowColor()
case "Cyan" : color = UIColor.cyanColor()
case "Red" : color = UIColor.redColor()
case "Brown" : color = UIColor.brownColor()
case "Orange" : color = UIColor.orangeColor()
case "Purple" : color = UIColor.purpleColor()
case "Grey" : color = UIColor.darkGrayColor()
default: color = UIColor.blackColor()
}
}
}
override func drawRect(rect: CGRect) {
var path = UIBezierPath()
switch shape {
case "Circle": path = UIBezierPath(ovalInRect: rect)
case "Square" : path = UIBezierPath(roundedRect: rect, cornerRadius: 5.0)
case "Triangle" :
path.moveToPoint(CGPoint(x: frame.width / 2, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: frame.height))
path.addLineToPoint(CGPoint(x: frame.width, y: frame.height))
case "Diamond" :
path.moveToPoint(CGPoint(x: frame.width / 2, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: frame.height / 2))
path.addLineToPoint(CGPoint(x: frame.width / 2, y: frame.height))
path.addLineToPoint(CGPoint(x: frame.width, y: frame.height / 2))
default: print("unknown shape")
}
color.setFill()
path.fill()
}
}

Related

Is is possible to animate text color changing only in a part of text in iOS?

I wonder if it even possible in iOS to animate changing color in only a part of the text, preferably not char by char, but pixel by pixel, like on this picture?
I know how to change text color in static with NSAttributedString and I know how to animate the whole text with CADisplayLink, but this makes me worry.
Maybe I can dive into CoreText, but I'm still not sure it is possible even with it. Any thoughts?
UPD I decided to add a video with my first results to make the question more clear:
my efforts for now (the label is overlapping)
You can quite easily achieve this using CoreAnimation possibilities.
I've added a simple demo, you play with it here (just build the project and tap anywhere to see the animation).
The logic is the following:
Create a custom subclass of UIView.
When some text is set, create two similar CATextLayers, each with the same text and frame.
Set different foregroundColor and mask for those layers. The mask of the left layer will be the left part of the view, and the mask of the right layer will be the right part.
Animate foregroundColor for those layers (simultaneously).
The code of a custom view:
class CustomTextLabel: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .green
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var textLayer1: CATextLayer?
private var textLayer2: CATextLayer?
func setText(_ text: String, fontSize: CGFloat) {
// create 2 layers with the same text and size, we'll set the colors for them later
textLayer1 = createTextLayer(text, fontSize: fontSize)
textLayer2 = createTextLayer(text, fontSize: fontSize)
// estimate the frame size needed for the text layer with such text and font size
let textSize = textLayer1!.preferredFrameSize()
let w = frame.width, h = frame.height
// calculate the frame such that both layers will be in center of view
let centeredTextFrame = CGRect(x: (w-textSize.width)/2, y: (h-textSize.height)/2, width: textSize.width, height: textSize.height)
textLayer1!.frame = centeredTextFrame
textLayer2!.frame = centeredTextFrame
// set up default color for the text
textLayer1!.foregroundColor = UIColor.yellow.cgColor
textLayer2!.foregroundColor = UIColor.yellow.cgColor
// set background transparent, that's very important
textLayer1!.backgroundColor = UIColor.clear.cgColor
textLayer2!.backgroundColor = UIColor.clear.cgColor
// set up masks, such that each layer's text is visible only in its part
textLayer1!.mask = createMaskLayer(CGRect(x: 0, y: 0, width: textSize.width/2, height: textSize.height))
textLayer2!.mask = createMaskLayer(CGRect(x: textSize.width/2, y: 0, width: textSize.width/2, height: textSize.height))
layer.addSublayer(textLayer1!)
layer.addSublayer(textLayer2!)
}
private var finishColor1: UIColor = .black, finishColor2: UIColor = .black
func animateText(leftPartColor1: UIColor, leftPartColor2: UIColor, rightPartColor1: UIColor, rightPartColor2: UIColor) {
finishColor1 = leftPartColor2
finishColor2 = rightPartColor2
if let layer1 = textLayer1, let layer2 = textLayer2 {
CATransaction.begin()
let animation1 = CABasicAnimation(keyPath: "foregroundColor")
animation1.fromValue = leftPartColor1.cgColor
animation1.toValue = leftPartColor2.cgColor
animation1.duration = 3.0
layer1.add(animation1, forKey: "animation1")
let animation2 = CABasicAnimation(keyPath: "foregroundColor")
animation2.fromValue = rightPartColor1.cgColor
animation2.toValue = rightPartColor2.cgColor
animation2.duration = 3.0
layer2.add(animation2, forKey: "animation2")
CATransaction.setCompletionBlock {
self.textLayer1?.foregroundColor = self.finishColor1.cgColor
self.textLayer2?.foregroundColor = self.finishColor2.cgColor
}
CATransaction.commit()
}
}
private func createTextLayer(_ text: String, fontSize: CGFloat) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.string = text
textLayer.fontSize = fontSize // TODO: also set font name
textLayer.contentsScale = UIScreen.main.scale
return textLayer
}
private func createMaskLayer(_ holeRect: CGRect) -> CAShapeLayer {
let layer = CAShapeLayer()
let path = CGMutablePath()
path.addRect(holeRect)
path.addRect(bounds)
layer.path = path
layer.fillRule = CAShapeLayerFillRule.evenOdd
layer.opacity = 1
return layer
}
}
The calls of a custom view:
class ViewController: UIViewController {
var customLabel: CustomTextLabel!
override func viewDidLoad() {
super.viewDidLoad()
let viewW = view.frame.width, viewH = view.frame.height
let labelW: CGFloat = 200, labelH: CGFloat = 50
customLabel = CustomTextLabel(frame: CGRect(x: (viewW-labelW)/2, y: (viewH-labelH)/2, width: labelW, height: labelH))
customLabel.setText("Optimizing...", fontSize: 20)
view.addSubview(customLabel)
let tapRecogniner = UITapGestureRecognizer(target: self, action: #selector(onTap))
view.addGestureRecognizer(tapRecogniner)
}
#objc func onTap() {
customLabel.animateText(leftPartColor1: UIColor.blue,
leftPartColor2: UIColor.red,
rightPartColor1: UIColor.white,
rightPartColor2: UIColor.black)
}
}
Thanks to Olha's (#OlhaPavliuk) answer, I used two CATextLayer shapes and two CAShapeLayer masks for text layers. In draw method I just change masks frames to calculated size (bounds.width * progress value), and also change the second mask origin to a new start (bounds.width - bounds.width * progress value).
Also, it was very important to set layer.fillRule = CAShapeLayerFillRule.evenOdd while creating a mask, so that both layers became visible.
It turned out that I actually didn't need any animation code involved, because changing frames looks just ok.
In motion: https://giphy.com/gifs/LMbmlMoxY9oaWhXfO1
Full code: https://gist.github.com/joliejuly/a792c2ab8d97d304d731a4a5202f741a

Incorrect upper and lower line color with UIGraphicsGetCurrentContext

The project uses ASDK (Texture). There is a main node, it has a subnode, under certain circumstances it is necessary to show the outer boundary of the subnode, in this project it is done by adding a border node and UIGraphicsGetCurrentContext, also the task is to border line width be the same on all devices regardless of UIScreen.main.scale. I did a test code and everything works fine, except that the lower and upper lines have a smaller width than the side lines (which are displayed correctly). Please tell how to fix it? I can not spread the screenshoot of the whole screen, but I uploaded the part so I can show what the problem is. You can see the green color on the bottom line, this should not be.
private func enableEditingMode() {
let solidBorderStyle = BorderStyle.solid
let border = ASBorderNode(shapeType: self.shapeType, borderType: solidBorderStyle)
border.frame = ASBorderNode.calculateFrame(self.shapeType, borderType: solidBorderStyle, superView: frame)
supernode?.insertSubnode(border, at: 0)
}
enum BorderStyle {
case dashed
case solid
case solidLight
var lineWidth: CGFloat {
switch self {
case .dashed:
return 1
case .solid:
return 6 / UIScreen.main.scale
case .solidLight:
return 2
}
}
}
class ASBorderNode: ASDisplayNode {
private (set) var shapeType: TemplateLayoutElement.Shape?
private (set) var borderType: BorderStyle
init(shapeType:TemplateLayoutElement.Shape?, borderType:BorderStyle = .dashed) {
self.borderType = borderType
self.shapeType = shapeType
super.init()
self.isOpaque = false
}
class func calculateFrame(_ shapeType: TemplateLayoutElement.Shape?, borderType: BorderStyle, superView frame: CGRect) -> CGRect {
switch borderType {
case .solid:
let lineWidth = borderType.lineWidth //* UIScreen.main.scale
let widthHeightValue = lineWidth * 2
return CGRect(x: -lineWidth + frame.origin.x, y: -lineWidth + frame.origin.y, width: frame.width + widthHeightValue, height: frame.height + widthHeightValue)
default:
assertionFailure("Implement for specific style")
return .zero
}
}
override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled isCancelledBlock: () -> Bool, isRasterizing: Bool) {
if let context = UIGraphicsGetCurrentContext(),
let parameters = parameters as? ASBorderNode {
switch parameters.borderType {
case .dashed:
break
case .solid:
context.setStrokeColor(UIColor.black.withAlphaComponent(0.5).cgColor)
context.setFillColor(UIColor.clear.cgColor)
context.setLineWidth(parameters.borderType.lineWidth)
case .solidLight:
break
}
switch parameters.shapeType {
case .circle?:
break
default:
switch parameters.borderType {
case .solid:
debugPrint("Bounds", bounds)
let frame = bounds.insetBy(dx: parameters.borderType.lineWidth / UIScreen.main.scale, dy: parameters.borderType.lineWidth / UIScreen.main.scale)
debugPrint("insetBy", frame)
context.addRect(frame)
default:
break
}
}
context.strokePath()
}
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return self
}
}
Update 1: Another example
I believe what you're seeing is a stroke artifact. The only way I know how to avoid them is to build a path that corresponds to what you want the stroke shape to be and use fill (so you're layering filled objects on top of each other, rather than just stroking the base path).

iOS swift: add kern space in the tab bar text

I'm not able to add kern space into the tab bar attributed text.
The UITabBar in question is a custom tabBar, you can find the code below.
I'm using the "attributed key" dictionary to add attributes to the items title, but I'm having an issue with the kern space.
class ProfileTabBar: UITabBar {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setStyle()
}
required override init(frame: CGRect) {
super.init(frame: frame)
self.setStyle()
}
func setStyle() {
self.tintColor = Style.shared.primary1
// Disable the default border
self.layer.borderWidth = 0.0
self.clipsToBounds = true
// Create a new bottom border
let bottomLine = CALayer()
let screenWidth = UIScreen.main.bounds.width
//let viewForFrame = self.superview ?? self
//let screenWidth = viewForFrame.bounds.width
bottomLine.frame = CGRect(x: 0.0, y: self.frame.height - 1, width: screenWidth, height: 2.0)
bottomLine.backgroundColor = UIColor(red: 235.0/255, green: 235.0/255, blue: 235.0/255, alpha: 1.0).cgColor
self.layer.addSublayer(bottomLine)
// Get the size of a single item
let markerSize = CGSize(width: screenWidth/CGFloat(self.items!.count), height: self.frame.height)
// Create the selection indicator
self.selectionIndicatorImage = UIImage().createSelectionIndicator(color: self.tintColor, size: markerSize , lineWidth: 3.0)
// Customizing the items
if let items = self.items {
for item in items {
item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -15)
let attributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font: UIFont(name: Style.shared.fontBold.fontName, size: 14) as Any,
NSAttributedStringKey.kern: NSNumber(value: 1.0)
]
item.setTitleTextAttributes(attributes, for: .normal)
}
}
}
All the attributes works except for the kern. What I'm doing wrong?
This question is old and there is an even older answer here. It appears that UITabBarItem appearance ignores NSAttributedString.Key.kern. That leaves us with a few options.
Subclass UITabBarItem this isn't easy because UITabBarItem inherits from UIBarItem which is an NSObject not a UIView.
Subclass UITabBar this can be done, but involves a decent amount of work for just some kern. You'll have to use UIButton instead of UITabBarItem so that the kern is applied.
You can add spacing using unicode characters in your title. This is really easy and can probably achieve the spacing you're looking for with just a few lines of code.
Unicode spacing:
U+0020 1/4 em
U+2004 1/3 em
U+2005 1/4 em
U+2006 1/6 em
U+2008 The width of a period “.”
U+2009 1/5 em (or sometimes 1/6 em)
You can use a unicode character in a String in Swift like this "\u{2006}". That means we can insert a small space between all the characters in our tabBarItem title. Like this:
extension String {
var withOneSixthEmSpacing: String {
let letters = Array(self)
return letters.map { String($0) + "\u{2006}" }.joined()
}
Using this for our tabBarItems:
self.navigationController.tabBarItem = UITabBarItem(
title: "Home".withOneSixthEmSpacing,
image: homeImage,
selectedImage: homeSelectedImage
)
Visually we end up with:
Instead of:
Another workaround is to subclass UITabBarController, and set the kerning in viewDidLayoutSubviews.
class FooTabBarController: UITabBarController {
private var tabBarButtons: [UIControl] {
tabBar.subviews.compactMap { $0 as? UIControl }
}
private var tabBarButtonLabels: [UILabel] {
tabBarButtons.compactMap { $0.subviews.first { $0 is UILabel } as? UILabel }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tabBarButtonLabels.forEach {
if let attributedText = $0.attributedText {
let mutable = NSMutableAttributedString(attributedString: attributedText)
mutable.addAttribute(.kern, value: 0.5, range: .init(location: 0, length: mutable.length))
$0.attributedText = mutable
$0.sizeToFit()
}
}
}
}
The caveats to this solution are:
It is somewhat fragile. It can break if Apple changes the view structure in the tab bar, ie if they stop using UIControl, or if they change the subview heirarchy.
It isn't all that efficient because the kerning has to be set every layout cycle.
I loved #DoesData's answer, it really helped me out a lot.
Here's a more "swifty" version of it I came up with if it helps anyone:
extension String {
var withAddedSpacing: String {
Array(self)
.compactMap { String($0) }
.joined(separator: "\u{2006}")
}
}

Adding border with width to UIView show small background outside

I'm trying to add circle border to a UIView with green background, I created simple UIView subclass with borderWidth, cornerRadius and borderColor properties and I'm setting it from storyboard.
#IBDesignable
class RoundedView: UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable var borderColor: UIColor {
get {
if let color = layer.borderColor {
return UIColor(cgColor: color)
} else {
return UIColor.clear
}
}
set {
layer.borderColor = newValue.cgColor
}
}
}
But when I compile and run an app or display it in InterfaceBuilder I can see a line outside the border that is still there (and is quite visible on white background).
This RoundedView with green background, frame 10x10, corner radius = 5 is placed in corner of plain UIImageView (indicates if someone is online or not). You can see green border outside on both UIImageView and white background.
Can you please tell me what's wrong?
What you are doing is relying on the layer to draw your border and round the corners. So you are not in charge of the result. You gave it a green background, and now you are seeing the background "stick out" at the edge of the border. And in any case, rounding the corners is a really skanky and unreliable way to make a round view. To make a round view, make a round mask.
So, the way to make your badge is to take complete charge of what it is drawn: you draw a green circle in the center of a white background, and mask it all with a larger circle to make the border.
Here is a Badge view that will do precisely what you're after, with no artifact round the outside:
class Badge : UIView {
class Mask : UIView {
override init(frame:CGRect) {
super.init(frame:frame)
self.isOpaque = false
self.backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.fillEllipse(in: CGRect(origin:.zero, size:rect.size))
}
}
let innerColor : UIColor
let outerColor : UIColor
let innerRadius : CGFloat
var madeMask = false
init(frame:CGRect, innerColor:UIColor, outerColor:UIColor, innerRadius:CGFloat) {
self.innerColor = innerColor
self.outerColor = outerColor
self.innerRadius = innerRadius
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.setFillColor(outerColor.cgColor)
con.fill(rect)
con.setFillColor(innerColor.cgColor)
con.fillEllipse(in: CGRect(
x: rect.midX-innerRadius, y: rect.midY-innerRadius,
width: 2*innerRadius, height: 2*innerRadius))
if !self.madeMask {
self.madeMask = true // do only once
self.mask = Mask(frame:CGRect(origin:.zero, size:rect.size))
}
}
}
I tried this with a sample setting as follows:
let v = Badge(frame: CGRect(x:100, y:100, width:16, height:16),
innerColor: .green, outerColor: .white, innerRadius: 5)
self.view.addSubview(v)
It looks fine. Adjust the parameters as desired.
I solved this by using a UIBezierPath and adding to the view's layer:
let strokePath = UIBezierPath(roundedRect: view.bounds, cornerRadius: view.frame.width / 2)
let stroke = CAShapeLayer()
stroke.frame = bounds
stroke.path = strokePath.cgPath
stroke.fillColor = .green.cgColor
stroke.lineWidth = 1.0
stroke.strokeColor = .white.cgColor
view.layer.insertSublayer(stroke, at: 2)
I solved this problem with gradients.
Just seting the backgroundColor of your circle as gradient.
let gradientLayer = CAGradientLayer()
//define colors
gradientLayer.colors = [<<your_bgc_color>>>>, <<border__bgc__color>>]
//define locations of colors as NSNumbers in range from 0.0 to 1.0
gradientLayer.locations = [0.0, 0.7]
//define frame
gradientLayer.frame = self.classView.bounds
self.classView.layer.insertSublayer(gradientLayer, at: 0)
MyImage
An easier fix might be to just mask it like this:
let mask = UIView()
mask.backgroundColor = .black
mask.frame = yourCircleView.bounds.inset(by: UIEdgeInsets(top: 0.1, left: 0.1, bottom: 0.1, right: 0.1))
mask.layer.cornerRadius = mask.height * 0.5
yourCircleView.mask = mask

Making a multicolor bar in swift

I'm trying to make a line (so basically UIView) that has fixed height and width and is divided to nine segments. I want to be able to control the height of each segment and its color. E.g. I want the first segment be yellow and 30% of the total height of the line, the second to be red and 8% of the total height etc.
I'm not really skilled in Swift, so my solution would be to make 9 UIViews, stack them on top of each other on my storyboard and then manually set the height and background color of every view, so they'd seem like a one multicolored line. Is there cleaner and less bulky solution? Thanks
I would highly recommend using Core Graphics for this.
As the drawing is dead simple (you just want to stack some colored lines within a view), you can easily achieve this by subclassing UIView and overriding drawRect() and drawing them in Core Graphics.
It's certainly a much cleaner solution than adding 9 subviews!
Something like this should achieve the desired result:
class LineView : UIView {
let colors:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.greenColor()]
let values:[CGFloat] = [0.35, 0.45, 0.2]
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]) // set fill color to the given color
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Going further...
You could allow the colors and values properties to be changed from outside the LineView class, allowing for much greater flexibility. You just have to override the didSet to trigger the view to be redrawn when the properties change.
For example:
class LineView : UIView {
/// An array of optional UIColors (clearColor is used when nil is provided) defining the color of each segment.
var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
/// An array of CGFloat values to define how much of the view each segment occupies. Should add up to 1.0.
var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]?.CGColor ?? UIColor.clearColor().CGColor) // set fill color to the given color if it's provided, else use clearColor
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Usage:
let lineView = LineView(frame: CGRectMake(50, 50, 20, view.bounds.size.height-100))
lineView.colors = [
UIColor(red: 1.0, green: 31.0/255.0, blue: 73.0/255.0, alpha: 1.0), // red
UIColor(red:1.0, green: 138.0/255.0, blue: 0.0, alpha:1.0), // orange
UIColor(red: 122.0/255.0, green: 108.0/255.0, blue: 1.0, alpha: 1.0), // purple
UIColor(red: 0.0, green: 100.0/255.0, blue: 1.0, alpha: 1.0), // dark blue
UIColor(red: 100.0/255.0, green: 241.0/255.0, blue: 183.0/255.0, alpha: 1.0), // green
UIColor(red: 0.0, green: 222.0/255.0, blue: 1.0, alpha: 1.0) // blue
]
lineView.values = [0.15, 0.1, 0.35, 0.15, 0.1, 0.15]
view.addSubview(lineView);
(I've only added 6 colors here, but you can add as many as you want).
Full project: https://github.com/hamishknight/Color-Segment-Line-View
I've just realized that this was not what you needed.
I leave the answer anyway so that maybe could be helpful to somebody else in the future.
Make sure your line view has it's own UIView subclass, so that we can override drawRect and achieve your goal.
Then a simple implementation would be:
class BarLine: UIView {
override func drawRect(rect: CGRect) {
//Height of each segment, in percentage
var heights : [CGFloat] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
//Lets create 9 rects and set each rect width to be 1/9th of the view size, then add them to the array
let width : CGFloat = rect.size.width / 9.0
var i : Int = Int()
//Loop to generate 9 segmnets
for (i = 0; i < 9; i++) {
//Each rect origin must be translated by i * width
let origin = CGPointMake(CGFloat(i) * width, rect.height)
//Generate a random color
let color = UIColor(red: heights[i], green: 0.5, blue: 0.5, alpha: 1)
let segment = CGRect(x: origin.x, y: origin.y, width: width, height: -heights[i] * rect.height)
//Set the color
color.set()
//Add the segment to the view by drawing it
UIRectFill(segment)
}
}
}
This will produce something like :
(Remember to set your UIView class to you custom class in IB)
I hope this helped
To make #Hamish code compatible for Swift 5, here the LineView class (and invert width & height in draw->fill to make it horizontal):
import UIKit
public class ColorLineView : UIView {
public var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
public var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override func draw(_ rect: CGRect) {
let r = self.bounds
let numberOfSegments = values.count
guard let ctx = UIGraphicsGetCurrentContext() else { return }
var cumulativeValue:CGFloat = 0
for i in 0..<numberOfSegments {
ctx.setFillColor(colors[i]?.cgColor ?? UIColor.clear.cgColor)
ctx.fill(CGRect(x: 0, y: cumulativeValue*r.size.height, width: r.size.width, height: values[i]*r.size.height))
cumulativeValue += values[i]
}
}
}

Resources