Making an oval gradient in shape - ios

I am trying to find out how to create an oval gradient which is not a perfect circle. For example an american football/rugby/eye shape instead of a circle.
At the moment I have created a CALayer subclass as below which creates a circle. How can I get this to a oval shape (as mentioned above)?
class CircleGradientLayer: CALayer {
var startRadius: CGFloat?
var startCenter: CGPoint?
var endCenter: CGPoint?
var endRadius: CGFloat?
var locations: [CGFloat]?
var colors: [UIColor]?
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
if let colors = self.colors, let locations = self.locations, let startRadius = self.startRadius, let startCenter = self.startCenter, let endCenter = self.endCenter, let endRadius = self.endRadius {
var colorSpace: CGColorSpaceRef?
var components = [CGFloat]()
for i in 0 ..< colors.count {
let colorRef = colors[i].CGColor
let colorComponents = CGColorGetComponents(colorRef)
let numComponents = CGColorGetNumberOfComponents(colorRef)
if colorSpace == nil {
colorSpace = CGColorGetColorSpace(colorRef)
}
for j in 0 ..< numComponents {
let component = colorComponents[j]
components.append(component)
}
}
if let colorSpace = colorSpace {
let gradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, locations.count)
CGContextDrawRadialGradient(ctx, gradient, startCenter, startRadius, endCenter, endRadius, CGGradientDrawingOptions.DrawsAfterEndLocation)
}
}
}
}

One way to do it would be to set your layer's transform by scaling the X up by some factor. For example:
layer.transform = CATransform3DMakeScale(1.5, 1.0)
Not sure if that's the best solution or not, but it should work!

Related

How do I make a hexagon with 6 triangular SCNNodes?

I'm trying to make a hexagon grid with triangles without altering any pivot points, but I can't seem to position the triangles correctly to make single hexagon. I'm creating SCNNodes with UIBezierPaths to form triangles and then rotating the bezier paths. This seems to work fine UNTIL I try to use a parametric equation to position the triangles around a circle to form the hexagon, then they don't end up in the correct position. Can you help me spot where I'm doing wrong here?
class TrianglePlane: SCNNode {
var size: CGFloat = 0.1
var coords: SCNVector3 = SCNVector3Zero
var innerCoords: Int = 0
init(coords: SCNVector3, innerCoords: Int, identifier: Int) {
super.init()
self.coords = coords
self.innerCoords = innerCoords
setup()
}
init(identifier: Int) {
super.init()
// super.init(identifier: identifier)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
let myPath = path()
let geo = SCNShape(path: myPath, extrusionDepth: 0)
geo.firstMaterial?.diffuse.contents = UIColor.red
geo.firstMaterial?.blendMode = .multiply
self.geometry = geo
}
func path() -> UIBezierPath {
let max: CGFloat = self.size
let min: CGFloat = 0
let bPath = UIBezierPath()
bPath.move(to: .zero)
bPath.addLine(to: CGPoint(x: max / 2,
y: UIBezierPath.middlePeak(height: max)))
bPath.addLine(to: CGPoint(x: max, y: min))
bPath.close()
return bPath
}
}
extension TrianglePlane {
static func generateHexagon() -> [TrianglePlane] {
var myArr: [TrianglePlane] = []
let colors = [UIColor.red, UIColor.green,
UIColor.yellow, UIColor.systemTeal,
UIColor.cyan, UIColor.magenta]
for i in 0 ..< 6 {
let tri = TrianglePlane(identifier: 0)
tri.geometry?.firstMaterial?.diffuse.contents = colors[i]
tri.position = SCNVector3( -0.05, 0, -0.5)
// Rotate bezier path
let angleInDegrees = (Float(i) + 1) * 180.0
print(angleInDegrees)
let angle = CGFloat(deg2rad(angleInDegrees))
let geo = tri.geometry as! SCNShape
let path = geo.path!
path.rotateAroundCenter(angle: angle)
geo.path = path
// Position triangle in hexagon
let radius = Float(tri.size)/2
let deg: Float = Float(i) * 60
let radians = deg2rad(-deg)
let x1 = tri.position.x + radius * cos(radians)
let y1 = tri.position.y + radius * sin(radians)
tri.position.x = x1
tri.position.y = y1
myArr.append(tri)
}
return myArr
}
static func deg2rad(_ number: Float) -> Float {
return number * Float.pi / 180
}
}
extension UIBezierPath {
func rotateAroundCenter(angle: CGFloat) {
let center = self.bounds.center
var transform = CGAffineTransform.identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: angle)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
static func middlePeak(height: CGFloat) -> CGFloat {
return sqrt(3.0) / 2 * height
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
What it currently looks like:
What it SHOULD look like:
I created two versions – SceneKit and RealityKit.
SceneKit (macOS version)
The simplest way to compose a hexagon is to use six non-uniformly scaled SCNPyramids (flat) with their shifted pivot points. Each "triangle" must be rotated in 60 degree increments (.pi/3).
import SceneKit
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let sceneView = self.view as! SCNView
let scene = SCNScene()
sceneView.scene = scene
sceneView.allowsCameraControl = true
sceneView.backgroundColor = NSColor.white
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
for i in 1...6 {
let triangleNode = SCNNode(geometry: SCNPyramid(width: 1.15,
height: 1,
length: 1))
// the depth of the pyramid is almost zero
triangleNode.scale = SCNVector3(5, 5, 0.001)
// move a pivot point from pyramid its base to upper vertex
triangleNode.simdPivot.columns.3.y = 1
triangleNode.geometry?.firstMaterial?.diffuse.contents = NSColor(
calibratedHue: CGFloat(i)/6,
saturation: 1.0,
brightness: 1.0,
alpha: 1.0)
triangleNode.rotation = SCNVector4(0, 0, 1,
-CGFloat.pi/3 * CGFloat(i))
scene.rootNode.addChildNode(triangleNode)
}
}
}
RealityKit (iOS version)
In this project I generated a triangle with the help of MeshDescriptor and copied it 5 more times.
import UIKit
import RealityKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
let anchor = AnchorEntity()
let camera = PointOfView()
let indices: [UInt32] = [0, 1, 2]
override func viewDidLoad() {
super.viewDidLoad()
self.arView.environment.background = .color(.black)
self.arView.cameraMode = .nonAR
self.camera.position.z = 9
let positions: [simd_float3] = [[ 0.00, 0.00, 0.00],
[ 0.52, 0.90, 0.00],
[-0.52, 0.90, 0.00]]
var descriptor = MeshDescriptor(name: "Hexagon's side")
descriptor.materials = .perFace(self.indices)
descriptor.primitives = .triangles(self.indices)
descriptor.positions = MeshBuffers.Positions(positions[0...2])
var material = UnlitMaterial()
let mesh: MeshResource = try! .generate(from: [descriptor])
let colors: [UIColor] = [.systemRed, .systemGreen, .yellow,
.systemTeal, .cyan, .magenta]
for i in 0...5 {
material.color = .init(tint: colors[i], texture: nil)
let triangleModel = ModelEntity(mesh: mesh,
materials: [material])
let trianglePivot = Entity() // made to control pivot point
trianglePivot.addChild(triangleModel)
trianglePivot.orientation = simd_quatf(angle: -.pi/3 * Float(i),
axis: [0,0,1])
self.anchor.addChild(trianglePivot)
}
self.anchor.addChild(self.camera)
self.arView.scene.anchors.append(self.anchor)
}
}
There are a few problems with the code as it stands. Firstly, as pointed out in the comments, the parametric equation for the translations needs to be rotated by 90 degrees:
let deg: Float = (Float(i) * 60) - 90.0
The next issue is that the centre of the bounding box of the triangle and the centroid of the triangle are not the same point. This is important because the parametric equation calculates where the centroids of the triangles must be located, not the centres of their bounding boxes. So we're going to need a way to calculate the centroid. This can be done by adding the following extension method to TrianglePlane:
extension TrianglePlane {
/// Calculates the centroid of the triangle
func centroid() -> CGPoint
{
let max: CGFloat = self.size
let min: CGFloat = 0
let peak = UIBezierPath.middlePeak(height: max)
let xAvg = (min + max / CGFloat(2.0) + max) / CGFloat(3.0)
let yAvg = (min + peak + min) / CGFloat(3.0)
return CGPoint(x: xAvg, y: yAvg)
}
}
This allows the correct radius for the parametric equation to be calculated:
let height = Float(UIBezierPath.middlePeak(height: tri.size))
let centroid = tri.centroid()
let radius = height - Float(centroid.y)
The final correction is to calculate the offset between the origin of the triangle and the centroid. This correction depends on whether the triangle has been flipped by the rotation or not:
let x1 = radius * cos(radians)
let y1 = radius * sin(radians)
let dx = Float(-centroid.x)
let dy = (i % 2 == 0) ? Float(centroid.y) - height : Float(-centroid.y)
tri.position.x = x1 + dx
tri.position.y = y1 + dy
Putting all this together gives the desired result.
Full working ViewController can be found int this gist
Note the code can be greatly simplified by making the origin of the triangle be the centroid.

