Adding CAShapeLayer to UIButton but it appears under the border line - ios

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

Related

How to get Width or Height of a UIView when I'm using "Equal width" constrain

I need help to solve this problem: I have a UITextFiled and I'm trying to apply a border at the bottom using this code:
func addBottomBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height: width)
self.layer.addSublayer(border)
}
The problem is that the result is not correct, the border goes outside the textfield because in the text Field I'm using the "Equal width constrain" and the Width at design time is not the same Width at "Didload()" time. There is a way to get the width to the textField after "Equal width constrain" correction?
A much better approach is to
subclass UITextField
create the "underline border" layer on initialization
change the frame of that layer in layoutSubviews()
Example:
#IBDesignable
class UnderlinedTextField: UITextField {
let underlineLayer: CALayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(underlineLayer)
underlineLayer.backgroundColor = UIColor.black.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
underlineLayer.frame = CGRect(x: 0, y: bounds.height - 2.0, width: bounds.width, height: 2)
}
}
Result (I gave the text field a background color of .cyan to make it easy to see):
It automatically resizes the "underline" when the field size changes - such as on device rotation:
Note that, by making it #IBDesignable, you can also see the underline layer during design-time.
This example uses a default color of black for the "underline" but you can change it via code just like any other property change, e.g.:
testField.underlineLayer.backgroundColor = UIColor.red.cgColor
Override bounds variable and call your border drawing in didSet. Your layer would be updated every time view changes bounds.
var border = CALayer()
init(frame: CGRect) {
super.init(farme: frame)
self.layer.addSublayer(border)
}
override var bounds: CGRect {
didSet {
addBottomBorderWithColor(color: .black, width: 2)
}
}
func addBottomBorderWithColor(color: UIColor, width: CGFloat) {
border.backgroundColor = color.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height: width)
self.layer.setNeedsLayout()
}
I found a possible solution by myself (not the perfect one).
Because the Constrains are probably applied after DidLoad() and after viewDidLayoutSubviews(), I called the function to add the border inside the function viewDidAppear(). Now it works even if the new borders are shown with a small delay.
The best way is sub-class a UITextFiled as described here.
Custom class that can be applied to every UITextField - Swift
In this case there the object is created correctly

UITextField subclass with bar underneath: bar not going all the way

The objective
I've made my own UITextField subclass, to design it in such a way that there is no border, only a white line under the text. (Subclass code below)
The issue
Unfortunately, the white line does not go to the end of the text field (in terms of width):
I've constrained the text field to have 0 px distance to both the left and right edges of the view, but as the images below show, the line does not meet those constraints.
My investigation and hypotheses
I've put in quite some time to debug, the only thing I can think of is that the dimensions specified in the code (copied below) are exécute before the constraints are applied to the view, which is not something I know how to resolve.
The code
//In a UITextField subclass
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override func awakeFromNib() {
super.awakeFromNib()
setup()
}
func setup() {
self.delegate = self
// BORDER
// Remove rectangular border
self.borderStyle = .none
self.layer.borderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0).cgColor //Make transparent
self.layer.borderWidth = 0 // width = 0
// Add bottom border
bottomBorder = UIView(frame: CGRect(x: 0, y: self.frame.height, width: self.frame.width, height: 1))
bottomBorder.backgroundColor = UIColor.white
self.addSubview(bottomBorder)
}
the only thing I can think of is that the dimensions specified in the code (copied below) are exécute before the constraints are applied to the view,
Perfectly correct! The whole problem is that you are hard-coding the frame of the bar based on the frame of the text field. But the frame of the text field is not yet known during init.
Your entire approach here is wrong. You should be using autolayout constraints to constrain the bar to the text field, not giving the bar a fixed frame. That way, no matter what happens to the size of the text field (as it obeys the constraints you have given it), the size of the bar will follow along in perfect harmony.
You need
override func layoutSubviews() {
super.layoutSubviews()
bottomBorder.frame = CGRect(x: 0, y: self.frame.height - 1, width: self.frame.width, height: 1)
}
As correct frame size of superView is captured inside layoutSubviews not in init / awakeFromNib where the frame is set for current development device dimensions if it's in IB

How to create an IBDesignable custom UIView from a UIBezierPath?

