I want to add a red dot to the border of a UIButton. My current code for adding a dot is like so:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
override func setUp() {
super.setUp()
layer.borderWidth = borderWidth
layer.borderColor = normalBorderColor.cgColor
let redDotLayer = CAShapeLayer()
redDotLayer.path = CGPath(ellipseIn: CGRect(x: 30, y: -3.5, width: 8, height: 8), transform: nil)
redDotLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(redDotLayer)
}
However when I add the red dot it appears under the border line. I need it to be on top of the border line.
What am I doing wrong here?
Nothing wrong with your approach.
For example just consider adding subview to parent UIView, subview always stays inside the parent it can't be added above the parent.
Same scenario applies to CALayer . You can't addSublayer above parent layer.
As an exception,
Unlike views, a superlayer does not automatically clip the contents of sublayers that lie outside its bounds rectangle. Instead, the superlayer allows its sublayers to be displayed in their entirety by default. However, you can reenable clipping by setting the masksToBounds property of the layer to YES.
as per apple documentation sublayer can go beyond parent visible region, but not above the parent.
Even below methods won't help us.
- insertSublayer:atIndex:
- insertSublayer:above:
- insertSublayer:below:
addSublayer:
Appends the layer to the layer’s list of sublayers.
Solution
Draw another CAShapeLayer around the button.
CALayers may just draw the border around itself last. I think the easiest solution for you is just be to draw the border in a separate layer so that you can control the ordering.
override func setUp() {
super.setUp()
let borderLayer = CALayer()
borderLayer.borderWidth = borderWidth
borderLayer.borderColor = normalBorderColor.cgColor
layer.addSublayer(borderLayer)
let redDotLayer = CAShapeLayer()
redDotLayer.path = CGPath(ellipseIn: CGRect(x: 30, y: -3.5, width: 8, height: 8), transform: nil)
redDotLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(redDotLayer)
}
In my code, I was unable to use another layer as a border so I've used another UIView as a border.
An important part is to set the border UIView isUserInteractionEnabled to false so the responder chain would pass it to the UIButton:
let borderView = UIView(frame: .zero)
borderView.backgroundColor = .clear
borderView.layer.borderWidth = 0.5
borderView.layer.borderColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.18).cgColor
borderView.layer.cornerRadius = 19
// Dont't forget to set it to false or else the button won't work
borderView.isUserInteractionEnabled = false
I created CAShapeLayer border for view using the below code:
func addBorderToWebView() {
borderShape = CAShapeLayer()
borderShape.path = UIBezierPath(rect: webView.frame).cgPath
borderShape.lineWidth = CGFloat(2)
borderShape.bounds = borderShape.path!.boundingBox
borderShape.strokeColor = UIColor(red: 68/255, green: 219/255, blue: 94/255, alpha: 1.0).cgColor
borderShape.position = CGPoint(x: borderShape.bounds.width/2, y: borderShape.bounds.height/2)
webView.layer.addSublayer(borderShape)
}
I right and bottom border lines are out of the frame, you can see it on the image below. However, if I define borderWidth property of webView.layer, everything appears to be showed fine. That is why I make a conclusion, that the problem is not in layout constraints. How should I configure UIBezierPath (or CAShapeLayer) to make the border be showed correctly? Thank you for any help!
Try to replace this
borderShape.path = UIBezierPath(rect: webView.frame).cgPath
with this:
borderShape.path = UIBezierPath(rect: webView.bounds).cgPath
Another possible cause of the problem is that webView's frame is not updated yet when you set it to borderShape. You can try to do self.view.updateLayoutIfNeeded() prior to adding border shape. If it doesn't help override the following method:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateBorderShape()
}
func updateBorderShape() {
borderShape.bounds = webView.bounds
borderShape.position = CGPoint(x: borderShape.bounds.width/2, y: borderShape.bounds.height/2)
}
This will make sure that every time your frames are updated, border shape is updated as well.
I'm having trouble with transparency in drawRect. I have a custom UIView. If I draw a rect bezier path and fill it with gray the color will be solid without any transparency even though I have the alpha set to 0.6. :
import UIKit
class MyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let grayOverlay = UIBezierPath(rect: rect)
UIColor(red: 34, green: 211, blue: 121, alpha: 0.6).set()
grayOverlay.fill()
}
}
Am I missing something here?
Views have and -opaque property.
By default is set to true due to performance reasons. If the view is not opaque it will require transparent blending, that is more intensive.
By setting to false you agree to have transparency between your view and what it is displayed under.
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]
}
}
}
I have a PNG image of pedal in my view with transparency. I have implemented shadow according to that question, using Swift. Here I have custom UIViewWithShadow:
import UIKit
class UIViewWithShadow: UIView {
let image: UIImage!
var shadowOffset = CGSize.init(width: 5, height: 5)
var shadowBlur: CGFloat = 5
override func drawRect(rect: CGRect) {
let c = UIGraphicsGetCurrentContext();
CGContextSetShadow(c, shadowOffset, shadowBlur);
CGContextSetShadowWithColor(c, shadowOffset, shadowBlur, UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.8).CGColor)
self.image.drawAtPoint(CGPointMake(rect.minX, rect.minY)
}
}
Example of shadow change
It draws correctly, and now I need to animate that shadow - specificly, to decrease shadow's blur and offset when my image is pressed. I wonder if there is a way to do that like it is usually done with layer's shadow animation?
UPD
The goal is to make an illusion of pressing the pedal. I have done 3D transformation of it, the only thing I need is decreasing shadow under it, to make it look like the pedal became closer to the floor. If there are other ways to achieve that (like making shadow dependent on the Z-coordinate of pedal's view), I'll appreciate that