Here is a swift port of UIView subclass that I use often in my apps. It allows me to easily set gradients in place of the backgroundColor property, and also makes it very easy to create views that have one, two, or three rounded corners.
I'm not super familiar with the mechanics behind core graphics and drawRect
, so maybe some optimizations could be made? I also like to animate views a lot, but changes to the corners or gradient won't animate with UIView.animate
. How can I implement that behavior?
//
// JFStylishView.swift
// Soapbox
//
// Created by Joseph Falcone on 6/2/16.
// Copyright © 2016 Joseph Falcone. All rights reserved.
//
import UIKit
public enum GradientType
{
case linear
case radial
}
//private enum BackgroundFillType
public enum BackgroundFillType
{
case solid
case gradient
}
@IBDesignable
open class JFStylishView : UIView
{
// TODO: should all the properties be private?
// Rounded Corners
@IBInspectable var cornerRadTL : CGFloat = 0.0
@IBInspectable var cornerRadTR : CGFloat = 0.0
@IBInspectable var cornerRadBR : CGFloat = 0.0
@IBInspectable var cornerRadBL : CGFloat = 0.0
// Border
@IBInspectable var borderWidth : CGFloat = 0.0
@IBInspectable var borderColor = UIColor.clear
// Colors
private var trueBackgroundColor = UIColor.clear // The backgroundColor property has to be clear so that the layer doesn't draw behind the clipping area, so we use this to track what the user wants
private var bgColors : [CGFloat] = [] // array of colors used in drawrect
// Gradient points
@IBInspectable private var gradientStart = CGPoint(x: 0.5, y: 0.0)
@IBInspectable private var gradientEnd = CGPoint(x: 0.5, y: 1.0)
@IBInspectable private var gradientColorStops : [CGFloat] = []
// Gradient type
private var gradientType : GradientType = .linear
// Background Mode
private var backgroundFillType : BackgroundFillType = .solid
// var shadowLayer: CAShapeLayer! // Not ready for this yet
// MARK: Initialization
public override init(frame: CGRect)
{
super.init(frame:frame)
initStylishStuff()
}
required public init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
initStylishStuff()
}
open override func awakeFromNib()
{
super.awakeFromNib()
initStylishStuff()
}
open func initStylishStuff()
{
}
// MARK: Color
private func getFillType() -> BackgroundFillType
{
// Rather than keeping a variable for this that gets set everywhere, we'll just use this getter to figure out what type we are using.
// Of course, if I get sloppy and don't make the unused elements empty when setting another fill parameter, this could produce a bug.
// RULES
// If using a gradient, trueBackgroundColor will be clear
// If using solid, bgColors will be empty
// If patterns are ever added, the above will be empty
if(bgColors.count == 0)
{return .solid}
if(trueBackgroundColor == UIColor.clear)
{return .gradient}
// Default
return .solid
}
override open var backgroundColor: UIColor?
{
get
{
return trueBackgroundColor
}
set
{
trueBackgroundColor = newValue!
super.backgroundColor = UIColor.clear
bgColors = []
backgroundFillType = .solid
}
}
// Default is linear, top to bottom
// startPoint, endPoint should be coordinates of 0.0-1.0
public func setBackgroundGradient(_ colors:[UIColor], stops:[CGFloat]? = nil, startPoint:CGPoint?=nil, endPoint:CGPoint?=nil, type:GradientType = .linear)
{
assert(colors.count > 1, "At least two colors must be specified.")
// We won't be using the backgroundColor property when drawing a gradient
//trueBackgroundColor = UIColor.clearColor()
backgroundColor = UIColor.clear
// Calculate the stops if they were not specified
var stops = stops // arguments are immutable, but we can declare a variable with the same name
if(stops == nil)
{
stops = AppocalypseUI.makeLinearColorStops(colors.count)
}
// Provide default start and end points if necessary
gradientType = type
switch type
{
case .linear: // top to bottom
gradientStart = startPoint == nil ? CGPoint.zero : startPoint!
gradientEnd = endPoint == nil ? CGPoint(x: 0, y: 1.0) : endPoint!
case .radial: // center to top
gradientStart = startPoint == nil ? CGPoint(x: 0.5, y: 0.5) : startPoint!
gradientEnd = endPoint == nil ? CGPoint(x: 0.5, y: 0) : endPoint!
}
assert(colors.count == stops?.count, "The number of colors and stops must be equal.")
//bgColorArr = colors
bgColors = AppocalypseUI.getFloatArrayFromUIColors(colors)
gradientColorStops = stops!
}
// MARK: Drawing
override open func draw(_ rect: CGRect)
{
// Get the current context
let context = UIGraphicsGetCurrentContext()
// Make the background gradient
let baseSpace = CGColorSpaceCreateDeviceRGB();
let gradient = CGGradient(colorSpace: baseSpace, colorComponents: bgColors, locations: gradientColorStops, count: gradientColorStops.count)
//let gradient = CGGradient(colorComponentsSpace: baseSpace, components: bgColors, locations: gradientColorStops, count: gradientColorStops.count);
// Set the border color and stroke
context?.setLineWidth(borderWidth);
context?.setStrokeColor(borderColor.cgColor);
// Fill in the background, inset by the border
// We do this weird BG Rect calc because a little bit of BG bleeds behind the border when corners are round
// As a hack, we just inset the bg a little so that it draws under the border
let bgRect = borderWidth <= 0 ? bounds : CGRect(x: bounds.origin.x+borderWidth/2 , y: bounds.origin.y+borderWidth/2 , width: bounds.size.width-borderWidth, height: bounds.size.height-borderWidth)
let borderRect = CGRect(x: bounds.origin.x+borderWidth/2, y: bounds.origin.y+borderWidth/2, width: bounds.size.width-borderWidth , height: bounds.size.height-borderWidth)
let bgPath = AppocalypseUI.newPathForRoundedRect(bgRect, radiusTL: cornerRadTL, radiusTR: cornerRadTR, radiusBL: cornerRadBL, radiusBR: cornerRadBR)
let borderPath = AppocalypseUI.newPathForRoundedRect(borderRect, radiusTL: cornerRadTL, radiusTR: cornerRadTR, radiusBL: cornerRadBL, radiusBR: cornerRadBR)
context?.strokePath()
// Background
context?.saveGState(); // Saves the state from before we clipped to the path
context?.addPath(bgPath);
context?.clip(); // Makes the background fill only the path
switch getFillType()
{
case .gradient:
let gradientStartInPoints = CGPoint(x: gradientStart.x*bounds.size.width, y: gradientStart.y*bounds.size.height);
let gradientEndInPoints = CGPoint(x: gradientEnd.x*bounds.size.width, y: gradientEnd.y*bounds.size.height);
switch(gradientType)
{
case .linear:
context?.drawLinearGradient(gradient!, start: gradientStartInPoints, end: gradientEndInPoints, options: []); // Draw a vertical gradient
case .radial:
// A radial gradient might not fill the layer...first, fill it with the end color
UIColor(red: bgColors[bgColors.count-4], green: bgColors[bgColors.count-3], blue: bgColors[bgColors.count-2], alpha: bgColors[bgColors.count-1]).setFill()
context?.addPath(bgPath); // Not sure why I need this...TODO: Investigate
context?.fillPath()
let endRadius = hypot(gradientStartInPoints.x-gradientEndInPoints.x, gradientStartInPoints.y-gradientEndInPoints.y)
context?.drawRadialGradient(gradient!, startCenter: gradientStartInPoints, startRadius: 0, endCenter: gradientStartInPoints, endRadius: endRadius, options: [])
}
case .solid:
trueBackgroundColor.setFill()
context?.addPath(bgPath); // Not sure why I need this...TODO: Investigate
context?.fillPath()
}
context?.restoreGState(); // Now we are no longer clipped to the path
// Border
context?.addPath(borderPath);
context?.strokePath();
}
// MARK: Config
public func setCornerRadius(_ radius: CGFloat, corners: UIRectCorner = .allCorners)
{
if(corners.contains(.allCorners))
{
cornerRadBL = radius
cornerRadBR = radius
cornerRadTL = radius
cornerRadTR = radius
return
}
if(corners.contains(.bottomLeft)) {cornerRadBL = radius}
if(corners.contains(.bottomRight)) {cornerRadBR = radius}
if(corners.contains(.topLeft)) {cornerRadTL = radius}
if(corners.contains(.topRight)) {cornerRadTR = radius}
}
}
Here is the support file needed for the class to compile:
//
// AppocalypseUI.swift
// Soapbox
//
// Created by Joseph Falcone on 6/2/16.
// Copyright © 2016 Joseph Falcone. All rights reserved.
//
import UIKit
public class AppocalypseUI: NSObject
{
/// Generates an array of CGFloat values ranging from 0.0-1.0 which represent the color stops in a gradient
public class func makeLinearColorStops(_ numStops:Int) -> [CGFloat]
{
assert(numStops >= 2, "Must have at least two color stops.")
let stepIncrement = 1.0/Double(numStops-1)
var returnArr : [CGFloat] = []
// The first stop is always 0
returnArr += [0.0]
for i in 1 ..< numStops-1
{
let stepVal = stepIncrement*Double(i)
let stepFactor = CGFloat(fmod(stepVal, 1.0))
returnArr += [stepFactor]
}
// The last stop is always 1
returnArr += [1.0]
// Fini
return returnArr
}
/// Returns the stop colors in an array
public class func colorsAlongArray(_ colorArr:[UIColor], steps:Int) -> [UIColor]
{
let arrCount = colorArr.count
let stepIncrement = Double(arrCount)/Double(steps)
var returnArr : [UIColor] = []
for i in 0..<steps
{
let stepVal = stepIncrement*Double(i)
let stepFactor = CGFloat(fmod(stepVal, 1.0))
let stepIndex1 = Int(floor(stepVal/1.0))
var stepIndex2 = Int(ceil(stepVal/1.0))
if(stepIndex2 > arrCount-1)
{stepIndex2 = arrCount-1}
let color1 = colorArr[stepIndex1]
let color2 = colorArr[stepIndex2]
let color = colorByInterpolatingColors(color1, color2: color2, factor: stepFactor)
returnArr += [color]
}
return returnArr
}
/// Returns a color between two colors on a gradient
public class func colorByInterpolatingColors(_ color1:UIColor, color2:UIColor, factor:CGFloat) -> UIColor
{
/*
// components is no longer exposed for some reason...
let startComponent = color1.cgColor.components
let endComponent = color2.cgColor.components
let startAlpha = color1.cgColor.alpha
let endAlpha = color2.cgColor.alpha
let r = (startComponent?[0])! + ((endComponent?[0])! - (startComponent?[0])!) * factor
let g = (startComponent?[1])! + ((endComponent?[1])! - (startComponent?[1])!) * factor
let b = (startComponent?[2])! + ((endComponent?[2])! - (startComponent?[2])!) * factor
let a = startAlpha + (endAlpha - startAlpha) * factor
*/
// Instead, we'll convert to a CIColor
let ci1 = CoreImage.CIColor(color: color1)
let ci2 = CoreImage.CIColor(color: color2)
let r = ci1.red + (ci2.red - ci1.red) * factor
let g = ci1.green + (ci2.green - ci1.green) * factor
let b = ci1.blue + (ci2.blue - ci1.blue) * factor
let a = ci1.alpha + (ci2.alpha - ci1.alpha) * factor
return UIColor(red: r, green: g, blue: b, alpha: a)
}
/// Returns an array containing the RGBA components of an array of colors
public class func getFloatArrayFromUIColors(_ colors:[UIColor]) -> [CGFloat]
{
var returnArr : [CGFloat] = []
for color : UIColor in colors
{
var red : CGFloat = 0.0
var green : CGFloat = 0.0
var blue : CGFloat = 0.0
var alpha : CGFloat = 0.0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
returnArr += [red, green, blue, alpha]
}
return returnArr
}
/// Returns a path for a rectangle with rounded corners
public class func newPathForRoundedRect(_ rect:CGRect, radiusTL radTL:CGFloat, radiusTR radTR:CGFloat, radiusBL radBL:CGFloat, radiusBR radBR:CGFloat, edges:UIRectEdge = .all) -> CGPath
{
let retPath = CGMutablePath()
// Convenience
let rectL = rect.origin.x
let rectR = rect.origin.x+rect.size.width
let rectT = rect.origin.y
let rectB = rect.origin.y+rect.size.height
/*
P2 P3
P1 P4
P8 P5
P7 P6
1.5π
*
π * * 0
*
0.5π
AT
*
AL * * AR
*
AB
*/
// Starting from the top left arc, move clockwise
let p1 = CGPoint(x: rectL , y: rectT+radTL)
let p2 = CGPoint(x: rectL+radTL , y: rectT)
let p3 = CGPoint(x: rectR-radTR , y: rectT)
let p4 = CGPoint(x: rectR , y: rectT+radTR)
let p5 = CGPoint(x: rectR , y: rectB-radBR)
let p6 = CGPoint(x: rectR-radBR , y: rectB)
let p7 = CGPoint(x: rectL+radBL , y: rectB)
let p8 = CGPoint(x: rectL , y: rectB-radBL)
let c1 = CGPoint(x: rect.origin.x , y: rect.origin.y)
let c2 = CGPoint(x: rect.origin.x+rect.size.width , y: rect.origin.y)
let c3 = CGPoint(x: rect.origin.x+rect.size.width , y: rect.origin.y+rect.size.height)
let c4 = CGPoint(x: rect.origin.x , y: rect.origin.y+rect.size.height)
/*
//let π = M_PI // The shortcut for π is Alt+P
let aT : CGFloat = CGFloat(M_PI*1.5)
let aR : CGFloat = 0
let aB : CGFloat = CGFloat(M_PI_2)
let aL : CGFloat = CGFloat(M_PI)
let cTL = CGPoint(x: rect.origin.x + radTL/2, y: rect.origin.y + radTL/2)
let cTR = CGPoint(x: rect.origin.x+rect.size.width - radTR/2, y: rect.origin.y + radTR/2)
let cBL = CGPoint(x: rect.origin.x + radBL/2, y: rect.origin.y+rect.size.height - radBL/2)
let cBR = CGPoint(x: rect.origin.x+rect.size.width - radBR/2, y: rect.origin.y+rect.size.height - radBR/2)
*/
if(edges.contains(.all) || (edges.contains(.left) && edges.contains(.right) && edges.contains(.top) && edges.contains(.bottom)))
{
retPath.move(to: p1)
retPath.addArc(tangent1End: c1, tangent2End: p2, radius: radTL)
retPath.addLine(to: p3)
retPath.addArc(tangent1End: c2, tangent2End: p4, radius: radTR)
retPath.addLine(to: p5)
retPath.addArc(tangent1End: c3, tangent2End: p6, radius: radBR)
retPath.addLine(to: p7)
retPath.addArc(tangent1End: c4, tangent2End: p8, radius: radBL)
retPath.addLine(to: p1)
retPath.closeSubpath()
return retPath
}
if(edges.contains(.top))
{
retPath.move(to: p1)
retPath.addArc(tangent1End: c1, tangent2End: p2, radius: radTL)
retPath.addLine(to: p3)
retPath.addArc(tangent1End: c2, tangent2End: p4, radius: radTR)
}
if(edges.contains(.right))
{
retPath.move(to: p3)
retPath.addArc(tangent1End: c2, tangent2End: p4, radius: radTR)
retPath.addLine(to: p5)
retPath.addArc(tangent1End: c3, tangent2End: p6, radius: radBR)
}
if(edges.contains(.bottom))
{
retPath.move(to: p5)
retPath.addArc(tangent1End: c3, tangent2End: p6, radius: radBR)
retPath.addLine(to: p7)
retPath.addArc(tangent1End: c4, tangent2End: p8, radius: radBL)
}
if(edges.contains(.left))
{
retPath.move(to: p7)
retPath.addArc(tangent1End: c4, tangent2End: p8, radius: radBL)
retPath.addLine(to: p1)
retPath.addArc(tangent1End: c1, tangent2End: p2, radius: radTL)
}
return retPath
}
public class func createHorizontalArcPath(_ startPoint:CGPoint, width:CGFloat, arcHeight:CGFloat, closed:Bool = false) -> CGMutablePath
{
// http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths
let arcRect = CGRect(x: startPoint.x, y: startPoint.y-arcHeight, width: width, height: arcHeight)
let arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
let arcCenter = CGPoint(x: arcRect.origin.x + arcRect.size.width/2, y: arcRect.origin.y + arcRadius);
let angle = acos(arcRect.size.width / (2*arcRadius));
let startAngle = CGFloat(M_PI)+angle // (180 degrees + angle)
let endAngle = CGFloat(M_PI*2)-angle // (360 degrees - angle)
let path = CGMutablePath();
path.addArc(center: arcCenter, radius: arcRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
if(closed == true)
{path.addLine(to: startPoint)}
return path;
}
}