ContainerView with shadow and rounded edges - ios

I would like to create custom ContainerView with shadowed and rounded edges. This ContainerView is in form of small rectangle placed on the top of another UIView. In this peculiar situation neither additional layers nor drawing shadow using CoreGraphics are helpful.

You're wrong that additional views/layers won't help.
You can place roundedContainer with rounded corners into another shadowedView with shadow added to it's layer.
To avoid those white corners make sure you set background color to clear somewhere.
Example:
//superview for container with rounded corners
shadowedView.backgroundColor = UIColor.clear //this will fix your white corners issue
shadowedView.layer.shadowColor = UIColor.black.cgColor
shadowedView.layer.shadowOffset = .zero
shadowedView.layer.shadowOpacity = 0.3
shadowedView.layer.shadowRadius = 5.0
//add a container with rounded corners
let roundedView = UIView()
roundedView.frame = baseView.bounds
roundedView.layer.cornerRadius = 10
roundedView.layer.masksToBounds = true
shadowedView.addSubview(roundedView)

I found a proper solution. I dropped shadow to ContainerView which is a superclass for every UIView inside. Then, I rounded edges using UIViewController class for this small rectangle area.
class GraphViewController: UIViewController {
#IBOutlet var graphView: GraphViewRenderer!
override func viewDidLoad() {
graphView.layer.cornerRadius = 20.0
graphView.layer.masksToBounds = true
super.viewDidLoad()
}
}
class GraphContainerView: UIView {
func applyPlainShadow() {
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize.zero
layer.shadowRadius = 5.0
layer.shadowOpacity = 0.7
}
override init(frame: CGRect) {
super.init(frame: frame)
applyPlainShadow()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
applyPlainShadow()
}
}

Related

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

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

Interface Builder Clipping Designable Views

I really need a hand here. I have created an #IBDesignable subclass of UILabel which works fine in the XCode Interface Builder. However, even if I set 'clipsToBounds' to false, Interface Builder will still clip it whilst changing the #IBInspectable properties works.
If I'm running the app on simulator or device, the UILabel isn't clipped and gives me the desired results (whilst still applying the values that Interface Builder has).
BEFORE THE CHANGE (The subviews are visible)
AFTER THE CHANGE IN INTERFACE BUILDER (The subviews are out of view)
AFTER THE CHANGE IN SIMULATOR (The subviews are as expected)
Any help would be massively appreciated. The code for the Custom Class is below.
#IBDesignable class UIFeaturedLabel: UILabel {
#IBInspectable var borderWidth: Float = 4
#IBInspectable var borderOffsetX: Float = 15
#IBInspectable var borderOffsetY: Float = 5
#IBInspectable var borderColor: UIColor = UIColor.whiteColor()
private var headerView:UIView!
private var footerView:UIView!
override init() {
super.init()
createViews()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createViews()
}
override init(frame: CGRect) {
super.init(frame: frame)
createViews()
}
func createViews() {
clipsToBounds = false
layer.masksToBounds = false
headerView = UIView()
footerView = UIView()
headerView.backgroundColor = UIColor.whiteColor()
footerView.backgroundColor = UIColor.whiteColor()
addSubview(headerView)
addSubview(footerView)
}
override func layoutSubviews() {
super.layoutSubviews()
let left = CGFloat( -borderOffsetX )
let right = CGFloat( frame.width + CGFloat(borderOffsetX*2) )
let top = CGFloat( -borderOffsetY )
let bottom = CGFloat( frame.height - CGFloat(borderWidth/2) ) + CGFloat( borderOffsetY )
headerView.frame = CGRectMake(left, top, right, CGFloat(borderWidth))
footerView.frame = CGRectMake(left, bottom, right, CGFloat(borderWidth))
}
}
Still occurring with XCode 7.3 iOS9.3, but fixed in XCode Version 8.0 beta (8S128d).

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