CAGradientLayer not fit perfectly on UITableViewCell bounds - swift

i am developing simple UITableView with custom UITableViewCell with Xib file
i have added MainView and pin it to ContentView in order to add all my views inside MainView
as shown below
UITableViewCell layout
when i try to add CAGradientLayer to MainView it doesnt fit perfectly on mainView bounds as i show below
whats happening on simulator
i use this piece of code to add CAGradientLayer
override func awakeFromNib() {
super.awakeFromNib()
let gradient = CAGradientLayer()
mainView.layer.cornerRadius = 10
gradient.frame = mainView.frame
gradient.colors = [ColorCompatibility.gradEnd.cgColor, ColorCompatibility.gradStart.cgColor]
gradient.endPoint = CGPoint(x: 0.5, y: 1)
gradient.startPoint = CGPoint(x: 0.5, y: 0)
// gradient.cornerRadius = 20
gradient.shadowColor = ColorCompatibility.systemBackground.cgColor
gradient.shadowOpacity = 0.8
gradient.shadowOffset = .zero
gradient.shadowRadius = 5
gradient.shadowPath = UIBezierPath(roundedRect: mainView.layer.bounds, cornerRadius: 20).cgPath
gradient.shouldRasterize = true
gradient.rasterizationScale = UIScreen.main.scale
contentView.layer.insertSublayer(gradient, at: 0)
}
how can i fix this?

Two issues here. The first is that in awakeFromNib the cell (and its content view) most likely doesn't have the correct frame so you could use another life cycle hook where the frame is correct but then you would run into problems when the device is rotated so the cell frame changes at runtime.
I would suggest to update the gradient layers frame in layoutSublayers(of:). Let's assume you created a property for the layer called gradient:
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
gradient.frame = mainView.bounds
}

Related

How to add an Gradient Layer to Image inside an CollectionView Cell