I want to shape a UIView and be able to see its shape in the Interface Builder, when I set the following class to my UIView it fails to build, I'm not sure where's the error.
#IBDesignable
class CircleExampleView: UIView {
override func layoutSubviews() {
setupMask()
}
func setupMask() {
let path = makePath()
// mask the whole view to that shape
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
private func makePath() -> UIBezierPath {
//// Oval Drawing
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 11, y: 12, width: 30, height: 30))
UIColor.gray.setFill()
ovalPath.fill()
return ovalPath
}
}
A couple of observations:
You are calling setFill followed by fill, but you only do that if drawing into a graphics context (e.g., in draw(_:)). When using CAShapeLayer, you should instead set the fillColor of the CAShapeLayer.
You are using the CAShapeLayer to set the mask of your view. If your UIView doesn't have a discernible backgroundColor, you won't see anything.
If you set the background color of the view to, say, blue, as shown below, your mask will reveal that blue background wherever the mask allows it to (in the oval of your path).
You have implemented layoutSubviews. You generally would do that only if you were doing something here that was contingent upon the bounds of the view. For example, here's a rendition where the oval path is based upon the bounds of the view:
#IBDesignable
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
setupMask()
}
private func setupMask() {
let mask = CAShapeLayer()
mask.path = path.cgPath
mask.fillColor = UIColor.gray.cgColor
layer.mask = mask
}
private var path: UIBezierPath {
return UIBezierPath(ovalIn: bounds)
}
}
As E. Coms said, if you override layoutSubviews, you really should call the super implementation. This isn't critical, as the default implementation actually does nothing, but it's best practice. E.g. if you later changed this class to subclass some other UIView subclass, you don't want to have to go to revisit all these overrides.
If you have a designable view, it's advisable to put that in a separate target. That way, the rendering of the view in the storyboard is not dependent upon any work that may be underway in the main project. As long as the designables target (often the name of your main target with Kit suffix) can build, the designable view can be rendered.
For example, here is a rendition of your designable view, in a separate framework target, and used in a storyboard where the view in question has a blue backgroundColor:
For what it's worth, I think it's exceedingly confusing to have to mask to reveal the background color inside the oval. An app developer has to set "background" color in order to set what's inside the oval, but not the background.
I might instead remove the "mask" logic and instead give the designable view an inspectable property, fillColor, and just add a CAShapeLayer as a sublayer, using that fillColor:
#IBDesignable
class CircleView: UIView {
private var shapeLayer = CAShapeLayer()
#IBInspectable var fillColor: UIColor = .blue {
didSet {
shapeLayer.fillColor = fillColor.cgColor
}
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
shapeLayer.fillColor = fillColor.cgColor
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
}
This accomplishes the same thing, but I think the distinction of fill colors vs background colors is more intuitive. But you may have had other reasons for using the masking approach, but just make sure if you do that, that you have something to reveal after it’s masked (e.g. a background color or something else you’re rendering).
Please add "super.layoutSubviews()"
override func layoutSubviews() {
setupMask()
super.layoutSubviews()
}
By this way, your design view will be "up to date" .

Creating a shadow for a UIImageView that has rounded corners?