How to fix inconsistent color-to-clear radial gradient layer

When trying to draw a radial gradient from white to clear, I obtain different results depending on whether I use UIColor(white:alpha:), UIColor.white.withAlpha.Components(alpha:) or UIColor.color. How Can I get the same gradient with a clear color as with a plain color?
I order to draw a radial gradient, I have overridden the draw(context:) method (see below). My code seems to work fine when using plain colours for a gradient but works "mysteriously" when using a clear color.
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
ctx.saveGState()
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colorFill as CFArray, locations: self.locations as! [CGFloat] )
let startPoint = CGPoint(x: startX, y: startY )
let endPoint = CGPoint(x: endX , y: endY)
let drawingOption : CGGradientDrawingOptions = radius > 0 ? [] : .drawsAfterEndLocation
//self.masksToBounds = true
if gradientType == .radial {
ctx.drawRadialGradient(gradient!, startCenter: startPoint, startRadius: 0.0, endCenter: endPoint, endRadius: self.frame.width / 2, options: drawingOption)
} else {
self.startPoint = startPoint
self.endPoint = endPoint
}
}
Here's the results I obtain depending on the input colours for the gradient:
[UIColor.white.cgColor,UIColor.black.cgColor]
(desired result with clear color instead of black)
[UIColor.white.cgColor, UIColor.clear.cgColor]
[UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
[UIColor(white: 0.0, alpha: 1).cgColor, UIColor(white: 1.0, alpha: 0).cgColor]
Could someone can explain why I don't get the same output as with only plain colours (output I wish to obtain)?
Thanks a lot for taking the time to read and reply!
The layer is not drawn only in the draw(in context:) method. This line of code confirms some properties had been set prior to the call of the method, which has priorly triggered the drawing of the layer (self.locations):
CGColorSpaceCreateDeviceRGB(), colors: colorFill as CFArray, locations: self.locations as! [CGFloat] )
By removing the set of self.colors and self.locations properties in the custom initializer and storing the values in Type Properties instead before calling self.setNeedDisplay(), the layer is only being drawn in the draw(in context:) method.
class CustomLayer {
override init() {
super.init()
needsDisplayOnBoundsChange = true
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var colors = []()
var colorMap = [NSNumber]()
custom init() {
colors = [color,color]
colorMap = [0.0, 1.0]
self.setNeedDisplay()
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
ctx.saveGState()
guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: colorMap as! [CGFloat] ) else {return}
let startPoint = CGPoint(x: startX, y: startY )
let endPoint = CGPoint(x: endX , y: endY)
self.masksToBounds = true
if gradientType == .radial {
ctx.drawRadialGradient(gradient, startCenter: startPoint, startRadius: 0.0, endCenter: endPoint, endRadius: self.frame.width / 2, options: drawingOption)
} else {
self.locations = colorMap
self.colors = colors
self.startPoint = startPoint
self.endPoint = endPoint
}
}
}
Cheers!

Filling Undefined forms with Gradient color SWIFT