I have custom Collection View Cell which contains ImageViews and labels. I want to add gradient colour to the image view.
I tried all the possible solutions like adding mask and adding gradient to sublayer of the image. But nothing works, guide me in solving this issue
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.asanasImage.layer.bounds
gradientLayer.colors = [UIColor.clear.cgColor,UIColor.black.cgColor]
gradientLayer.locations = [0.7,1.2]
self.asanasImage.layer.addSublayer(gradientLayer)
Hie ! Trying setting the gradient for your imageView in the method
cellForItemAt(_ indexpath: IndexPath) method as its in a collection View Cell.
Use this method
func setGradientBackground(imageView: UIImageView, colorTop: UIColor, colorBottom: UIColor) {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [colorBottom.cgColor, colorTop.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.locations = [0, 1]
gradientLayer.frame = self.view.bounds
imageView.layer.insertSublayer(gradientLayer, at: 0)
}
Finally inside the cellForItemAt(_ indexpath: IndexPath) set the gradient of the imageView like so
setGradientBackground(imageView: cell.your_image_View, colorTop: any_color, colorBottom: any_color)
Customise the method according to your needs and the colors you want for your gradient.
Hope i was able to help. If it works lemme know and vote up please ! Thank You :D

Adding a CAShapeLayer to a subView

I trying to add a CAShapeLayer to a smaller UIView on the main view (single view application). I have the shape Layer animated and reacting to touch gesture to start drawing it. But it is drawn from the center of the main view and I need to be able to move it and constrain it for layout purposes. I have seen another post which seemed to have the same issues but i think i have confused myself more. I have the below code in the viewDidLoad function on the main ViewController.
It is a test project i am playing with to add new functionality to my existing app once I have it working.
I have tried adding a UIView in the MainStoryBoard of the project and adding linking it to the ViewController.swift file (control drag) to create an outlet then adding both shapeLayer & trackLayer to the UIView.
Code below
#IBOutlet weak var gaugeView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let centre = view.center
let circularPath = UIBezierPath(arcCenter: centre, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
// Track Layer Under Gauge
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
//view.layer.addSublayer(trackLayer)
gaugeView.layer.addSublayer(trackLayer)
// Animated Circular Guage
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 10
shapeLayer.lineCap = .round
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
//view.layer.addSublayer(shapeLayer)
gaugeView.layer.addSublayer(shapeLayer)
}
Once I have the app in the simulator it draws and animates the circle when the view.layer.addSubLayer is not commented out but will not add it to the UIView container with the gaugeView.layer.addSubView is in there.
You need to set the center to gaugeView center instead of view.center here,
override func viewDidLoad() {
super.viewDidLoad()
let centre = CGPoint(x: gaugeView.frame.width/2, y: gaugeView.frame.height/2)
...
}
Note: While using Autolayout, viewDidLoad is never the right place to use frame of a subView that is not yet laid out by the autolayout. Best place to get and use the frame is viewDidLayoutSubviews.

UIBezierPath: How to add a border around a view with rounded corners?

I am using UIBezierPath to have my imageview have round corners but I also want to add a border to the imageview. Keep in mind the top is a uiimage and the bottom is a label.
Currently using this code produces:
let rectShape = CAShapeLayer()
rectShape.bounds = myCell2.NewFeedImageView.frame
rectShape.position = myCell2.NewFeedImageView.center
rectShape.path = UIBezierPath(roundedRect: myCell2.NewFeedImageView.bounds,
byRoundingCorners: .TopRight | .TopLeft,
cornerRadii: CGSize(width: 25, height: 25)).CGPath
myCell2.NewFeedImageView.layer.mask = rectShape
I want to add a green border to that but I cant use
myCell2.NewFeedImageView.layer.borderWidth = 8
myCell2.NewFeedImageView.layer.borderColor = UIColor.greenColor().CGColor
because it cuts off the top left and top right corner of the border as seen in this image:
Is there a way too add in a border with UIBezierPath along with my current code?
You can reuse the UIBezierPath path and add a shape layer to the view. Here is an example inside a view controller.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Create a view with red background for demonstration
let v = UIView(frame: CGRectMake(0, 0, 100, 100))
v.center = view.center
v.backgroundColor = UIColor.redColor()
view.addSubview(v)
// Add rounded corners
let maskLayer = CAShapeLayer()
maskLayer.frame = v.bounds
maskLayer.path = UIBezierPath(roundedRect: v.bounds, byRoundingCorners: .TopRight | .TopLeft, cornerRadii: CGSize(width: 25, height: 25)).CGPath
v.layer.mask = maskLayer
// Add border
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path // Reuse the Bezier path
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = UIColor.greenColor().CGColor
borderLayer.lineWidth = 5
borderLayer.frame = v.bounds
v.layer.addSublayer(borderLayer)
}
}
The end result looks like this.
Note that this only works as expected when the view's size is fixed. When the view can resize, you will need to create a custom view class and resize the layers in layoutSubviews.
As it says above:
It is not easy to do this perfectly.
Here's a drop-in solution.
This
correctly addresses the issue that you are drawing HALF OF THE BORDER LINE
is totally usable in autolayout
completely re-works itself when the size of the view changes or animates
is totally IBDesignable - you can see it in realtime in your storyboard
for 2019 ...
#IBDesignable
class RoundedCornersAndTrueBorder: UIView {
#IBInspectable var cornerRadius: CGFloat = 10 {
didSet { setup() }
}
#IBInspectable var borderColor: UIColor = UIColor.black {
didSet { setup() }
}
#IBInspectable var trueBorderWidth: CGFloat = 2.0 {
didSet { setup() }
}
override func layoutSubviews() {
setup()
}
var border:CAShapeLayer? = nil
func setup() {
// make a path with round corners
let path = UIBezierPath(
roundedRect: self.bounds, cornerRadius:cornerRadius)
// note that it is >exactly< the size of the whole view
// mask the whole view to that shape
// note that you will ALSO be masking the border we'll draw below
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
// add another layer, which will be the border as such
if (border == nil) {
border = CAShapeLayer()
self.layer.addSublayer(border!)
}
// IN SOME APPROACHES YOU would INSET THE FRAME
// of the border-drawing layer by the width of the border
// border.frame = bounds.insetBy(dx: borderWidth, dy: borderWidth)
// so that when you draw the line, ALL of the WIDTH of the line
// DOES fall within the actual mask.
// here, we will draw the border-line LITERALLY ON THE EDGE
// of the path. that means >HALF< THE LINE will be INSIDE
// the path and HALF THE LINE WILL BE OUTSIDE the path
border!.frame = bounds
let pathUsingCorrectInsetIfAny =
UIBezierPath(roundedRect: border!.bounds, cornerRadius:cornerRadius)
border!.path = pathUsingCorrectInsetIfAny.cgPath
border!.fillColor = UIColor.clear.cgColor
// the following is not what you want:
// it results in "half-missing corners"
// (note however, sometimes you do use this approach):
//border.borderColor = borderColor.cgColor
//border.borderWidth = borderWidth
// this approach will indeed be "inside" the path:
border!.strokeColor = borderColor.cgColor
border!.lineWidth = trueBorderWidth * 2.0
// HALF THE LINE will be INSIDE the path and HALF THE LINE
// WILL BE OUTSIDE the path. so MAKE IT >>TWICE AS THICK<<
// as requested by the consumer class.
}
}
So that's it.
Beginner help for the question in the comments ...
Make a "new Swift file" called "Fattie.swift". (Note, funnily enough it actually makes no difference what you call it. If you are at the stage of "don't know how to make a new file" seek basic Xcode tutorials.)
Put all of the above code in the file
You've just added a class "RoundedCornersAndTrueBorder" to your project.
On your story board. Add an ordinary UIView to your scene. In fact, make it actually any size/shape whatsoever, anything you prefer.
Look at the Identity Inspector. (If you do not know what that is, seek basic tutorials.) Simply change the class to "RoundedCornersAndTrueBorder". (Once you start typing "Roun...", it will guess which class you mean.
You're done - run the project.
Note that you have to, of course, add complete and correct constraints to the UIView, just as with absolutely anything you do in Xcode. Enjoy!
Similar solutions:
https://stackoverflow.com/a/57465440/294884 - image + rounded + shadows
https://stackoverflow.com/a/41553784/294884 - two-corner problem
https://stackoverflow.com/a/59092828/294884 - "shadows + hole" or "glowbox" problem
https://stackoverflow.com/a/57400842/294884 - the "border AND gap" problem
https://stackoverflow.com/a/57514286/294884 - basic "adding" beziers
And please also see the alternate answer below! :)
Absolutely perfect 2019 solution
Without further ado, here's exactly how you do this.
Don't actually use the "basic" layer that comes with the view
Make a new layer only for the image. You can now mask this (circularly) without affecting the next layer
Make a new layer for the border as such. It will safely not be masked by the picture layer.
The key facts are
With a CALayer, you can indeed apply a .mask and it only affects that layer
When drawing a circle (or indeed any border), have to attend very carefully to the fact that you only get "half the width" - in short never crop using the same path you draw with.
Notice the original cat image is exactly as wide as the horizontal yellow arrow. You have to be careful to paint the image so that the whole image appears in the roundel, which is smaller than the overall custom control.
So, setup in the usual way
import UIKit
#IBDesignable class GreenCirclePerson: UIView {
#IBInspectable var borderColor: UIColor = UIColor.black { didSet { setup() } }
#IBInspectable var trueBorderThickness: CGFloat = 2.0 { didSet { setup() } }
#IBInspectable var trueGapThickness: CGFloat = 2.0 { didSet { setup() } }
#IBInspectable var picture: UIImage? = nil { didSet { setup() } }
override func layoutSubviews() { setup() }
var imageLayer: CALayer? = nil
var border: CAShapeLayer? = nil
func setup() {
if (imageLayer == nil) {
imageLayer = CALayer()
self.layer.addSublayer(imageLayer!)
}
if (border == nil) {
border = CAShapeLayer()
self.layer.addSublayer(border!)
}
Now carefully make the layer for the circularly-cropped image:
// the ultimate size of our custom control:
let box = self.bounds.aspectFit()
let totalInsetOnAnyOneSide = trueBorderThickness + trueGapThickness
let boxInWhichImageSits = box.inset(by:
UIEdgeInsets(top: totalInsetOnAnyOneSide, left: totalInsetOnAnyOneSide,
bottom: totalInsetOnAnyOneSide, right: totalInsetOnAnyOneSide))
// just a note. that version of inset#by is much clearer than the
// confusing dx/dy variant, so best to use that one
imageLayer!.frame = boxInWhichImageSits
imageLayer!.contents = picture?.cgImage
imageLayer?.contentsGravity = .resizeAspectFill
let halfImageSize = boxInWhichImageSits.width / 2.0
let maskPath = UIBezierPath(roundedRect: imageLayer!.bounds,
cornerRadius:halfImageSize)
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
imageLayer!.mask = maskLayer
Next as a completely separate layer, draw the border as you wish:
// now create the border
border!.frame = bounds
// To draw the border, you must inset it by half the width of the border,
// otherwise you'll be drawing only half the border. (Indeed, as an additional
// subtle problem you are clipping rather than rendering the outside edge.)
let halfWidth = trueBorderThickness / 2.0
let borderCenterlineBox = box.inset(by:
UIEdgeInsets(top: halfWidth, left: halfWidth,
bottom: halfWidth, right: halfWidth))
let halfBorderBoxSize = borderCenterlineBox.width / 2.0
let borderPath = UIBezierPath(roundedRect: borderCenterlineBox,
cornerRadius:halfBorderBoxSize)
border!.path = borderPath.cgPath
border!.fillColor = UIColor.clear.cgColor
border!.strokeColor = borderColor.cgColor
border!.lineWidth = trueBorderThickness
}
}
Everything works perfectly as in iOS standard controls:
Everything which is invisible is invisible; you can see-through the overall custom control to any material behind, there are no "half thickness" problems or missing image material, you can set the custom control background color in the usual way, etc etc. The inspector controls all work properly. (Phew!)
Similar solutions:
https://stackoverflow.com/a/57465440/294884 - image + rounded + shadows
https://stackoverflow.com/a/41553784/294884 - two-corner problem
https://stackoverflow.com/a/59092828/294884 - "shadows + hole" or "glowbox" problem
https://stackoverflow.com/a/57400842/294884 - the "border AND gap" problem
https://stackoverflow.com/a/57514286/294884 - basic "adding" beziers
**use this extension for round borders and corner**
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
func roundCornersWithBorder(corners: UIRectCorner, radius: CGFloat) {
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: radius, height: radius)).cgPath
layer.mask = maskLayer
// Add border
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path // Reuse the Bezier path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor(red:3/255, green:33/255, blue:70/255, alpha: 0.15).cgColor
borderLayer.lineWidth = 2
borderLayer.frame = bounds
layer.addSublayer(borderLayer)
}
}
Use like this
myView.roundCornersWithBorder(corners: [.topLeft, .topRight], radius: 8.0)
myView.roundCorners(corners: [.topLeft, .topRight], radius: 8.0)
There sure is! Every view has a layer property (which you know from giving your layer rounded corners). Another two properties on layer are borderColor and borderWidth. Just by setting those you can add a border to your view! (The border will follow the rounded corners.) Be sure to use UIColor.CGColor for borderColor as a plain UIColor won't match the type.

