Change UIButton rect without drawRect - ios

I have a UIButton on xib which is a rectangle, I want to change it's frame to have an arrow like corner, but keep it on the xib (from interface builder), how can I do that?
I know that I can do that with UIBezierPath in drawRect, but that is only to create a custom shape and add it from code.
Is there another way?
This is my code with custom button class:
class ButtonContinue: UIButton {
var path: UIBezierPath!
override func awakeFromNib() {
backgroundColor = UIColor.green
addTarget(self, action: #selector(touchDown), for: .touchDown)
}
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path.move(to: CGPoint(x: 32, y: 28 + (rect.size.height / 2)))
path.addLine(to: CGPoint(x: 70, y: 28))
path.addLine(to: CGPoint(x: rect.size.width, y: 28))
path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height + 28))
path.addLine(to: CGPoint(x: 70, y: rect.size.height + 28))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
}
func touchDown(button: ButtonContinue, event: UIEvent) {
if let touch = event.touches(for: button)?.first {
let location = touch.location(in: button)
if path.contains(location) == false {
button.cancelTracking(with: nil)
}
}
}
}

Just put the image you provided in assets of the project. And set buttons image to the image in assets.
self.imageView.image = UIImage(named: "imageName")
And if you would like to change color of the lines of the image you can set the image rendering to alwaysTemplate
self.imageView?.image = UIImage(named: "imageName")?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)
self.imageView?.tintColor = .red

Related

UIView diagonal border on one corner

I have a view that needs to be displayed with a slanted corner on one side. I've already done it when the view has a background color like this:
But I also need it to be displayed with a clear background. After setting its background to clear and adding a border to it this is the output:
Here is the code for the custom view that I'm using to create the diagonal corner:
class PointedView: UIImageView {
#IBInspectable var borderColor: UIColor = UIColor.clear {
didSet {
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable
/// Percentage of the slant based on the width
var slopeFactor: CGFloat = 15 {
didSet {
updatePath()
}
}
private let shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 0
// with masks, the color of the shape layer doesn’t matter;
// it only uses the alpha channel; the color of the view is
// dictate by its background color
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
private func updatePath() {
let path = UIBezierPath()
// Start from x = 0 but the mid point of y of the view
path.move(to: CGPoint(x: 0, y: bounds.midY*2))
// Create the top slanting line
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY))
// Straight line from end of slant to the end of the view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
// Straight line to come down to the bottom, perpendicular to view
path.addLine(to: CGPoint(x: bounds.maxX, y: ((bounds.maxY*3)/4) + 20))
// Go back to the slant end position but from the bottom
path.addLine(to: CGPoint(x: (bounds.maxX*3)/4, y: bounds.maxY))
// Close path back to where you started
path.close()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}
}
Is there any possible solution to this?
class PointedView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createBezierPath().cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.position = CGPoint(x: 10, y: 10)
self.layer.addSublayer(shapeLayer)
}
func createBezierPath() -> UIBezierPath {
let path = UIBezierPath()
// Start from x = 0 but the mid point of y of the view
path.move(to: CGPoint(x: 0, y: bounds.midY*2))
// Create the top slanting line
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY))
// Straight line from end of slant to the end of the view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
// Straight line to come down to the bottom, perpendicular to view
path.addLine(to: CGPoint(x: bounds.maxX, y: ((bounds.maxY*3)/4) + 20))
// Go back to the slant end position but from the bottom
path.addLine(to: CGPoint(x: (bounds.maxX*3)/4, y: bounds.maxY))
// Close path back to where you started
path.close() // draws the final line to close the path
return path
}
}
Managed to solve it by drawing another CAShapeLayer() following the same path as the original shape.
let borderLayer = CAShapeLayer()
borderLayer.path = path.cgPath
borderLayer.lineWidth = 2
borderLayer.strokeColor = borderColor.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.frame = bounds
layer.addSublayer(borderLayer)
if you are using StoryBoard with #IBInspectable you can try like thisenter image description here
Please use this method
func roundCorners(corners: UIRectCorner = .allCorners, radius: CGFloat = 0.0, borderColor: UIColor = .clear, borderWidth: CGFloat = 0.0, clipToBonds: Bool = true) {
clipsToBounds = clipToBonds
layer.cornerRadius = radius
layer.borderWidth = borderWidth
layer.borderColor = borderColor.cgColor
if corners.contains(.allCorners){
layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
return
}
var maskedCorners = CACornerMask()
if corners.contains(.topLeft) { maskedCorners.insert(.layerMinXMinYCorner) }
if corners.contains(.topRight) { maskedCorners.insert(.layerMaxXMinYCorner) }
if corners.contains(.bottomLeft) { maskedCorners.insert(.layerMinXMaxYCorner) }
if corners.contains(.bottomRight) { maskedCorners.insert(.layerMaxXMaxYCorner) }
layer.maskedCorners = maskedCorners
}
and by using your UIVIEW
View.roundCorners(corners: [.topLeft, .bottomLeft], radius: 20, borderColor: .clear, borderWidth: 0, clipToBonds: true)

Color of the triangle view drawn by UIBezierPath does not change

I would like to draw a triangle view and change the filled color programmatically.
Following is my code.
import UIKit
class ViewController: UIViewController {
let triangleView = TriangleView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
triangleView.frame = CGRect(x: 0,
y: 100,
width: 50,
height: 50)
self.view.addSubview(triangleView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
triangleView.drawColor(color: .yellow)
}
}
class TriangleView: UIView {
let path = UIBezierPath()
override func draw(_ rect: CGRect) {
print("TriangleView draw")
path.move(to: CGPoint(x: 0, y: self.bounds.height / 2))
path.addLine(to: CGPoint(x: self.bounds.maxX, y: 0))
path.addLine(to: CGPoint(x: self.bounds.maxX, y: self.bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: self.bounds.height / 2))
path.close()
self.drawColor(color: .green)
self.backgroundColor = .clear
}
func drawColor(color: UIColor) {
print("TriangleView drawColor")
color.setFill()
path.lineWidth = 0
path.fill()
path.stroke()
}
}
In this code, TriangleView draws a triangle filled with green color.
After that, ViewController changes filled color by yellow.
Following the result.
There are two problems.
Background color is black though expectation is clear.
Triangle color is not changed to yellow.
Could anyone give me advice ?
Use UIGraphicsGetCurrentContext
class TriangleView: UIView {
private var triangleColor: UIColor = .green {
didSet {
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
print("TriangleView draw")
self.backgroundColor = .white // Set any background color
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
defer { context.restoreGState() }
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: self.bounds.height / 2))
path.addLine(to: CGPoint(x: self.bounds.maxX, y: 0))
path.addLine(to: CGPoint(x: self.bounds.maxX, y: self.bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: self.bounds.height / 2))
path.close()
context.addPath(path.cgPath)
context.setFillColor(triangleColor.cgColor)
context.closePath()
context.closePath()
context.fillPath()
context.restoreGState()
}
func drawColor(color: UIColor) {
triangleColor = color
}
}
Or you can use CAShapeLayer
class TriangleView: UIView {
private let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.initialConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initialConfig()
}
override func draw(_ rect: CGRect) {
print("TriangleView draw")
self.shapeLayer.frame = self.bounds
drawShape()
}
private func initialConfig() {
self.backgroundColor = .white
self.shapeLayer.fillColor = UIColor.green.cgColor
self.layer.addSublayer(shapeLayer)
}
private func drawShape() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: self.bounds.height / 2))
path.addLine(to: CGPoint(x: self.bounds.maxX, y: 0))
path.addLine(to: CGPoint(x: self.bounds.maxX, y: self.bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: self.bounds.height / 2))
path.close()
shapeLayer.path = path.cgPath
}
func drawColor(color: UIColor) {
self.shapeLayer.fillColor = color.cgColor
}
}

UIBezierPath partially stroke