I am new to programming and I have no idea how I can fill a undefined geometrical form with a gradient color...
I managed to do with a simple color like that:
func fillRegion(pixelX: Int, pixelY: Int, withColor color: UIColor) {
var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
var newColor = (UInt32)(alpha*255)<<24 | (UInt32)(red*255)<<16 | (UInt32)(green*255)<<8 | (UInt32)(blue*255)<<0
let pixelColor = regionsData.advanced(by: (pixelY * imageHeight) + pixelX).pointee
if pixelColor == blackColor { return }
var pointerRegionsData: UnsafeMutablePointer<UInt32> = regionsData
var pointerImageData: UnsafeMutablePointer<UInt32> = imageData
var pixelsChanged = false
for i in 0...(imageHeight * imageHeight - 1) {
if pointerRegionsData.pointee == pixelColor {
pointerImageData = imageData.advanced(by: i)
if pointerImageData.pointee != newColor {
// newColor = newColor + 1
pointerImageData.pointee = newColor
pixelsChanged = true
}
}
pointerRegionsData = pointerRegionsData.successor()
}
if pixelsChanged {
self.image = UIImage(cgImage: imageContext.makeImage()!)
DispatchQueue.main.async {
CATransaction.setDisableActions(true)
self.layer.contents = self.image.cgImage
self.onImageDraw?(self.image)
}
self.playTapSound()
}
}
Pixel by pixel it fill the color (ignoring the black color) any ideas how to do that with Gradient color? thanks!
You can make a gradient layer and apply an image or a shape layer as its mask. Here is a playground.
import PlaygroundSupport
import UIKit
class V: UIView {
private lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.red.cgColor,
UIColor.purple.cgColor,
UIColor.blue.cgColor,
UIColor.white.cgColor]
gradientLayer.locations = [0, 0.3, 0.9, 1]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
gradientLayer.mask = self.strokeLayer
self.layer.addSublayer(gradientLayer)
return gradientLayer
}()
private lazy var strokeLayer: CAShapeLayer = {
let strokeLayer = CAShapeLayer()
strokeLayer.path = UIBezierPath(ovalIn: CGRect(x:0, y: 0, width: 100, height: 100)).cgPath
return strokeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
strokeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
}
}
let v = V(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
PlaygroundPage.current.liveView = v
I'm not 100% sure I understand the question, but it seems like you want to fill any-old shape with a gradient, right? If so, there are a couple of ways to do that, but the easiest is to make a gradient that's the same size as the boundary of the shape and then apply that as its color. I'm typing this on my PC so I'm sure there's syntax errors, but here goes...
let size = CGSize(width, height)
UIGraphicsRenderer(size, false, 0) // I KNOW I have this one wrong
let colors = [tColour.cgColor, bColour.cgColor] as CFArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors , locations: nil)
Set the colors array as needed and then send that into the UIImage. You can use locations: to change the orientation.

Convert from CIRectangleFeature coordinates to view coordinates

