I have a simple custom CALayer to create an overlaying gradient effect on my UIView. Here is the code:
class GradientLayer: CALayer {
var locations: [CGFloat]?
var origin: CGPoint?
var radius: CGFloat?
var color: CGColor?
convenience init(view: UIView, locations: [CGFloat]?, origin: CGPoint?, radius: CGFloat?, color: UIColor?) {
self.init()
self.locations = locations
self.origin = origin
self.radius = radius
self.color = color?.CGColor
self.frame = view.bounds
}
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
guard let locations = self.locations else { return }
guard let origin = self.origin else { return }
guard let radius = self.radius else { return }
let colorSpace = CGColorGetColorSpace(color)
let colorComponents = CGColorGetComponents(color)
let gradient = CGGradientCreateWithColorComponents(colorSpace, colorComponents, locations, locations.count)
CGContextDrawRadialGradient(ctx, gradient, origin, CGFloat(0), origin, radius, [.DrawsAfterEndLocation])
}
}
I initialize and set these layers here:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let gradient1 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 100.0, color: UIColor(white: 1.0, alpha: 0.2))
let gradient2 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX-20, y: view.frame.midY+20), radius: 160.0, color: UIColor(white: 1.0, alpha: 0.2))
let gradient3 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX+30, y: view.frame.midY-30), radius: 300.0, color: UIColor(white: 1.0, alpha: 0.2))
gradient1.setNeedsDisplay()
gradient2.setNeedsDisplay()
gradient3.setNeedsDisplay()
view.layer.addSublayer(gradient1)
view.layer.addSublayer(gradient2)
view.layer.addSublayer(gradient3)
}
The view seems to display properly most of the time, but (seemingly) randomly I'll get different renderings as you'll see below. Here are some examples (the first one is what I want):
What is causing this malfunction? How do I only load the first one every time?
You have several problems.
First off, you should think of a gradient as an array of stops, where a stop has two parts: a color and a location. You must have an equal number of colors and locations, because every stop has one of each. You can see this if, for example, you check the CGGradientCreateWithColorComponents documentation regarding the components argument:
The number of items in this array should be the product of count and the number of components in the color space.
It's a product (the result of a multiplication) because you have count stops and you need a complete set of color components for each stop.
You're not providing enough color components. Your GradientLayer could have any number of locations (and you're giving it two) but has only one color. You're getting that one color's components and passing that as the components array to CGGradientCreateWithColorComponents, but the array is too short. Swift doesn't catch this error—notice that the type of your colorComponents is UnsafePointer<CGFloat>. The Unsafe part tells you that you're in dangerous territory. (You can see the type of colorComponents by option-clicking it in Xcode.)
Since you're not providing a large enough array for components, iOS is using whatever random values happen to be in memory after the components of your one color. Those may change from run to run and are often not what you want them to be.
In fact, you shouldn't even use CGGradientCreateWithColorComponents. You should use CGGradientCreateWithColors, which takes an array of CGColor so it's not only simpler to use, but safer because it's one less UnsafePointer floating around.
Here's what GradientLayer should look like:
class RadialGradientLayer: CALayer {
struct Stop {
var location: CGFloat
var color: UIColor
}
var stops: [Stop] { didSet { self.setNeedsDisplay() } }
var origin: CGPoint { didSet { self.setNeedsDisplay() } }
var radius: CGFloat { didSet { self.setNeedsDisplay() } }
init(stops: [Stop], origin: CGPoint, radius: CGFloat) {
self.stops = stops
self.origin = origin
self.radius = radius
super.init()
needsDisplayOnBoundsChange = true
}
override init(layer other: AnyObject) {
guard let other = other as? RadialGradientLayer else { fatalError() }
stops = other.stops
origin = other.origin
radius = other.radius
super.init(layer: other)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawInContext(ctx: CGContext) {
let locations = stops.map { $0.location }
let colors = stops.map { $0.color.CGColor }
locations.withUnsafeBufferPointer { pointer in
let gradient = CGGradientCreateWithColors(nil, colors, pointer.baseAddress)
CGContextDrawRadialGradient(ctx, gradient, origin, 0, origin, radius, [.DrawsAfterEndLocation])
}
}
}
Next problem. You're adding more gradient layers every time the system calls viewWillLayoutSubviews. It can call that function multiple times! For example, it will call it if your app supports interface rotation, or if a call comes in and iOS makes the status bar taller. (You can test that in the simulator by choosing Hardware > Toggle In-Call Status Bar.)
You need to create the gradient layers once, storing them in a property. If they have already been created, you need to update their frames and not create new layers:
class ViewController: UIViewController {
private var gradientLayers = [RadialGradientLayer]()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if gradientLayers.isEmpty {
createGradientLayers()
}
for layer in gradientLayers {
layer.frame = view.bounds
}
}
private func createGradientLayers() {
let bounds = view.bounds
let mid = CGPointMake(bounds.midX, bounds.midY)
typealias Stop = RadialGradientLayer.Stop
for (point, radius, color) in [
(mid, 100, UIColor(white:1, alpha:0.2)),
(CGPointMake(mid.x - 20, mid.y + 20), 160, UIColor(white:1, alpha:0.2)),
(CGPointMake(mid.x + 30, mid.y - 30), 300, UIColor(white:1, alpha:0.2))
] as [(CGPoint, CGFloat, UIColor)] {
let stops: [RadialGradientLayer.Stop] = [
Stop(location: 0, color: color),
Stop(location: 1, color: color.colorWithAlphaComponent(0))]
let layer = RadialGradientLayer(stops: stops, origin: point, radius: radius)
view.layer.addSublayer(layer)
gradientLayers.append(layer)
}
}
}
Your problem is the code you have written in viewWillLayoutSubviews function its is called multiple times when views loads just add a check to run it once or better yet add a check in viewdidlayoutsubviews to run it once
Related
I am trying to draw multiple dots in a loop by passing set of points. But not a single dot is getting drawn on view. Idea - I am parsing xml document and extracting point to draw it on view.
Edit - I have updated the code with suggested changes and it is working.
//View Class
class DrawTrace: UIView
{
var points = [CGPoint]()
{
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect)
{
let size = CGSize(width: 1, height: 1)
UIColor.white.set()
for point in points
{
print(point.x,point.y)
let dot = UIBezierPath(ovalIn: CGRect(origin: point, size: size))
dot.fill()
}
}
}
//View Controller Class
class ViewController: UIViewController
{
var object : traceDoc = traceDoc()
let trace = DrawTrace(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
override func viewDidLoad()
{
super.viewDidLoad()
object.collectionOfData()
trace.points = object.tracePoints
self.view.addSubview(trace)
}
}
Added view instance to view hierarchy that is in view controller.Created instance of DrawTrace and appended to tracepoints array.
Your code is way more complex than it needs to be. One big issue is that you keep adding more and more layers each time draw is called.
There is no need to use layers to draw dots in your custom view. Here is a much simpler solution:
class DrawTrace: UIView
{
var points = [CGPoint]() {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect)
{
let size = CGSize(width: 2, height: 2)
UIColor.black.set()
for point in points {
let dot = UIBezierPath(ovalIn: CGRect(origin: point, size: size))
// this just draws the circle by filling it.
// Update any properties of the path as needed.
// Use dot.stroke() if you need an outline around the circle.
dot.fill()
}
}
}
let trace = DrawTrace(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
trace.backgroundColor = .white
trace.points = [ CGPoint(x: 10, y: 10), CGPoint(x: 35, y: 100)]
The above can be copied into a playground.
Black dotes on black view aren't visible, try to make dotes white:
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.strokeColor = UIColor.white.cgColor
Also, in UIViewController code you are not adding your DrawTrace to views hierarchy, try in viewDidLoad:
self.view.addSubview(drawTraceInstance)
Finally, it seems that you have to iterate over array of CGPoint's. Instead of use stride, user FastEnumeration over Array:
for point in points
{
drawDot(x: point.x, y: point.y,Color: fillColor, size: size)
}
Core-Animation treats angles as described in this image:
(image from http://btk.tillnagel.com/tutorials/rotation-translation-matrix.html)
EDIT: Adding an animated gif to explain better what I'm needing:
I need to animate a slice to grow wider, starting at 300:315 degrees, and ending 300:060.
To create each slice I'm using this function:
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
func createSlice(angle1:CGFloat, angle2:CGFloat) -> UIBezierPath! {
let path: UIBezierPath = UIBezierPath()
let width: CGFloat = self.frame.size.width/2
let height: CGFloat = self.frame.size.height/2
let centerToOrigin: CGFloat = sqrt((height)*(height)+(width)*(width));
let ctr: CGPoint = CGPoint(x: width, y: height)
path.move(to: ctr)
path.addArc( withCenter: ctr,
radius: centerToOrigin,
startAngle: CGFloat(angle1).toRadians(),
endAngle: CGFloat(angle2).toRadians(),
clockwise: true
)
path.close()
return path
}
I can now create the two slices and a sublayer with the smaller one, but I can't find how to proceed from this point:
func doStuff() {
path1 = self.createSlice(angle1: 300,angle2: 315)
path2 = self.createSlice(angle1: 300,angle2: 60)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path1.cgPath
shapeLayer.fillColor = UIColor.cyan.cgColor
self.layer.addSublayer(shapeLayer)
I would highly appreciate any help here!
Only a single color
If you want to animate the angle of a solid color filled pie segment like the one in your question, then you can do it by animating the strokeEnd of a CAShapeLayer.
The "trick" here is to make a very wide line. More specifically, you can create a path that is just an arc (the dashed line in the animation below) at half of the intended radius and then giving it the full radius as its line width. When you animate stroking that line it looks like the orange segment below:
Depending on your use case, you can either:
create a path from one angle to the other angle and animate stroke end from 0 to 1
create a path for a full circle, set stroke start and stroke end to some fraction of the circle, and animate stroke end from the start fraction to the end fraction.
If your drawing is just a single color like this, then this will be the smallest solution to your problem.
However, if your drawing is more complex (e.g. also stroking the pie segment) then this solutions simply won't work and you'll have to do something more complex.
Custom drawing / Custom animations
If your drawing of the pie segment is any more complex, then you'll quickly find yourself having to create a layer subclass with custom animatable properties. Doing so is a bit more code - some of which might look a bit unusual1 - but not as scary as it might sound.
This might be one of those things that is still more convenient to do in Objective-C.
Dynamic properties
First, create a layer subclass with the properties you're going to need. In Objective-C parlance these properties should be #dynamic, i.e. not synthesized. This isn't the same as dynamic in Swift. Instead we have to use #NSManaged.
class PieSegmentLayer : CALayer {
#NSManaged var startAngle, endAngle, strokeWidth: CGFloat
#NSManaged var fillColor, strokeColor: UIColor?
// More to come here ...
}
This allows Core Animation to handle these properties dynamically allowing it to track changes and integrate them into the animation system.
Note: a good rule of thumb is that these properties should all be related to drawing / visual presentation of the layer. If they aren't then it's quite likely that they don't belong on the layer. Instead they could be added to a view that in turn uses the layer for its drawing.
Copying layers
During the custom animation, Core Animation is going to want to create and render different layer configurations for different frames. Unlike most of Apple's other frameworks, this happens using the copy constructor init(layer:). For the above five properties to be copied along, we need to override init(layer:) and copy over their values.
In Swift we also have to override the plain init() and init?(coder).
override init(layer: Any) {
super.init(layer: layer)
guard let other = layer as? PieSegmentLayer else { return }
fillColor = other.fillColor
strokeColor = other.strokeColor
startAngle = other.startAngle
endAngle = other.endAngle
strokeWidth = other.strokeWidth
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
return nil
}
Reacting to change
Core Animation is in many ways built for performance. One of the ways it achieves this is by avoiding unnecessary work. By default, a layer won't redraw itself when a property changes. But these properties is used for drawing, and we want the layer to redraw when any of them changes. To do that, we need to override needsDisplay(forKey:) and return true if the key was one of these properties.
override class func needsDisplay(forKey key: String) -> Bool {
switch key {
case #keyPath(startAngle), #keyPath(endAngle),
#keyPath(strokeWidth),
#keyPath(fillColor), #keyPath(strokeColor):
return true
default:
return super.needsDisplay(forKey: key)
}
}
Additionally, If we want the layers default implicit animations for these properties, we need to override action(forKey:) to return a partially configured animation object. If we only want some properties (e.g. the angles) to implicitly animate, then we only need to return an animation for those properties. Unless we need something very custom, it's good to just return a basic animation with the fromValue set to the current presentation value:
override func action(forKey key: String) -> CAAction? {
switch key {
case #keyPath(startAngle), #keyPath(endAngle):
let anim = CABasicAnimation(keyPath: key)
anim.fromValue = presentation()?.value(forKeyPath: key)
return anim
default:
return super.action(forKey: key)
}
}
Drawing
The last piece of a custom animation is the custom drawing. This is done by overriding draw(in:) and using the supplied context to draw the layer:
override func draw(in ctx: CGContext) {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// subtract half the stroke width to avoid clipping the stroke
let radius = min(center.x, center.y) - strokeWidth / 2
// The two angle properties are in degrees but CG wants them in radians.
let start = startAngle * .pi / 180
let end = endAngle * .pi / 180
ctx.beginPath()
ctx.move(to: center)
ctx.addLine(to: CGPoint(x: center.x + radius * cos(start),
y: center.y + radius * sin(start)))
ctx.addArc(center: center, radius: radius,
startAngle: start, endAngle: end,
clockwise: start > end)
ctx.closePath()
// Configure the graphics context
if let fillCGColor = fillColor?.cgColor {
ctx.setFillColor(fillCGColor)
}
if let strokeCGColor = strokeColor?.cgColor {
ctx.setStrokeColor(strokeCGColor)
}
ctx.setLineWidth(strokeWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
// Draw
ctx.drawPath(using: .fillStroke)
}
Here I've filled and stroked a pie segment that extends from the center of the layer to the nearest edge. You should replace this with your custom drawing.
A custom animation in action
With all that code in place, we now have a custom layer subclass whose properties can be animated both implicitly (just by changing them) and explicitly (by adding a CAAnimation for their key). The results looks something like this:
Final words
It might not be obvious with the frame rate of those animations but one strong benefit from leveraging Core Animation (in different ways) in both these solutions is that it decouples the drawing of a single state from the timing of an animations.
That means that the layer doesn't know and doesn't have to know about the duration, delays, timing curves, etc. These can all be configured and controlled externally.
So at last I have found a solution. It took me time to understand that there is indeed no way to animate the fill of the shape, but we can trick CA engine by creating a filled circle by making the stroke (i.e. the border of the arc) extremely wide, so that it fills the whole circle!
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
import UIKit
class SliceView: UIView {
let circleLayer = CAShapeLayer()
var fromAngle:CGFloat = 30
var toAngle:CGFloat = 150
var color:UIColor = UIColor.magenta
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(frame:CGRect, fromAngle:CGFloat, toAngle:CGFloat, color:UIColor) {
self.init(frame:frame)
self.fromAngle = fromAngle
self.toAngle = toAngle
self.color = color
}
func setup() {
circleLayer.strokeColor = color.cgColor
circleLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(circleLayer)
layer.backgroundColor = UIColor.brown.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
let startAngle:CGFloat = (fromAngle-90).toRadians()
let endAngle:CGFloat = (toAngle-90).toRadians()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 4
let path = UIBezierPath(arcCenter: CGPoint(x: 0,y :0), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.position = center
circleLayer.lineWidth = radius*2
circleLayer.path = path.cgPath
}
public func animate() {
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 3.0;
pathAnimation.fromValue = 0.0;
pathAnimation.toValue = 1.0;
circleLayer.add(pathAnimation, forKey: "strokeEndAnimation")
}
}
So, now we can add it into our view controller and run the animation. In my case - I'm bridging it into Objecive-C but you can easily adapt it to swift.
I simply can't believe that in 2017 it was still not possible to find a ready solution for this simple task. It took me days to have that done. I really hope it will help others!
Here is how I'm using my class:
#implementation ViewController
{
SliceView *sv_;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.grayColor;
CGFloat width = 240.0;
CGFloat height = 160.0;
CGRect r = CGRectMake(
self.view.frame.size.width/2 - width/2,
self.view.frame.size.height/2 - height/2,
width, height);
sv_ = [[SliceView alloc] initWithFrame:r fromAngle:150 toAngle:30 color:[UIColor yellowColor] ];
[self.view addSubview:sv_];
}
- (IBAction)pressedGo:(id)sender {
[sv_ animate];
}
I'm adding a slight improvement for David's class. (David - you are welcome to copy into your book-quality answer!)
You can add the following init function:
convenience init(frame:CGRect, startAngle:CGFloat, endAngle:CGFloat, fillColor:UIColor,
strokeColor:UIColor, strokeWidth:CGFloat) {
self.init()
self.frame = frame
self.startAngle = startAngle
self.endAngle = endAngle
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
}
and then call it like this (Objective-C in my case):
PieSegmentLayer *sliceLayer = [[PieSegmentLayer alloc] initWithFrame:r startAngle:30 endAngle:180 fillColor:[UIColor cyanColor] strokeColor:[UIColor redColor] strokeWidth:4];
[self.view.layer addSublayer:sliceLayer];
I am attempting to use radial gradience within my app on a background UIView. My issue comes to play, where I want to update the view colors of the gradience multiple times. I have no errors with my code, but I can't seem to figure out how to get around this.
What I have tried is reloading the Input Views within the regular UIView as-well as the gradience class; remove the subview of the uiview, and adding a new view to the screen, which worked for only change of set colors; and I have looked over the internet, but can't seem to resolve this. All I want is for the UIView to update its colors based on the new color parameters I give it.
Here is my radial gradience code:
import UIKit
class RadialGradient: UIView {
var innerColor = UIColor.yellow
var outterColor = UIColor.red
override func draw(_ rect: CGRect) {
let colors = [innerColor.cgColor, outterColor.cgColor] as CFArray
let endRadius = min(frame.width, frame.height)
let center = CGPoint(x: bounds.size.width/2, y: bounds.size.height/2)
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
UIGraphicsGetCurrentContext()!.drawRadialGradient(gradient!,
startCenter: center,
startRadius: 0.0,
endCenter: center,
endRadius: endRadius,
options: CGGradientDrawingOptions.drawsAfterEndLocation)
}
}
Here is where I am using it:
import UIKit
class TestIssuesVC: UIViewController {
var check : Bool = false
#IBAction func buttonClicked(_ sender: Any) {
if check == true {
backgroundsetting.removeFromSuperview()
print("Why wont you change to purple and black?????")
cheapFix(inner: UIColor.purple, outter: UIColor.black)
} else {
backgroundsetting.removeFromSuperview()
cheapFix(inner: UIColor.red, outter: UIColor.blue)
check = true
}
}
func cheapFix(inner: UIColor, outter: UIColor) {
let backgroundsetting = RadialGradient()
backgroundsetting.innerColor = inner
backgroundsetting.outterColor = outter
backgroundsetting.frame = (frame: CGRect(x: self.view.frame.size.width * 0, y: self.view.frame.size.height * 0, width:self.view.frame.size.width, height: self.view.frame.size.height))
self.view.addSubview(backgroundsetting)
self.view.sendSubview(toBack: backgroundsetting)
self.reloadInputViews()
}
let backgroundsetting = RadialGradient()
override func viewWillAppear(_ animated: Bool) {
backgroundsetting.innerColor = UIColor.green
backgroundsetting.outterColor = UIColor.red
backgroundsetting.frame = (frame: CGRect(x: self.view.frame.size.width * 0, y: self.view.frame.size.height * 0, width:self.view.frame.size.width, height: self.view.frame.size.height))
self.view.addSubview(backgroundsetting)
self.view.sendSubview(toBack: backgroundsetting)
self.reloadInputViews()
}
}
I see two things.
Your cheapFix method never updates the backgroundsetting property. It creates its own local variable of the same name. So you are actually adding new views over and over but each is sent to the back so you only ever see the first one. This is why nothing ever appears to change.
None of that is necessary. Simply create one RadialGradient view. When you want its colors to change, simply update its colors. That class needs to be fixed so it redraws itself when its properties are updated.
Make the following change to the two properties in your RadialGradient class:
var innerColor = UIColor.yellow {
didSet {
setNeedsDisplay()
}
}
var outterColor = UIColor.red {
didSet {
setNeedsDisplay()
}
}
Those changes will ensure the view redraws itself when its colors are updated.
Currently I am tracking my location on an MKMapView. My objective is to draw a bezier path identical to an MKPolyline created from tracked locations.
What I have attempted is: Store all location coordinates in a CLLocation array. Iterate over that array and store the lat/lng coordinates in a CLLocationCoordinate2D array. Then ensure the polyline is in the view of the screen to then convert all the location coordinates in CGPoints.
Current attempt:
#IBOutlet weak var bezierPathView: UIView!
var locations = [CLLocation]() // values from didUpdateLocation(_:)
func createBezierPath() {
bezierPathView.isHidden = false
var coordinates = [CLLocationCoordinate2D]()
for location in locations {
coordinates.append(location.coordinate)
}
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
fitPolylineInView(polyline: polyline)
let mapPoints = polyline.points()
var points = [CGPoint]()
for point in 0...polyline.pointCount
{
let coordinate = MKCoordinateForMapPoint(mapPoints[point])
points.append(mapView.convert(coordinate, toPointTo: polylineView))
}
print(points)
let path = UIBezierPath(points: points)
path.lineWidth = 2.0
path.lineJoinStyle = .round
let layer = CAShapeLayer(path: path, lineColor: UIColor.red, fillColor: UIColor.black)
bezierPathView.layer.addSublayer(layer)
}
extension UIBezierPath {
convenience init(points:[CGPoint])
{
self.init()
//connect every points by line.
//the first point is start point
for (index,aPoint) in points.enumerated()
{
if index == 0 {
self.move(to: aPoint)
}
else {
self.addLine(to: aPoint)
}
}
}
}
extension CAShapeLayer
{
convenience init(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor)
{
self.init()
self.path = path.cgPath
self.strokeColor = lineColor.cgColor
self.fillColor = fillColor.cgColor
self.lineWidth = path.lineWidth
self.opacity = 1
self.frame = path.bounds
}
}
I am able to output the points to the console that stored from the convert(_:) method( not sure if they are correct ). Yet the there is not output on the bezierPathView-resulting in an empty-white background-view controller.
Your extensions work fine. The problem may be in the code that adds the layer to the view (which you do not show).
I'd suggest that you simplify your project, for example use predefined array of points that definitely fit to your view. For example, for a view that is 500 pixels wide and 300 pixels high, you could use something like:
let points = [
CGPoint(x: 10, y: 10),
CGPoint(x: 490, y: 10),
CGPoint(x: 490, y: 290),
CGPoint(x: 10, y: 290),
CGPoint(x: 10, y: 10)
]
Use colors that are clearly visible, like black and yellow for your stroke and fill.
Make sure that your path is correctly added to the view, for example:
let path = UIBezierPath(points: points)
let shapeLayer = CAShapeLayer(path: path, lineColor: UIColor.blue, fillColor: UIColor.lightGray)
view.layer.addSublayer(shapeLayer)
Inspect the controller that contains the view in Xcode's Interface Builder. In the debug view hierarchy function:
this might help you, in case you haven't solved it yet.
I wanted the shape of an MKPolyline as an image without any background.
I used the code above as an inspiration and had the same troubles as you had, the route was not shown.
In fact it was kind a scaling problem I think. At least it looked like that in the playground.
Anyway, with this methods I get an image of the polylines shape.
private func createPolylineShapeAsImage() -> UIImage? {
let vw = UIView(frame: mapView.bounds)
var image : UIImage?
if let polyline = viewModel.tourPolyline {
let path = createBezierPath(mapView, polyline, to: mapView)
let layer = getShapeLayer(path: path, lineColor: UIColor.white, fillColor: .clear)
vw.layer.addSublayer(layer)
image = vw.asImage()
}
return image
}
func createBezierPath(_ mapView : MKMapView, _ polyline : MKPolyline, to view : UIView) -> UIBezierPath {
let mapPoints = polyline.points()
var points = [CGPoint]()
let max = polyline.pointCount - 1
for point in 0...max {
let coordinate = mapPoints[point].coordinate
points.append(mapView.convert(coordinate, toPointTo: view))
}
let path = UIBezierPath(points: points)
path.lineWidth = 5.0
return path
}
private func getShapeLayer(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = lineColor.cgColor
layer.fillColor = fillColor.cgColor
layer.lineWidth = path.lineWidth
layer.opacity = 1
layer.frame = path.bounds
return layer
}
And to get the image of the view use this extension
import UIKit
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(self.frame.size)
self.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}
I want to create a circle with an inner circle that looks like the image below. I'm having trouble with the inner circle and I don't know how to create it so it's easy to adjust percentage (like the image is showing).
So far I have this CircleGraph class which can draw the ouster circle and an inner circle which can only draw 50 %.
import Foundation
import UIKit
class CircleGraph: UIView
{
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect)
{
super.drawRect(rect)
// Outer circle
Colors().getMainColor().setFill()
let outerPath = UIBezierPath(ovalInRect: rect)
outerPath.fill()
// inner circle so far
let percentage = 0.5
UIColor.whiteColor().setFill()
let circlePath = UIBezierPath(arcCenter: CGPoint(x: rect.height/2,y: rect.height/2), radius: CGFloat(rect.height/2), startAngle: CGFloat(-M_PI_2), endAngle:CGFloat(M_PI * 2 * percentage - M_PI_2), clockwise: true)
circlePath.fill()
}
}
Can anyone assist me?
What I want is something simliar to the image below:
I would go for the easy solution and create a UIView with a UIView and UILabel as subviews. If you use something like:
// To make it round
let width = self.frame.width
self.view.layer.cornerRadius = width * 0.5
self.view.layer.masksToBounds = true
for each of the sublayers. If you have set the background colour of the UIView's background layer to something like Red and the UIView layer above to have a whiteish background colour with alpha 0.5 than you already achieve this effect.
If you do not know how to proceed with this tip ill try to provide a code sample.
-- EDIT --
Here is the code sample:
import UIKit
class CircleView: UIView {
var percentage : Int?
var transparency : CGFloat?
var bottomLayerColor : UIColor?
var middleLayerColor : UIColor?
init(frame : CGRect, percentage : Int, transparency : CGFloat, bottomLayerColor : UIColor, middleLayerColor : UIColor) {
super.init(frame : frame)
self.percentage = percentage
self.transparency = transparency
self.bottomLayerColor = bottomLayerColor
self.middleLayerColor = middleLayerColor
viewDidLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
viewDidLoad()
}
func viewDidLoad() {
let width = self.frame.width
let height = self.frame.height
let textFrame = CGRectMake(0, 0, width, height)
guard let percentage = self.percentage
else {
print("Error")
return
}
let newHeight = (CGFloat(percentage)/100.0)*height
let middleFrame = CGRectMake(0,height - newHeight, width, newHeight)
// Set Background Color
if let bottomLayerColor = self.bottomLayerColor {
self.backgroundColor = bottomLayerColor
}
// Make Bottom Layer Round
self.layer.cornerRadius = width * 0.5
self.layer.masksToBounds = true
// Create Middle Layer
let middleLayer = UIView(frame: middleFrame)
if let middleLayerColor = self.middleLayerColor {
middleLayer.backgroundColor = middleLayerColor
}
if let transparency = self.transparency {
middleLayer.alpha = transparency
}
// The Label
let percentageLayer = UILabel(frame: textFrame)
percentageLayer.textAlignment = NSTextAlignment.Center
percentageLayer.textColor = UIColor.whiteColor()
if let percentage = self.percentage {
percentageLayer.text = "\(percentage)%"
}
// Add Subviews
self.addSubview(middleLayer)
self.addSubview(percentageLayer)
}
}
To use in a View Controller:
let redColor = UIColor.redColor()
let blueColor = UIColor.blueColor()
let frame = CGRectMake(50, 50, 100, 100)
// 50% Example
let circleView = CircleView(frame: frame, percentage: 50, transparency: 0.5, bottomLayerColor: redColor, middleLayerColor: blueColor)
self.view.addSubview(circleView)
// 33% Example
let newFrame = CGRectMake(50, 150, 120, 120)
let newCircleView = CircleView(frame: newFrame, percentage: 33, transparency: 0.7, bottomLayerColor: UIColor.redColor(), middleLayerColor: UIColor.whiteColor())
self.view.addSubview(newCircleView)
This will yield something like this: