I have a subclass of UIView connected to a XIB file of a UIViewController.
I'm using PocketSVG to convert my SVG file to CGPath like that:
override func awakeFromNib() {
let myPath = PocketSVG.pathFromSVGFileNamed("").takeUnretainedValue()
let myShapeLayer = CAShapeLayer()
myShapeLayer.path = myPath
myShapeLayer.strokeColor = UIColor.redColor().CGColor
myShapeLayer.lineWidth = 3
myShapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(myShapeLayer)
}
the problem is that when I run the app I can't see anything, the layer isn't visible.
What am I doing wrong now?
Thank you!
Given what you reported for CGPathGetPathBoundingBox, you may want to transform the path so it falls within the visible portion of the window.
For example, if you want to translate and scale this so it fits the view:
let insetRect = CGRectInset(bounds, lineWidth / 2.0, lineWidth / 2.0)
let boundingRect = CGPathGetPathBoundingBox(myPath)
let scale = min(insetRect.size.height / boundingRect.size.height, insetRect.size.width / boundingRect.size.width)
var transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(-boundingRect.origin.x * scale + insetRect.origin.x, -boundingRect.origin.y * scale + insetRect.origin.y), scale, scale)
let transformedPath = CGPathCreateMutableCopyByTransformingPath(myPath, &transform)
There are lots of permutations on this idea, but hopefully it illustrates the concept.
Related
I've been trying to solve this for days, and none of the suggestions solved my problem. I have a UIView inside a UITableView. I've been trying to draw my CAShapeLayer inside my UIView, but I just can't get it right.
I need my yellow CAShapeLayer to be exactly center within the gray UIView.
func DrawActivityRect(cell: CalorieDashboardTVCell){
let rectangle = UIBezierPath(rect: cell.calorieBurnUIView.bounds)
let trackLayer = CAShapeLayer()
trackLayer.frame = cell.calorieBurnUIView.bounds
trackLayer.path = rectangle.cgPath
trackLayer.position = cell.calorieBurnUIView.center
trackLayer.strokeColor = UIColor.yellow.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
cell.calorieBurnUIView.layer.addSublayer(trackLayer)
}
Result:
I suspect the cell size change after you have set the layer. The easiest place to fix this is in the cell's layoutSubviews(). Since your path is using absolute coordinates you need to update both the layer frame and the path.
override func layoutSubviews() {
super.layoutSubviews()
// You need to keep trackLayer around after you created it
let rectangle = UIBezierPath(rect: cell.calorieBurnUIView.bounds)
self.trackLayer.frame = cell.calorieBurnUIView.bounds
self.trackLayer.path = rectangle.cgPath
self.trackLayer.position = cell.calorieBurnUIView.center
}
https://www.dropbox.com/s/wlizis5zybsvnfz/File%202017-04-04%2C%201%2052%2024%20PM.jpeg?dl=0
Hello all Swifters,
Could anyone tell me how to set this kind of UI? Is there any half rounded image that they have set?
Or there are two images. One with the mountains in the background and anohter image made half rounded in white background and placed in on top?
Please advise
Draw an ellipse shape using UIBezier path.
Draw a rectangle path exactly similar to imageView which holds your image.
Transform the ellipse path with CGAffineTransform so that it will be in the center of the rect path.
Translate rect path with CGAffineTransform by 0.5 to create intersection between ellipse and the rect.
Mask the image using CAShapeLayer.
Additional: As Rob Mayoff stated in comments you'll probably need to calculate the mask size in viewDidLayoutSubviews. Don't forget to play with it, test different cases (different screen sizes, orientations) and adjust the implementation based on your needs.
Try the following code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
guard let image = imageView.image else {
return
}
let size = image.size
imageView.clipsToBounds = true
imageView.image = image
let curveRadius = size.width * 0.010 // Adjust curve of the image view here
let invertedRadius = 1.0 / curveRadius
let rect = CGRect(x: 0,
y: -40,
width: imageView.bounds.width + size.width * 2 * invertedRadius,
height: imageView.bounds.height)
let ellipsePath = UIBezierPath(ovalIn: rect)
let transform = CGAffineTransform(translationX: -size.width * invertedRadius, y: 0)
ellipsePath.apply(transform)
let rectanglePath = UIBezierPath(rect: imageView.bounds)
rectanglePath.apply(CGAffineTransform(translationX: 0, y: -size.height * 0.5))
ellipsePath.append(rectanglePath)
let maskShapeLayer = CAShapeLayer()
maskShapeLayer.frame = imageView.bounds
maskShapeLayer.path = ellipsePath.cgPath
imageView.layer.mask = maskShapeLayer
}
}
Result:
You can find an answer here:
https://stackoverflow.com/a/34983655/5479510
But generally, I wouldn't recommend using a white image overlay as it may appear distorted or pixelated on different devices. Using a masking UIView would do just great.
Why you could not just create (draw) rounded transparent image and add UIImageView() with UIImage() at the top of the view with required height and below this view add other views. I this this is the easiest way. I would write comment but I cant.
I must admit I have no clue how to do this in iOS -
Here's some code that makes a nice dotted line:
Now, I want that line to "run" upwards:
So, every one second it will move upwards by, itemLength * 2.0.
Of course, it would wrap around top to bottom.
So, DottedVertical should just do this completely on its own.
Really, how do you do this in iOS?
It would be great if the solution is general and will "scroll" any I suppose layer or drawn thing.
In say a game engine it's trivial, you just animate the offset of the texture. Can you perhaps offset the layer, or something, in iOS?
What's the best way?
I guess you'd want to use the GPU (layer animation right?) to avoid melting the cpu.
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = UIColor.faveColor
override func draw(_ rect: CGRect) {
// say you want 8 dots, with perfect fenceposting:
let totalCount = 8 + 8 - 1
let fullHeight = bounds.size.height
let width = bounds.size.width
let itemLength = fullHeight / CGFloat(totalCount)
let beginFromTop = !lowerHalfOnly ? 0.0 : (fullHeight * 8.0 / 15.0)
let top = CGPoint(x: width/2, y: beginFromTop)
let bottom = CGPoint(x: width/2, y: fullHeight)
let path = UIBezierPath()
path.move(to: top)
path.addLine(to: bottom)
path.lineWidth = width
let dashes: [CGFloat] = [itemLength, itemLength]
path.setLineDash(dashes, count: dashes.count, phase: 0)
dotColor.setStroke()
path.stroke()
}
(Bonus - if you had a few of these on screen, they'd have to be synced of course. There'd need to be a "sync" call that starts the running animation, so you can start them all at once with a notification or other message.)
Hate to answer my own question, here's a copy and paste solution based on the Men's suggestions above!
Good one! Superb effect...
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = sfBlack6 { didSet {setup()} }
override func layoutSubviews() { setup() }
var s:CAShapeLayer? = nil
func setup() {
// say you want 8 dots, with perfect fenceposting:
- calculate exactly as in the example in the question above -
// marching ants...
if (s == nil) {
s = CAShapeLayer()
self.layer.addSublayer(s!)
}
s!.strokeColor = dotColor.cgColor
s!.fillColor = backgroundColor?.cgColor
s!.lineWidth = width
let ns = NSNumber(value: Double(itemLength))
s!.lineDashPattern = [ns, ns]
let path = CGMutablePath()
path.addLines(between: [top, bottom])
s!.path = path
let anim = CABasicAnimation(keyPath: "lineDashPhase")
anim.fromValue = 0
anim.toValue = ns + ns
anim.duration = 1.75 // seconds
anim.repeatCount = Float.greatestFiniteMagnitude
s!.add(anim, forKey: nil)
self.layer.addSublayer(s!)
}
}
I have a custom path drawn using CAShapeLayer in a UIView. Its a semicircle, upper half, drawn from left to right.
Following is the code used, writted in a UIView subclass:
func drawSemicircle() {
// drawing an upper half of a circle -> 180 degree to 0 degree, clockwise
let startAngle = CGFloat(M_PI)
let endAngle = CGFloat(0.0)
let centerPoint = CGPointMake(CGRectGetWidth(frame)/2 , CGRectGetHeight(frame))
// path set here
let semirCircleLayer = CAShapeLayer()
let semirCirclePath = UIBezierPath(arcCenter:centerPoint, radius: CGRectGetWidth(frame)/2 - 20.0 , startAngle:startAngle, endAngle:endAngle, clockwise: true)
semirCircleLayer.path = semirCirclePath.CGPath
// layer customisation
semirCircleLayer.fillColor = UIColor.clearColor().CGColor
semirCircleLayer.strokeColor = UIColor(red: 237.0/255.0, green: 236.0/255.0, blue: 236.0/255.0, alpha: 1.0).CGColor
semirCircleLayer.lineWidth = 20.0
semirCircleLayer.lineCap = kCALineCapButt
layer.addSublayer(semirCircleLayer)
}
Current path looks like this:
What I am tryign to implement is to mark some points on the bar .Like a analog clock graduation + diigts, but not so complex. Just mark 1 point at any progress level. So a final output, something like this :
Can I get some help on this?
You'll want to use another CAShapeLayer for the graduation line and a CATextLayer for the text at the end of the graduation.
Instead of doing Core Graphics drawing as my answer here does, I recommend you use a 100% layer approach as to best fit in with your existing code.
Something like this should do the trick:
func drawSemicircle() {
// drawing an upper half of a circle -> 180 degree to 0 degree, clockwise
let startAngle = CGFloat(M_PI)
let endAngle = CGFloat(0.0)
let centerPoint = CGPoint(x: CGRectGetWidth(frame)*0.5 , y: CGRectGetHeight(frame))
let radius = frame.size.width*0.5 - 80.0 // radius of your arc
// path set here
let semiCircleLayer = CAShapeLayer()
semiCircleLayer.frame = bounds // requried for layer calculations
let semiCirclePath = UIBezierPath(arcCenter:centerPoint, radius:radius, startAngle:startAngle, endAngle:endAngle, clockwise: true)
semiCircleLayer.path = semiCirclePath.CGPath
// layer customisation
semiCircleLayer.fillColor = UIColor.clearColor().CGColor
semiCircleLayer.strokeColor = UIColor(red: 237.0/255.0, green: 236.0/255.0, blue: 236.0/255.0, alpha: 1.0).CGColor
semiCircleLayer.lineWidth = 20.0
semiCircleLayer.lineCap = kCALineCapButt
layer.addSublayer(semiCircleLayer)
// draw graduation (cue the wall of code!)
let graduationLayer = CAShapeLayer() // the graduation layer that'll display the graduation
graduationLayer.frame = semiCircleLayer.bounds
let graduationWidth = CGFloat(4.0) // the width of the graduation
let graduationLength = CGFloat(50.0) // the length of the graduation
let graduationColor = UIColor.redColor() // the color of both the graduation line and text
let startGradRad = radius-semiCircleLayer.lineWidth*0.5 // the starting radius of the graduation
let endGradRad = startGradRad+graduationLength // the ending radius of the graduation
let graduationAngle = CGFloat(M_PI*0.79) // 21% along the arc from the left (0 degrees coresponds to the right hand side of the circle, with the positive angle direction going anti-clocwise (much like a unit circle in maths), so we define 79% along the arc, from the right hand side)
// the starting point of the graduation line. the angles are negative as the arc is effectively drawn upside-down in the UIKit coordinate system.
let startGradPoint = CGPoint(x: cos(-graduationAngle)*startGradRad+centerPoint.x, y: sin(-graduationAngle)*startGradRad+centerPoint.y)
let endGradPoint = CGPoint(x: cos(-graduationAngle)*endGradRad+centerPoint.x, y: sin(-graduationAngle)*endGradRad+centerPoint.y)
// the path for the graduation line
let graduationPath = UIBezierPath()
graduationPath.moveToPoint(startGradPoint) // start point
graduationPath.addLineToPoint(endGradPoint) // end point
graduationLayer.path = graduationPath.CGPath // add path to the graduation shape layer
// configure stroking options
graduationLayer.fillColor = UIColor.clearColor().CGColor
graduationLayer.strokeColor = graduationColor.CGColor
graduationLayer.lineWidth = graduationWidth
// add to semi-circle layer
semiCircleLayer.addSublayer(graduationLayer)
// the font of the text to render at the end of the graduation
let textFont = UIFont.systemFontOfSize(30)
// the text to render at the end of the graduation - do you custom value logic here
let str : NSString = "value"
// default paragraph style
let paragraphStyle = NSParagraphStyle()
// the text attributes dictionary. used to obtain a size of the drawn text in order to calculate its frame
let textAttributes = [NSParagraphStyleAttributeName:paragraphStyle, NSFontAttributeName:textFont]
// size of the rendered text
let textSize = str.sizeWithAttributes(textAttributes)
let xOffset = abs(cos(graduationAngle))*textSize.width*0.5 // the x-offset of the text from the end of the graduation line
let yOffset = abs(sin(graduationAngle))*textSize.height*0.5 // the y-offset of the text from the end of the graduation line
/// the padding between the graduation line and the text
let graduationTextPadding = CGFloat(5.0)
// bit of pythagorus to determine how far away the center of the text lies from the end of the graduation line. multiplying the values together is cheaper than using pow. the text padding is added onto it.
let textOffset = sqrt(xOffset*xOffset+yOffset*yOffset)+graduationTextPadding
// the center of the text to render
let textCenter = CGPoint(x: cos(-graduationAngle)*textOffset+endGradPoint.x, y: sin(-graduationAngle)*textOffset+endGradPoint.y)
// the frame of the text to render
let textRect = CGRect(x: textCenter.x-textSize.width*0.5, y: textCenter.y-textSize.height*0.5, width: textSize.width, height: textSize.height)
let textLayer = CATextLayer()
textLayer.contentsScale = UIScreen.mainScreen().scale // to ensure the text is rendered at the screen scale
textLayer.frame = textRect
textLayer.string = str
textLayer.font = textFont
textLayer.fontSize = textFont.pointSize // required as CATextLayer ignores the font size of the font you pass
textLayer.foregroundColor = graduationColor.CGColor // color of text
graduationLayer.addSublayer(textLayer)
}
Output:
There's quite a lot of code here, as you have to do some extra logic in order to calculate the size of the CATextLayer. You could simplify this by using a UILabel as you can use sizeToFit in order to calculate the size, however this may complicate the layer hierarchy.
I've tried my best to explain each line of code - but if you still have questions, I'll be happy to answer them!
Furthermore, if you re-structure the code, you could easily allow for the graduation angle, as well as the other variables to be changed from outside the class. I've provided an example of this in the full project below.
Full Project: https://github.com/hamishknight/Dial-View
I have two UIBezierPath...
First path shows the total path from to and fro destination and the second path is a copy of the first path but that copy should be a percentage of the first path which I am unable to do.
Basically I would like the plane to stop at the part where green UIBezier path ends and not go until the past green color.
I am attaching a video in hte link that will show the animation I am trying to get. http://cl.ly/302I3O2f0S3Y
Also a similar question asked is Move CALayer via UIBezierPath
Here is the relevant code
override func viewDidLoad() {
super.viewDidLoad()
let endPoint = CGPointMake(fullFlightLine.frame.origin.x + fullFlightLine.frame.size.width, fullFlightLine.frame.origin.y + 100)
self.layer = CALayer()
self.layer.contents = UIImage(named: "Plane")?.CGImage
self.layer.frame = CGRectMake(fullFlightLine.frame.origin.x - 10, fullFlightLine.frame.origin.y + 10, 120, 120)
self.path = UIBezierPath()
self.path.moveToPoint(CGPointMake(fullFlightLine.frame.origin.x, fullFlightLine.frame.origin.y + fullFlightLine.frame.size.height/4))
self.path.addQuadCurveToPoint(endPoint, controlPoint:CGPointMake(self.view.bounds.size.width/2, -200))
self.animationPath = self.path.copy() as! UIBezierPath
let w = (viewModel?.completePercentage) as CGFloat!
// let animationRectangle = UIBezierPath(rect: CGRectMake(fullFlightLine.frame.origin.x-20, fullFlightLine.frame.origin.y-270, fullFlightLine.frame.size.width*w, fullFlightLine.frame.size.height-20))
// let currentContext = UIGraphicsGetCurrentContext()
// CGContextSaveGState(currentContext)
// self.animationPath.appendPath(animationRectangle)
// self.animationPath.addClip()
// CGContextRestoreGState(currentContext)
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = path.CGPath
self.shapeLayer.strokeColor = UIColor.redColor().CGColor
self.shapeLayer.fillColor = UIColor.clearColor().CGColor
self.shapeLayer.lineWidth = 10
self.animationLayer = CAShapeLayer()
self.animationLayer.path = animationPath.CGPath
self.animationLayer.strokeColor = UIColor.greenColor().CGColor
self.animationLayer.strokeEnd = w
self.animationLayer.fillColor = UIColor.clearColor().CGColor
self.animationLayer.lineWidth = 3
fullFlightLine.layer.addSublayer(shapeLayer)
fullFlightLine.layer.addSublayer(animationLayer)
fullFlightLine.layer.addSublayer(self.layer)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
updateAnimationForPath(self.animationLayer)
}
func updateAnimationForPath(pathLayer : CAShapeLayer) {
let animation : CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "position")
animation.path = pathLayer.path
animation.calculationMode = kCAAnimationPaced
animation.delegate = self
animation.duration = 3.0
self.layer.addAnimation(animation, forKey: "bezierPathAnimation")
}
}
extension Int {
var degreesToRadians : CGFloat {
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
Your animation for traveling a path is exactly right. The only problem now is that you are setting the key path animation's path to the whole bezier path:
animation.path = pathLayer.path
If you want the animation to cover only part of that path, you have two choices:
Supply a shorter version of the bezier path, instead of pathLayer.path.
Alternatively, wrap the whole animation in a CAAnimationGroup with a shorter duration. For example, if your three-second animation is wrapped inside a two-second animation group, it will stop two-thirds of the way through.