I am using the CIDetector class in IOS to find CIRectangleFeatures in my UIImage. Afterwards, I aim to show the cornerPoints drawn into a layer which I then, in turn, add to my UIImageView. Unfortunately, the coordinates given with the CIRectangleFeature are in image space. And, even though I am trying to convert them using the CGContextConvertToUserSpace function, the rectangle drawn is quite off of the actual rectangle in the image.
Here is my code, when an image is taken, workWithTheImage is called:
func analyzeImage(image: UIImage) -> [RectanglePoint]
{
guard let ciImage = CIImage(image: image)
else { return [] }
let context = CIContext(options: nil)
let detector = CIDetector(ofType: CIDetectorTypeRectangle, context: context, options: nil)
let features = detector.featuresInImage(ciImage)
UIGraphicsBeginImageContext(ciImage.extent.size)
let currentContext = UIGraphicsGetCurrentContext()
var points: [RectanglePoint] = []
for feature in features as! [CIRectangleFeature]
{
let topLeft = CGContextConvertPointToUserSpace(currentContext, feature.topLeft)
let topRight = CGContextConvertPointToUserSpace(currentContext, feature.topRight)
let bottomRight = CGContextConvertPointToUserSpace(currentContext, feature.bottomRight)
let bottomLeft = CGContextConvertPointToUserSpace(currentContext, feature.bottomLeft)
let point = RectanglePoint(bottomLeft: bottomLeft, topLeft: topLeft, bottomRight: bottomRight, topRight: topRight)
points.append(point)
}
UIGraphicsEndImageContext()
return points
}
func workWithImage(image: UIImage)
{
let imageView = UIImageView(frame: view.frame)
imageView.image = image
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = imageView.bounds
for i in analyzeImage(image)
{
path.moveToPoint(i.topLeft)
path.addLineToPoint(i.topRight)
path.addLineToPoint(i.bottomRight)
path.addLineToPoint(i.bottomLeft)
path.addLineToPoint(i.topLeft)
}
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = UIColor.redColor().CGColor
imageView.layer.addSublayer(shapeLayer)
}
If your path is only off in the Y dimension:
I think they haven't fully ported CIDetector to UIKit yet. The coordinates of the feature are in the Cocoa coordinate system. Simply doing container.height - point.y will convert it.
I also gave your struct the correct name. The rest of the stuff in there, I used to figure out what was going on. Might be useful to you.
Code :
func analyzeImage(image: UIImage) -> [Quadrilateral]
{
guard let ciImage = CIImage(image: image)
else { return [] }
let flip = true // set to false to prevent flipping the coordinates
let context = CIContext(options: nil)
let detector = CIDetector(ofType: CIDetectorTypeRectangle, context: context, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh])
let features = detector.featuresInImage(ciImage)
UIGraphicsBeginImageContext(ciImage.extent.size)
let currentContext = UIGraphicsGetCurrentContext()
var frames: [Quadrilateral] = []
for feature in features as! [CIRectangleFeature]
{
var topLeft = CGContextConvertPointToUserSpace(currentContext, feature.topLeft)
var topRight = CGContextConvertPointToUserSpace(currentContext, feature.topRight)
var bottomRight = CGContextConvertPointToUserSpace(currentContext, feature.bottomRight)
var bottomLeft = CGContextConvertPointToUserSpace(currentContext, feature.bottomLeft)
if flip {
topLeft = CGPoint(x: topLeft.x, y: image.size.height - topLeft.y)
topRight = CGPoint(x: topRight.x, y: image.size.height - topRight.y)
bottomLeft = CGPoint(x: bottomLeft.x, y: image.size.height - bottomLeft.y)
bottomRight = CGPoint(x: bottomRight.x, y: image.size.height - bottomRight.y)
}
let frame = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
frames.append(frame)
}
UIGraphicsEndImageContext()
return frames
}
Quadrilateral struct :
struct Quadrilateral {
var topLeft : CGPoint = CGPointZero
var topRight : CGPoint = CGPointZero
var bottomLeft : CGPoint = CGPointZero
var bottomRight : CGPoint = CGPointZero
var path : UIBezierPath {
get {
let tempPath = UIBezierPath()
tempPath.moveToPoint(topLeft)
tempPath.addLineToPoint(topRight)
tempPath.addLineToPoint(bottomRight)
tempPath.addLineToPoint(bottomLeft)
tempPath.addLineToPoint(topLeft)
return tempPath
}
}
init(topLeft topLeft_I: CGPoint, topRight topRight_I: CGPoint, bottomLeft bottomLeft_I: CGPoint, bottomRight bottomRight_I: CGPoint) {
topLeft = topLeft_I
topRight = topRight_I
bottomLeft = bottomLeft_I
bottomRight = bottomRight_I
}
var frame : CGRect {
get {
let highestPoint = max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
let lowestPoint = min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
let farthestPoint = max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
let closestPoint = min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
// you might want to set origin to (0,0)
let origin = CGPoint(x: closestPoint, y: lowestPoint)
let size = CGSize(width: farthestPoint, height: highestPoint)
return CGRect(origin: origin, size: size)
}
}
var size : CGSize {
get {
return frame.size
}
}
var origin : CGPoint {
get {
return frame.origin
}
}
}

EXC_BAD_INSTRUCTION when trying to pass data to UIViews

I created a call of a UIView in which I draw a graph. I am trying to pass it new data and have it update.
When I run the app on the simulator and click the tab in which the the controller housing the view is in, I receive this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0)
at the line:
let maxValue = graphPoints.maxElement()
Here is my code for the view:
#IBDesignable class GraphView: UIView {
var graphPoints :[Int]!
override init(frame: CGRect) {
super.init(frame: frame)
}
init(graphPoints: [Int]) {
self.graphPoints = graphPoints
super.init(frame: CGRectZero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#IBInspectable var startColor: UIColor = UIColor.redColor()
#IBInspectable var endColor: UIColor = UIColor.greenColor()
override func drawRect(rect: CGRect) {
let width = rect.width
let height = rect.height
//set up background clipping area
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: UIRectCorner.AllCorners,
cornerRadii: CGSize(width: 8.0, height: 8.0))
path.addClip()
//2 - get the current context
let context = UIGraphicsGetCurrentContext()
let colors = [startColor.CGColor, endColor.CGColor]
//3 - set up the color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
//4 - set up the color stops
let colorLocations:[CGFloat] = [0.0, 1.0]
//5 - create the gradient
let gradient = CGGradientCreateWithColors(colorSpace,
colors,
colorLocations)
//6 - draw the gradient
var startPoint = CGPoint.zeroPoint
var endPoint = CGPoint(x:0, y:self.bounds.height)
CGContextDrawLinearGradient(context,
gradient,
startPoint,
endPoint,
.DrawsBeforeStartLocation)
//calculate the x point
let rightMargin:CGFloat = 40
let leftMargin : CGFloat = 10
let columnXPoint = { (column:Int) -> CGFloat in
//Calculate gap between points
let spacer = (width - rightMargin - leftMargin - 4) /
CGFloat((self.graphPoints.count - 1))
var x:CGFloat = CGFloat(column) * spacer
x += leftMargin + 2
print(x)
return x
}
// calculate the y point
let topBorder:CGFloat = 30
let bottomBorder:CGFloat = 50
let graphHeight = height - topBorder - bottomBorder
let maxValue = graphPoints.maxElement()
let columnYPoint = { (graphPoint:Int) -> CGFloat in
var y:CGFloat = CGFloat(graphPoint) /
CGFloat(maxValue!) * graphHeight
y = graphHeight + topBorder - y // Flip the graph
print(y)
return y
}
// draw the line graph
UIColor.whiteColor().setFill()
UIColor.whiteColor().setStroke()
//set up the points line
let graphPath = UIBezierPath()
//go to start of line
graphPath.moveToPoint(CGPoint(x:columnXPoint(0), y:columnYPoint(graphPoints[0])))
//add points for each item in the graphPoints array
//at the correct (x, y) for the point
for i in 1..<graphPoints.count {
let nextPoint = CGPoint(x:columnXPoint(i),
y:columnYPoint(graphPoints[i]))
graphPath.addLineToPoint(nextPoint)
}
//Create the clipping path for the graph gradient
//1 - save the state of the context (commented out for now)
CGContextSaveGState(context)
//2 - make a copy of the path
let clippingPath = graphPath.copy() as! UIBezierPath
//3 - add lines to the copied path to complete the clip area
clippingPath.addLineToPoint(CGPoint(
x: columnXPoint(graphPoints.count - 1),
y:height))
clippingPath.addLineToPoint(CGPoint(
x:columnXPoint(0),
y:height))
clippingPath.closePath()
//4 - add the clipping path to the context
clippingPath.addClip()
let highestYPoint = columnYPoint(maxValue!)
startPoint = CGPoint(x:leftMargin, y: highestYPoint)
endPoint = CGPoint(x:rightMargin, y:self.bounds.height)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, .DrawsBeforeStartLocation)
CGContextRestoreGState(context)
//draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()
//Draw the circles on top of graph stroke
for i in 0..<graphPoints.count {
var point = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints[i]))
point.x -= 5.0/2
point.y -= 5.0/2
let circle = UIBezierPath(ovalInRect:
CGRect(origin: point,
size: CGSize(width: 5.0, height: 5.0)))
circle.fill()
}
//Draw horizontal graph lines on the top of everything
let linePath = UIBezierPath()
//top line
linePath.moveToPoint(CGPoint(x:leftMargin, y: topBorder))
linePath.addLineToPoint(CGPoint(x: width - rightMargin,
y:topBorder))
//center line
linePath.moveToPoint(CGPoint(x:leftMargin,
y: graphHeight/2 + topBorder))
linePath.addLineToPoint(CGPoint(x:width - rightMargin,
y:graphHeight/2 + topBorder))
//bottom line
linePath.moveToPoint(CGPoint(x:leftMargin,
y:height - bottomBorder))
linePath.addLineToPoint(CGPoint(x:width - rightMargin,
y:height - bottomBorder))
let color = UIColor(white: 1.0, alpha: 0.3)
color.setStroke()
linePath.lineWidth = 1.0
linePath.stroke()
}
And here is the code for my view controller in which I pass data to the view.
import UIKit
import QuartzCore
class ProgressViewController: UIViewController {
#IBOutlet weak var graphView: UIView!
var firstGraph : GraphView!
override func viewDidLoad() {
self.graphView = self.firstGraph
self.firstGraph = GraphView(graphPoints: [2240, 1983, 2171, 2017, 1842, 1992, 2347])
I'm new to Swift and I'm stumped on this problem after looking everywhere for an answer. Any help is appreciated.

Resources