I am trying to create an ImageView that has rounded corners and a shadow to give it some depth. I was able to create a shadow for the UIImageView, but whenever I added the code to also make it have rounded corners, it only had rounded corners with no shadow. I have an IBOutlet named myImage, and it is inside of the viewDidLoad function. Does anybody have any ideas on how to make it work? What am I doing wrong?
override func viewDidLoad() {
super.ViewDidLoad()
myImage.layer.shadowColor = UIColor.black.cgColor
myImage.layer.shadowOpacity = 1
myImage.layer.shadowOffset = CGSize.zero
myImage.layer.shadowRadius = 10
myImage.layer.shadowPath = UIBezierPath(rect: myImage.bounds).cgPath
myImage.layer.shouldRasterize = false
myImage.layer.cornerRadius = 10
myImage.clipsToBounds = true
}
If you set clipsToBounds to true, this will round the corners but prevent the shadow from appearing. In order to resolve this, you can create two views. The container view should have the shadow, and its subview should have the rounded corners.
The container view has clipsToBounds set to false, and has the shadow properties applied. If you want the shadow to be rounded as well, use the UIBezierPath constructor that takes in a roundedRect and cornerRadius.
let outerView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
outerView.clipsToBounds = false
outerView.layer.shadowColor = UIColor.black.cgColor
outerView.layer.shadowOpacity = 1
outerView.layer.shadowOffset = CGSize.zero
outerView.layer.shadowRadius = 10
outerView.layer.shadowPath = UIBezierPath(roundedRect: outerView.bounds, cornerRadius: 10).cgPath
Next, set the image view (or any other type of UIView) to be the same size of the container view, set clipsToBounds to true, and give it a cornerRadius.
let myImage = UIImageView(frame: outerView.bounds)
myImage.clipsToBounds = true
myImage.layer.cornerRadius = 10
Finally, remember to make the image view a subview of the container view.
outerView.addSubview(myImage)
The result should look something like this:
Swift 5:
You can use the below extension:
extension UIImageView {
func applyshadowWithCorner(containerView : UIView, cornerRadious : CGFloat){
containerView.clipsToBounds = false
containerView.layer.shadowColor = UIColor.black.cgColor
containerView.layer.shadowOpacity = 1
containerView.layer.shadowOffset = CGSize.zero
containerView.layer.shadowRadius = 10
containerView.layer.cornerRadius = cornerRadious
containerView.layer.shadowPath = UIBezierPath(roundedRect: containerView.bounds, cornerRadius: cornerRadious).cgPath
self.clipsToBounds = true
self.layer.cornerRadius = cornerRadious
}
}
How to use:
Drag a UIView on the storyboard
Drag an ImageView inside that UIView
Storyboard should look like this:
Create IBOutlet for both Views, call extension on your ImageView, and pass above created UIView as an argument.
Here is the output :
Finally here is how to
Properly have an image view, with rounded corners AND shadows.
It's this simple:
First some bringup code ..
class ShadowRoundedImageView: UIView {
override init(frame: CGRect) {
super.init(frame: frame); common() }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder); common() }
private func common() {
backgroundColor = .clear
clipsToBounds = false
self.layer.addSublayer(shadowLayer)
self.layer.addSublayer(imageLayer) // (in that order)
}
#IBInspectable var image: UIImage? = nil {
didSet {
imageLayer.contents = image?.cgImage
shadowLayer.shadowPath = (image == nil) ? nil : shapeAsPath
}
}
and then the layers ...
var imageLayer: CALayer = CALayer()
var shadowLayer: CALayer = CALayer()
var shape: UIBezierPath {
return UIBezierPath(roundedRect: bounds, cornerRadius:50)
}
var shapeAsPath: CGPath {
return shape.cgPath
}
var shapeAsMask: CAShapeLayer {
let s = CAShapeLayer()
s.path = shapeAsPath
return s
}
override func layoutSubviews() {
super.layoutSubviews()
imageLayer.frame = bounds
imageLayer.contentsGravity = .resizeAspectFill // (as preferred)
imageLayer.mask = shapeAsMask
shadowLayer.shadowPath = (image == nil) ? nil : shapeAsPath
shadowLayer.shadowOpacity = 0.80 // etc ...
}
}
Here is the
Explanation
UIImageView is useless, you use a UIView
You need two layers, one for the shadow and one for the image
To round an image layer you use a mask
To round a shadow layer you use a path
For the shadow qualities, obviously add code as you see fit
shadowLayer.shadowOffset = CGSize(width: 0, height: 20)
shadowLayer.shadowColor = UIColor.purple.cgColor
shadowLayer.shadowRadius = 5
shadowLayer.shadowOpacity = 0.80
For the actual shape (the bez path) make it any shape you wish.
(For example this tip https://stackoverflow.com/a/41553784/294884 shows how to make only one or two corners rounded.)
Summary:
• Use two layers on a UIView
Make your bezier and ...
• Use a mask on the image layer
• Use a path on the shadow layer
Here is a another solution (tested code) in swift 2.0
If you set clipsToBounds to true, this will round the corners but prevent the shadow from appearing. So, you can add same size UIView in storyboard behind imageview and we can give shadow to that view
SWIFT 2.0
outerView.layer.cornerRadius = 20.0
outerView.layer.shadowColor = UIColor.blackColor().CGColor
outerView.layer.shadowOffset = CGSizeMake(0, 2)
outerView.layer.shadowOpacity = 1
outerView.backgroundColor = UIColor.whiteColor()
You can use a simple class I have created to add image with rounded corners and shadow directly from Storyboard
You can find the class here

How to add shadow to ImageView? [duplicate]

I am trying to create an ImageView that has rounded corners and a shadow to give it some depth. I was able to create a shadow for the UIImageView, but whenever I added the code to also make it have rounded corners, it only had rounded corners with no shadow. I have an IBOutlet named myImage, and it is inside of the viewDidLoad function. Does anybody have any ideas on how to make it work? What am I doing wrong?
override func viewDidLoad() {
super.ViewDidLoad()
myImage.layer.shadowColor = UIColor.black.cgColor
myImage.layer.shadowOpacity = 1
myImage.layer.shadowOffset = CGSize.zero
myImage.layer.shadowRadius = 10
myImage.layer.shadowPath = UIBezierPath(rect: myImage.bounds).cgPath
myImage.layer.shouldRasterize = false
myImage.layer.cornerRadius = 10
myImage.clipsToBounds = true
}
If you set clipsToBounds to true, this will round the corners but prevent the shadow from appearing. In order to resolve this, you can create two views. The container view should have the shadow, and its subview should have the rounded corners.
The container view has clipsToBounds set to false, and has the shadow properties applied. If you want the shadow to be rounded as well, use the UIBezierPath constructor that takes in a roundedRect and cornerRadius.
let outerView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
outerView.clipsToBounds = false
outerView.layer.shadowColor = UIColor.black.cgColor
outerView.layer.shadowOpacity = 1
outerView.layer.shadowOffset = CGSize.zero
outerView.layer.shadowRadius = 10
outerView.layer.shadowPath = UIBezierPath(roundedRect: outerView.bounds, cornerRadius: 10).cgPath
Next, set the image view (or any other type of UIView) to be the same size of the container view, set clipsToBounds to true, and give it a cornerRadius.
let myImage = UIImageView(frame: outerView.bounds)
myImage.clipsToBounds = true
myImage.layer.cornerRadius = 10
Finally, remember to make the image view a subview of the container view.
outerView.addSubview(myImage)
The result should look something like this:
Swift 5:
You can use the below extension:
extension UIImageView {
func applyshadowWithCorner(containerView : UIView, cornerRadious : CGFloat){
containerView.clipsToBounds = false
containerView.layer.shadowColor = UIColor.black.cgColor
containerView.layer.shadowOpacity = 1
containerView.layer.shadowOffset = CGSize.zero
containerView.layer.shadowRadius = 10
containerView.layer.cornerRadius = cornerRadious
containerView.layer.shadowPath = UIBezierPath(roundedRect: containerView.bounds, cornerRadius: cornerRadious).cgPath
self.clipsToBounds = true
self.layer.cornerRadius = cornerRadious
}
}
How to use:
Drag a UIView on the storyboard
Drag an ImageView inside that UIView
Storyboard should look like this:
Create IBOutlet for both Views, call extension on your ImageView, and pass above created UIView as an argument.
Here is the output :
Finally here is how to
Properly have an image view, with rounded corners AND shadows.
It's this simple:
First some bringup code ..
class ShadowRoundedImageView: UIView {
override init(frame: CGRect) {
super.init(frame: frame); common() }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder); common() }
private func common() {
backgroundColor = .clear
clipsToBounds = false
self.layer.addSublayer(shadowLayer)
self.layer.addSublayer(imageLayer) // (in that order)
}
#IBInspectable var image: UIImage? = nil {
didSet {
imageLayer.contents = image?.cgImage
shadowLayer.shadowPath = (image == nil) ? nil : shapeAsPath
}
}
and then the layers ...
var imageLayer: CALayer = CALayer()
var shadowLayer: CALayer = CALayer()
var shape: UIBezierPath {
return UIBezierPath(roundedRect: bounds, cornerRadius:50)
}
var shapeAsPath: CGPath {
return shape.cgPath
}
var shapeAsMask: CAShapeLayer {
let s = CAShapeLayer()
s.path = shapeAsPath
return s
}
override func layoutSubviews() {
super.layoutSubviews()
imageLayer.frame = bounds
imageLayer.contentsGravity = .resizeAspectFill // (as preferred)
imageLayer.mask = shapeAsMask
shadowLayer.shadowPath = (image == nil) ? nil : shapeAsPath
shadowLayer.shadowOpacity = 0.80 // etc ...
}
}
Here is the
Explanation
UIImageView is useless, you use a UIView
You need two layers, one for the shadow and one for the image
To round an image layer you use a mask
To round a shadow layer you use a path
For the shadow qualities, obviously add code as you see fit
shadowLayer.shadowOffset = CGSize(width: 0, height: 20)
shadowLayer.shadowColor = UIColor.purple.cgColor
shadowLayer.shadowRadius = 5
shadowLayer.shadowOpacity = 0.80
For the actual shape (the bez path) make it any shape you wish.
(For example this tip https://stackoverflow.com/a/41553784/294884 shows how to make only one or two corners rounded.)
Summary:
• Use two layers on a UIView
Make your bezier and ...
• Use a mask on the image layer
• Use a path on the shadow layer
Here is a another solution (tested code) in swift 2.0
If you set clipsToBounds to true, this will round the corners but prevent the shadow from appearing. So, you can add same size UIView in storyboard behind imageview and we can give shadow to that view
SWIFT 2.0
outerView.layer.cornerRadius = 20.0
outerView.layer.shadowColor = UIColor.blackColor().CGColor
outerView.layer.shadowOffset = CGSizeMake(0, 2)
outerView.layer.shadowOpacity = 1
outerView.backgroundColor = UIColor.whiteColor()
You can use a simple class I have created to add image with rounded corners and shadow directly from Storyboard
You can find the class here

Resources