I have this code to draw a rectangle which is rounded rect only on one side.
override func draw(_ rect: CGRect) {
// Drawing code
guard let context = UIGraphicsGetCurrentContext() else { return }
let lineWidth = CGFloat(4)
let pathRect = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
let path = UIBezierPath(roundedRect: pathRect.inset(by: UIEdgeInsets(top: lineWidth, left: lineWidth, bottom: lineWidth, right: 0)), byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: 7, height: 7))
context.setFillColor(UIColor.black.cgColor)
path.fill()
context.setLineWidth(lineWidth)
}
I want to stroke it with red color on all but the right edge (no stroke on the right edge). How do I do it?
You’ll have to create your own path.
A couple of observations:
Don’t use the rect parameter. The rect is what is being asked to being drawn at this point in time, which may not be the entire view. Use bounds when figuring out what the overall path should be.
I might inset the path so that the stroke stays within the bounds of the view.
You can make this #IBDesignable if you want to also be able to see it rendered in IB.
You don’t really need UIGraphicsGetCurrentContext(). The UIKit methods fill(), stroke(), setFill(), and setStroke() methods automatically use the current context.
Thus:
#IBDesignable
class OpenRightView: UIView {
#IBInspectable var lineWidth: CGFloat = 4 { didSet { setNeedsDisplay() } }
#IBInspectable var radius: CGFloat = 7 { didSet { setNeedsDisplay() } }
#IBInspectable var fillColor: UIColor = .black { didSet { setNeedsDisplay() } }
#IBInspectable var strokeColor: UIColor = .red { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
let path = UIBezierPath()
path.lineWidth = lineWidth
path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
fillColor.setFill()
path.fill()
strokeColor.setStroke()
path.stroke()
}
}
That yields:
Theoretically, it might be more efficient to use CAShapeLayer and let Apple take care of the draw(_:) for us. E.g., they may have optimized the rendering to handle partial view updates, etc.
That might look like the following:
#IBDesignable
class OpenRightView: UIView {
#IBInspectable var lineWidth: CGFloat = 4 { didSet { updatePath() } }
#IBInspectable var radius: CGFloat = 7 { didSet { updatePath() } }
#IBInspectable var fillColor: UIColor = .black { didSet { shapeLayer.fillColor = fillColor.cgColor } }
#IBInspectable var strokeColor: UIColor = .red { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = lineWidth
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
private extension OpenRightView {
func configure() {
layer.addSublayer(shapeLayer)
}
func updatePath() {
let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
let path = UIBezierPath()
path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = lineWidth
}
}

Add shadow to UIView whose shape has been changed by UIBezierPath

I've created an extension for UIView that allows me to make a concave shape.
extension UIView {
func createConcave(depth: CGFloat) {
let width = self.bounds.width
let height = self.bounds.height
let path = UIBezierPath()
let p0 = CGPoint(x: 0, y: 0)
let p2 = CGPoint(x: width, y: 0)
let p1 = CGPoint(x: width / 2, y: depth)
path.move(to: p0)
path.addQuadCurve(to: p2, controlPoint: p1)
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.addLine(to: p0)
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
self.layer.masksToBounds = false
}
}
What would be a good solution to add a shadow to the view that matches the shape? Would I have to specify the shadow path to be the same path as the concave shape?
You are masking the layer to the path. Thus anything, including the shadow, will be clipped by that mask.
Instead of masking, add sublayer.
E.g.
#IBDesignable
class ConcaveView: UIView {
#IBInspectable var depth: CGFloat = 10 { didSet { updatePath() } }
#IBInspectable var fillColor: UIColor = .red { didSet { shapeLayer.fillColor = fillColor.cgColor } }
private lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowRadius = 5
shapeLayer.shadowOpacity = 1
shapeLayer.shadowOffset = .zero
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
layer.addSublayer(shapeLayer)
clipsToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
func updatePath() {
let path = UIBezierPath()
let point0 = CGPoint(x: bounds.minX, y: bounds.minY)
let point2 = CGPoint(x: bounds.maxX, y: bounds.minY)
let point1 = CGPoint(x: bounds.width / 2, y: bounds.minY + depth)
path.move(to: point0)
path.addQuadCurve(to: point2, controlPoint: point1)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.addLine(to: point0)
shapeLayer.path = path.cgPath
}
}
That yields:

Corner radius image Swift

I'm trying to make this corner radius image...it's not exactly the same shape of the image..any easy answer instead of trying random numbers of width and height ?
thanks alot
let rectShape = CAShapeLayer()
rectShape.bounds = self.mainImg.frame
rectShape.position = self.mainImg.center
rectShape.path = UIBezierPath(roundedRect: self.mainImg.bounds, byRoundingCorners: [.bottomLeft , .bottomRight ], cornerRadii: CGSize(width: 50, height: 4)).cgPath
You can use QuadCurve to get the design you want.
Here is a Swift #IBDesignable class that lets you specify the image and the "height" of the rounding in Storyboard / Interface Builder:
#IBDesignable
class RoundedBottomImageView: UIView {
var imageView: UIImageView!
#IBInspectable var image: UIImage? {
didSet { self.imageView.image = image }
}
#IBInspectable var roundingValue: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
doMyInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
doMyInit()
}
func doMyInit() {
imageView = UIImageView()
imageView.backgroundColor = UIColor.red
imageView.contentMode = UIViewContentMode.scaleAspectFill
addSubview(imageView)
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - roundingValue
let curveTo:CGFloat = rect.size.height + roundingValue
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
Result with 300 x 200 image view, rounding set to 40:
Edit - (3.5 years later)...
To answer #MiteshDobareeya comment, we can switch the rounded edge from Bottom to Top by transforming the bezier path:
let c = CGAffineTransform(scaleX: 1, y: -1).concatenating(CGAffineTransform(translationX: 0, y: bounds.size.height))
myBezier.apply(c)
It's been quite a while since this answer was originally posted, so a few changes:
subclass UIImageView directly - no need to make it a UIView with an embedded UIImageView
add a Bool roundTop var
if set to False (the default), we round the Bottom
if set to True, we round the Top
re-order and "name" our path points for clarity
So, the basic principle:
We create a UIBezierPath and:
move to pt1
add a line to pt2
add a line to pt3
add a quad-curve to pt4 with controlPoint
close the path
use that path for a CAShapeLayer mask
the result:
If we want to round the Top, after closing the path we can apply apply a scale transform using -1 as the y value to vertically mirror it. Because that transform mirror it at "y-zero" we also apply a translate transform to move it back down into place.
That gives us:
Here's the updated class:
#IBDesignable
class RoundedTopBottomImageView: UIImageView {
#IBInspectable var roundingValue: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
#IBInspectable var roundTop: Bool = false {
didSet {
self.setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let r = bounds
let myBezier = UIBezierPath()
let pt1: CGPoint = CGPoint(x: r.minX, y: r.minY)
let pt2: CGPoint = CGPoint(x: r.maxX, y: r.minY)
let pt3: CGPoint = CGPoint(x: r.maxX, y: r.maxY - roundingValue)
let pt4: CGPoint = CGPoint(x: r.minX, y: r.maxY - roundingValue)
let controlPoint: CGPoint = CGPoint(x: r.midX, y: r.maxY + roundingValue)
myBezier.move(to: pt1)
myBezier.addLine(to: pt2)
myBezier.addLine(to: pt3)
myBezier.addQuadCurve(to: pt4, controlPoint: controlPoint)
myBezier.close()
if roundTop {
// if we want to round the Top instead of the bottom,
// flip the path vertically
let c = CGAffineTransform(scaleX: 1, y: -1) //.concatenating(CGAffineTransform(translationX: 0, y: bounds.size.height))
myBezier.apply(c)
}
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
You can try with UIView extension. as
extension UIView {
func setBottomCurve(){
let offset = CGFloat(self.frame.size.height + self.frame.size.height/1.8)
let bounds = self.bounds
let rectBounds = CGRect(x: bounds.origin.x,
y: bounds.origin.y ,
width: bounds.size.width,
height: bounds.size.height / 2)
let rectPath = UIBezierPath(rect: rectBounds)
let ovalBounds = CGRect(x: bounds.origin.x - offset / 2,
y: bounds.origin.y ,
width: bounds.size.width + offset,
height: bounds.size.height)
let ovalPath = UIBezierPath(ovalIn: ovalBounds)
rectPath.append(ovalPath)
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = rectPath.cgPath
self.layer.mask = maskLayer
}
}
& use it in viewWillAppear like methods where you can get actual frame of UIImageView.
Usage:
override func viewWillAppear(_ animated: Bool) {
//use it in viewWillAppear like methods where you can get actual frame of UIImageView
myImageView.setBottomCurve()
}

Resources