Swift - How to create a view with a shape cropped in it

I'm trying to achieve the result shown in the image using swift 1.2 and xcode 6.
Basically I want to create a view with a shape cut in it to be able to see the the view below to make a tutorial for my app.
I know how to create a circular shape but i don't know how to cut it out in a view.
I need a complete example on how to do it please.
Thanks in advance
Even though there is an answer, i'd like to share my way:
// Let's say that you have an outlet to the image view called imageView
// Create the white view
let whiteView = UIView(frame: imageView.bounds)
let maskLayer = CAShapeLayer() //create the mask layer
// Set the radius to 1/3 of the screen width
let radius : CGFloat = imageView.bounds.width/3
// Create a path with the rectangle in it.
let path = UIBezierPath(rect: imageView.bounds)
// Put a circle path in the middle
path.addArcWithCenter(imageView.center, radius: radius, startAngle: 0.0, endAngle: CGFloat(2*M_PI), clockwise: true)
// Give the mask layer the path you just draw
maskLayer.path = path.CGPath
// Fill rule set to exclude intersected paths
maskLayer.fillRule = kCAFillRuleEvenOdd
// By now the mask is a rectangle with a circle cut out of it. Set the mask to the view and clip.
whiteView.layer.mask = maskLayer
whiteView.clipsToBounds = true
whiteView.alpha = 0.8
whiteView.backgroundColor = UIColor.whiteColor()
//If you are in a VC add to the VC's view (over the image)
view.addSubview(whiteView)
// Annnnnd you're done.
//assume you create a UIImageView and content image before execute this code
let sampleMask = UIView()
sampleMask.frame = self.view.frame
sampleMask.backgroundColor = UIColor.black.withAlphaComponent(0.6)
//assume you work in UIViewcontroller
self.view.addSubview(sampleMask)
let maskLayer = CALayer()
maskLayer.frame = sampleMask.bounds
let circleLayer = CAShapeLayer()
//assume the circle's radius is 150
circleLayer.frame = CGRect(x:0 , y:0,width: sampleMask.frame.size.width,height: sampleMask.frame.size.height)
let finalPath = UIBezierPath(roundedRect: CGRect(x:0 , y:0,width: sampleMask.frame.size.width,height: sampleMask.frame.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(ovalIn: CGRect(x:sampleMask.center.x - 150, y:sampleMask.center.y - 150, width: 300, height: 300))
finalPath.append(circlePath.reversing())
circleLayer.path = finalPath.cgPath
circleLayer.borderColor = UIColor.white.withAlphaComponent(1).cgColor
circleLayer.borderWidth = 1
maskLayer.addSublayer(circleLayer)
sampleMask.layer.mask = maskLayer
Here is sample code for how you can make a circle Mask for a UIView:
let sampleView = UIView(frame: UIScreen.mainScreen().bounds)
let maskLayer = CALayer()
maskLayer.frame = sampleView.bounds
let circleLayer = CAShapeLayer()
//assume the circle's radius is 100
circleLayer.frame = CGRectMake(sampleView.center.x - 100, sampleView.center.y - 100, 200, 200)
let circlePath = UIBezierPath(ovalInRect: CGRectMake(0, 0, 200, 200))
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.blackColor().CGColor
maskLayer.addSublayer(circleLayer)
sampleView.layer.mask = maskLayer
Here is what I made in the playground:
The easiest way to do this would be to create a png image with partly transparent white around the outside and a clear circle in the middle. Then stack 2 image views on top of each other, with the masking image on top, and set its "opaque" flag to false.
You could also do this by creating a CAShapeLayer and set it up to use a translucent white color, then install a shape that is the square with the hole cut out of it shape. You'd install that shape layer on top of your image view's layer.
The most general-purpose way to do that would be to create a custom subclass of UIImageView and have the init method of your subclass create and install the shape layer. I just created a gist yesterday that illustrated creating a custom subclass of UIImageView. Here is the link: ImageViewWithGradient gist
That gist creates a gradient layer. It would be a simple matter to adapt it to create a shape layer instead, and if you modified the layoutSubviews method you could make it adapt the view and path if the image view gets resized.
EDIT:
Ok, I took the extra step of creating a playground that creates a cropping image view. You can find that at ImageViewWithMask on github
The resulting image for my playground looks like this:
class MakeTransparentHoleOnOverlayView: UIView {
#IBOutlet weak var transparentHoleView: UIView!
// MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
if self.transparentHoleView != nil {
// Ensures to use the current background color to set the filling color
self.backgroundColor?.setFill()
UIRectFill(rect)
let layer = CAShapeLayer()
let path = CGMutablePath()
// Make hole in view's overlay
// NOTE: Here, instead of using the transparentHoleView UIView we could use a specific CFRect location instead...
path.addRect(transparentHoleView.frame)
path.addRect(bounds)
layer.path = path
layer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = layer
}
}
override func layoutSubviews () {
super.layoutSubviews()
}
// MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}

UIImageView's shadow gets bigger than intended when shadowPath is set

CGRect rect = biggerImageView.bounds;
if([biggerImageView.layer respondsToSelector:#selector(setShadowColor:)])
{
float shadowOffset = rect.size.width * 0.02;
biggerImageView.layer.shadowColor = [UIColor colorWithWhite: 0.25 alpha: 0.55].CGColor;
biggerImageView.layer.shadowOffset = CGSizeMake(shadowOffset, shadowOffset);
biggerImageView.layer.shadowOpacity = 0.8;
// biggerImageView.layer.shadowPath = [UIBezierPath bezierPathWithRect: rect].CGPath;
}
The commented out line causes the shadow becomes bigger than intended.
(vertically longer shadows on top and bottom)
I looked up CALayer reference but got no clue there.
That is probably because the UIView bounds is not the real one yet. (Like in awakeFromNib or viewDidLoad)
Wait for the view layoutSubviews or viewController viewWillLayoutSubviews to be called, and update the path of your shadow there.
I created this swift extension to add the same shadow (with cornerRadius support) everywhere I need to :
import UIKit
extension UIView {
/// Call this from `layoutSubviews` or `viewWillLayoutSubviews`.
/// The shadow will be the same color as this view background.
func layoutShadow() {
// Update frame
if let shadowView = superview?.viewWithTag(978654123) {
shadowView.frame = frame
shadowView.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
return
}
// Create the shadow the first time
let shadowView = UIView(frame: frame)
shadowView.tag = 978654123
shadowView.translatesAutoresizingMaskIntoConstraints = false
superview?.insertSubview(shadowView, belowSubview: self)
shadowView.layer.shadowColor = (backgroundColor ?? UIColor.black).cgColor
shadowView.layer.shadowOpacity = 0.8
shadowView.layer.shadowOffset = CGSize(width: -2.0, height: 4.0)
shadowView.layer.shadowRadius = 4.0
shadowView.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
}
}