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

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)
}
}

Related

Using Swift and CAShapeLayer() with masking, how can I avoid inverting the mask when masked regions intersect?

This question was challenging to word, but explaining the situation further should help.
Using the code below, I'm essentially masking a circle on the screen wherever I tap to reveal what's underneath the black UIView. When I tap, I record the CGPoint in an array to keep track of the tapped locations. For every subsequent tap I make, I remove the black UIView and recreate each tapped point from the array of CGPoints I'm tracking in order to create a new mask that includes all the previous points.
The result is something like this:
I'm sure you can already spot what I'm asking about... How can I avoid the mask inverting wherever the circles intersect? Thanks for your help!
Here's my code for reference:
class MiniGameShadOViewController: UIViewController {
//MARK: - DECLARATIONS
var revealRadius : CGFloat = 50
var tappedAreas : [CGPoint] = []
//Objects
#IBOutlet var shadedRegion: UIView!
//Gesture Recognizers
#IBOutlet var tapToReveal: UITapGestureRecognizer!
//MARK: - VIEW STATES
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - USER INTERACTIONS
#IBAction func regionTapped(_ sender: UITapGestureRecognizer) {
let tappedPoint = sender.location(in: view)
tappedAreas.append(tappedPoint) //Hold a list of all previously tapped points
//Clean up old overlays before adding the new one
for subview in shadedRegion.subviews {
if subview.accessibilityIdentifier != "Number" {subview.removeFromSuperview()}
}
//shadedRegion.layer.mask?.removeFromSuperlayer()
createOverlay()
}
//MARK: - FUNCTIONS
func createOverlay(){
//Create the shroud that covers the orbs on the screen
let overlayView = UIView(frame: shadedRegion.bounds)
overlayView.alpha = 1
overlayView.backgroundColor = UIColor.black
overlayView.isUserInteractionEnabled = false
shadedRegion.addSubview(overlayView)
let path = CGMutablePath()
//Create the box that represents the inverse/negative area relative to the circles
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
//For each point tapped so far, create a circle there
for point in tappedAreas {
path.addArc(center: point, radius: revealRadius, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
path.closeSubpath() //This is required to prevent all circles from being joined together with lines
}
//Fill each of my circles
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = .evenOdd
//Cut out the circles inside that box
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
}
}
You asked:
how can I avoid inverting the mask when masked regions intersect?
In short, do not use the .evenOdd fill rule.
You have specified a fillRule of .evenOdd. That results in intersections of paths to invert. Here is a red colored view with a mask consisting of a path with two overlapping circular arcs with the .evenOdd rule:
If you use .nonZero (which, coincidentally, is the default fill rule for shape layers), they will not invert each other:
E.g.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var maskLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
var points: [CGPoint] = [] // this isn't strictly necessary, but just in case you want an array of the points that were tapped
var path = UIBezierPath()
override func viewDidLoad() {
super.viewDidLoad()
imageView.layer.mask = maskLayer
}
#IBAction func handleTapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
points.append(point)
path.move(to: point)
path.addArc(withCenter: point, radius: 40, startAngle: 0, endAngle: .pi * 2, clockwise: true)
maskLayer.path = path.cgPath
}
}
Resulting in:

shadow on bezierpath view adds weird strokes to corners

I wanted to recreate the AppStore's "today" cards with rounded corners and a light drop shadow.
I created a path, a maskLayer and a separate shadowLayer, which – according to several sources – is the way of doing it.
The problem, however, is that my lovely rounded rectangle with a shadow has got some gray strokes at it corners. How can I solve this? I tried different shadow opacities and different radii. It didn't solve my problem.
Here you can see my screenshots and my code below.
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// create sample view and add to view hierarchy
let bigTeaser = UIView(frame: CGRect(x: 16, y: 200, width: 343, height: 267))
bigTeaser.backgroundColor = UIColor.white
view.addSubview(bigTeaser)
// create the path for the rounded corners and the shadow
let roundPath = UIBezierPath(roundedRect: bigTeaser.bounds, cornerRadius: 20)
// create maskLayer
let maskLayer = CAShapeLayer()
maskLayer.frame = bigTeaser.bounds
maskLayer.path = roundPath.cgPath
bigTeaser.layer.mask = maskLayer
// create shadowLayer
let shadowLayer = CAShapeLayer()
shadowLayer.path = roundPath.cgPath
shadowLayer.frame = bigTeaser.frame
shadowLayer.shadowOpacity = 0.3
shadowLayer.shadowRadius = 24
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0, height: 2)
// insert layers
bigTeaser.superview!.layer.insertSublayer(shadowLayer, below: bigTeaser.layer)
}
If you replace:
shadowLayer.path = roundPath.cgPath
to
shadowLayer.shadowPath = roundPath.cgPath
The ugly borders will magically disappear.

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.

Using an UIView as a Mask in another UIView on Swift

I am developing an App on xCode with Swift. My screen has an Image of an animal (outlet "backImage"), over this image I have a view (outlet "coverView"), color black, which covers all the screen. So so far you can't see the animal image, because the "coverView" is in front of it. Then I have another view (outlet "maskView") which is smaller and it's over the big view "coverView". What I want is to use this "maskView" as mask and therefor see the "backImage" through it, like a window.
Is there anyone out there able to figure this out?
Here is my screen, I want to see the woman character behind the big gray view through the smaller white view:
You can set the alpha property from your mask view and add in front of the other view, for instance:
let maskView = UIView()
maskView.backgroundColor = UIColor(white: 0, alpha: 0.5) //you can modify this to whatever you need
maskView.frame = CGRect(x: 0, y: 0, width: imageView.frame.width, height: imageView.frame.height)
yourView.addSubview(maskView)
EDIT: Now that you edited your question with an image, now I see what you need, so here is how you can accomplish that.
func setMask(with hole: CGRect, in view: UIView){
// Create a mutable path and add a rectangle that will be h
let mutablePath = CGMutablePath()
mutablePath.addRect(view.bounds)
mutablePath.addRect(hole)
// Create a shape layer and cut out the intersection
let mask = CAShapeLayer()
mask.path = mutablePath
mask.fillRule = kCAFillRuleEvenOdd
// Add the mask to the view
view.layer.mask = mask
}
With this function, all you need is to have a view and create a shape that it's going to be a hole in that view, for instance:
// Create the view (you can also use a view created in the storyboard)
let newView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
newView.backgroundColor = UIColor(white: 0, alpha: 1)
// You can play with these values and find one that fills your need
let rectangularHole = CGRect(x: view.bounds.width*0.3, y: view.bounds.height*0.3, width: view.bounds.width*0.5, height: view.bounds.height*0.5)
// Set the mask in the created view
setMask(with: rectangularHole, in: newView)
Thank you, #Alexandre Lara! You did it!
Here goes the solution:
#IBOutlet weak var windowView: UIView!
#IBOutlet weak var bigCoverView: UIView!
func setMask(with hole: CGRect, in view: UIView){
// Create a mutable path and add a rectangle that will be h
let mutablePath = CGMutablePath()
mutablePath.addRect(view.bounds)
mutablePath.addRect(hole)
// Create a shape layer and cut out the intersection
let mask = CAShapeLayer()
mask.path = mutablePath
mask.fillRule = kCAFillRuleEvenOdd
// Add the mask to the view
view.layer.mask = mask
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let rectangularHole = windowView.frame.integral
// Set the mask in the created view
setMask(with: rectangularHole, in: bigCoverView!)
}

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.