CAShapeLayer offsetting from UIVIew's CALayer - uiview

I'm trying to add a CAShapeLayer to a custom UIViews layer, which is going well, except for the fact that it's offsetting itself...
It's probably best I explain with code and screenshots.. So here it goes:
All I'm trying to achieve is a simple "slash" like CAShapeLayer in Swift 3.
in the custom UIView I have:
var slashLayer: CAShapeLayer?
var slashPath: CGPath?
override func layoutSublayers(of layer: CALayer) {
if layer == self.layer {
if slashLayer == nil {
slashLayer = CAShapeLayer()
slashLayer?.bounds = layer.bounds
slashLayer?.strokeColor = UIColor.black.cgColor
slashLayer?.fillColor = UIColor.black.cgColor
slashLayer?.lineWidth = 3.0
let slashPath = UIBezierPath()
slashPath.move(to: CGPoint(x: layer.frame.minX, y: layer.frame.minY))
slashPath.addLine(to: CGPoint(x: layer.frame.maxX, y: layer.frame.maxY))
slashPath.close()
slashLayer?.path = slashPath.cgPath
layer.addSublayer(slashLayer!)
}
}
}
And that's it in the UIView subclass.
This is what I wind up with in Interface Builder (and the App when I fire it up)..
As CALayer.layoutManager isn't around any more, how do I keep the layers in line with one-another?

Related

How can I get the coordinates of a Label inside a view?

What I am trying to do is to get the position of my label (timerLabel) in order to pass those coordinates to UIBezierPath (so that the center of the shape and the center of the label coincide).
Here's my code so far, inside the viewDidLoad method, using Xcode 13.2.1:
// getting the center of the label
let center = CGPoint.init(x: timerLabel.frame.midX , y: timerLabel.frame.midY)
// drawing the shape
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
and this is what I have when I run my app:
link
What I don't understand is why I get (0,0) as coordinates even though I access the label's property (timerLabel.frame.midX).
The coordinates of your label may vary depending on current layout. You need to track all changes and reposition your circle when changes occur. In view controller that uses constraints you would override
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// recreate your circle here
}
this alone does not explain why your circle is so far out. First of all, looking at your image you do not get (0, 0) but some other value which may be relative position of your label within the blue bubble. The frame is always relative to its superview so you need to convert that into your own coordinate system:
let targetView = self.view!
let sourceView = timerLabel!
let centerOfSourceViewInTargetView: CGPoint = targetView.convert(CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.midY), to: targetView)
// Use centerOfSourceViewInTargetView as center
but I suggest using neither of the two. If you are using constraints (which you should) then rather create more views than adding layers to your existing views.
For instance you could try something like this:
#IBDesignable class CircleView: UIView {
#IBInspectable var lineWidth: CGFloat = 10 { didSet { refresh() } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { refresh() } }
override var frame: CGRect { didSet { refresh() } }
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRadius: CGFloat = min(bounds.width, bounds.height)*0.5
let strokeRadius: CGFloat = fillRadius - lineWidth*0.5
let path = UIBezierPath(ovalIn: .init(x: bounds.midX-strokeRadius, y: bounds.midY-strokeRadius, width: strokeRadius*2.0, height: strokeRadius*2.0))
path.lineWidth = lineWidth
strokeColor.setStroke()
UIColor.clear.setFill() // Probably not needed
path.stroke()
}
private func refresh() {
setNeedsDisplay() // This is to force redraw
}
}
this view should draw your circle within itself by overriding draw rect method. You can easily use it in your storyboard (first time it might not draw in storyboard because Xcode. Simply close your project and reopen it and you should see the circle even in storyboard).
Also in storyboard you can directly modify both line width and stroke color which is very convenient.
About the code:
Using #IBDesignable to see drawing in storyboard
Using #IBInspectable to be able to set values in storyboard
Refreshing on any value change to force redraw (sometimes needed)
When frame changes forcing a redraw (Needed when setting frame from code)
A method layoutSubviews is called when resized from constraints. Again redrawing.
Path is computed so that it fits within the size of view.

Using CAShapeLayer and removeFromSuperLayer

I am making an app, which should draw object from touch points.
I am using CAShapeLayerand UIBezierPath, but when I use removeFromSuperlayer() nothing happens.
I need to delete old shapes and draw just the new one.
I do not know why, but after one build and upload this app to my iPad, Xcode give me an error:
Cannot use optional chaining on non-optional value of type CAShapeLayer
Anyone who can help me?
And another question is:
How to draw just one line?
When I have 2 points, it draws nothing, the app draws just object after 3 points and more.
Here is a part of my code:
private func drawObj(){
let objectPath = UIBezierPath()
objectPath.move(to: CGPoint(x: pointsX[0], y: pointsY[0]))
let xx = pointsX.count - 1
print(xx)
for i in 1...xx {
objectPath.addLine(to: CGPoint(x: pointsX[i], y: pointsY[i]))
}
objectPath.close()
let object = CAShapeLayer()
object.removeFromSuperlayer()
object.path = objectPath.cgPath
object.fillColor = UIColor.red.cgColor
object.opacity = 0.2
self.view.layer.addSublayer(object)
}
If you create a new CAShapeLayer, there's no point in calling removeFromSuperlayer on that layer. You only do that for layers that have previously been added as sublayer of some other layer. So, don't call removeFromSuperlayer using this new layer that you just created, but save a reference to the previous one. That's the one that you should remove.
Or, as Dmitry pointed out, you should just add the shape layer once, and just update its path:
private var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.opacity = 0.2
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
return shapeLayer
}()
// add this to your view's layer in the logical place, e.g. `viewDidLoad`
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(shapeLayer)
}
// now your draw method only needs to update the path
private func drawObj() {
let objectPath = UIBezierPath()
objectPath.move(to: CGPoint(x: pointsX[0], y: pointsY[0]))
for i in 1 ..< pointsX.count {
objectPath.addLine(to: CGPoint(x: pointsX[i], y: pointsY[i]))
}
objectPath.close()
shapeLayer.path = object.cgPath
}
Note, I've also updated the lineWidth and the strokeColor of the shape layer so that if there are only two points and there is nothing to "fill", you can at least see the stroked path.
There is no need to remove the old CAShapeLayer from the parent layer and inserting a new one. Just create one CAShapeLayer, insert it to the parent layer. And then just update the 'path' parameter of it when you need to change it.

iOS: Animating a circle slice into a wider one

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];

Delete border of UIView when app changes size class

I have a blue view with a white dashed border. As you can see from the image below, once the app is rotated the view changes its dimensions and the border doesn't adjust to the view's new width and height.
I need to find a way to
Know when the view controller changes its size - perhaps using viewWillTransitionToSize.
Delete the previously drawn border, if any.
Add a new border to the view - in the view's drawRect method.
How can I delete the border previously drawn on the view?
#IBOutlet weak var myView: UIView!
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition(nil, completion: {
_ in
self.myView.setNeedsDisplay()
})
}
class RenderView: UIView {
override func drawRect(rect: CGRect) {
self.addDashedBorder()
}
}
extension UIView {
func addDashedBorder() {
let color = UIColor.whiteColor().CGColor
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
shapeLayer.bounds = shapeRect
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 6
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [6,3]
shapeLayer.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 5).CGPath
self.layer.addSublayer(shapeLayer)
}
}
Download sample project here.
This is not the right way to do things. Each time your RenderView is redrawn, its drawRect method will be called, and addDashedBorder will add a new layer to your view.
drawRect is for drawing inside of your view using CoreGraphics or UIKit, not CoreAnimation. Inside that method, you should just draw, nothing else. If you want to use it a layer, layoutSubviews is a better place to add it, and to update it to match the view.
Here are two ways to solve your problem. Both update the border correctly, and animate the border smoothly when the device is rotated.
Alternative 1: Just draw the border in drawRect, rather than using a separate shape layer. Also, set your view's contentMode so it automatically redraws when its size changes.
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.myView.contentMode = .Redraw
}
}
class RenderView: UIView {
override func drawRect(rect: CGRect) {
let path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 5)
path.lineWidth = 6
let pattern: [CGFloat] = [6.0, 3.0]
path.setLineDash(pattern, count: 2, phase: 0)
UIColor.whiteColor().setStroke()
path.stroke()
}
}
Alternative 2: Continue to use CAShapeLayer, but only create a single one, by using a lazy stored property. Update the layer in an override of layoutSubviews, and if necessary, animate it alongside any changes in the view's bounds.
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
}
class RenderView: UIView {
// Create the borderLayer, and add it to our view's layer, on demand, only once.
lazy var borderLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.lineWidth = 6
shapeLayer.lineDashPattern = [6,3]
self.layer.addSublayer(shapeLayer)
return shapeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
// We will update the borderLayer's path to match the view's current bounds.
let newPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 5).CGPath
// We may be animating from the old bounds to the new bounds.
// If so, we want the borderLayer to animate alongside that.
// (UIView does not do this automatically, since it does not know
// anything about our borderLayer; it's at the the CoreAnimation level,
// below UIKit.)
//
// We want an animation that uses the same properties as the existing
// animation, but applies to a different value: the borderLayer's path.
// We'll find the existing animation on the view's bounds.size,
// and if it exists, add our own animation based on it that will
// apply the path change.
if let viewBoundsAnimation = self.layer.animationForKey("bounds.size") {
let pathAnimation = CABasicAnimation()
pathAnimation.beginTime = viewBoundsAnimation.beginTime
pathAnimation.duration = viewBoundsAnimation.duration
pathAnimation.speed = viewBoundsAnimation.speed
pathAnimation.timeOffset = viewBoundsAnimation.timeOffset
pathAnimation.timingFunction = viewBoundsAnimation.timingFunction
pathAnimation.keyPath = "path"
pathAnimation.fromValue = borderLayer.path
pathAnimation.toValue = newPath
borderLayer.addAnimation(pathAnimation, forKey: "path")
}
// Finally, whether we are animating or not, make the border layer show the new path.
// If we are animating, this will appear when the animation is finished.
// If we are not animating, this will appear immediately.
self.borderLayer.path = newPath
}
}
Note that neither of these alternatives require overriding viewWillTransitionToSize or traitCollectionDidChange. Those are higher-level UIViewController concepts, that may get called during device rotation, but won't happen if some other code changes your view's size. It's better to use the simple UIView-level drawRect or layoutSubviews methods, because they will always work.
Call set needs display when the view transitions.You can detect it by using this function.It works perfectly to detect orientation change
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
self.myView.setNeedsDisplay()
}

How To Create in Swift a Circular Profile Picture or Rounded Corner Image with a border which does not leak?

Basing on the source code below:
#IBOutlet var myUIImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
self.makingRoundedImageProfileWithRoundedBorder()
}
private func makingRoundedImageProfileWithRoundedBorder() {
// Making a circular image profile.
// self.myUIImageView.layer.cornerRadius = self.myUIImageView.frame.size.width / 2
// Making a rounded image profile.
self.myUIImageView.layer.cornerRadius = 20.0
self.myUIImageView.clipsToBounds = true
// Adding a border to the image profile
self.myUIImageView.layer.borderWidth = 10.0
self.myUIImageView.layer.borderColor = UIColor.whiteColor().CGColor
}
Indeed I am able to render a circular or rounded UIImageView, but the problem is that if we add the border, the image leaks a bit. It's way worse with a circular UIImageView, it leaks whenever the border is bent, so LEAKS EVERYWHERE! You can find a screenshot of the result below:
Any way to fix that in Swift? Any sample code which answers to this question will be highly appreciated.
Note: as far as possible the solution has to be compatible with iOS 7 and 8+.
First Solution
Basing on the #Jesper Schläger suggestion
"If I may suggest a quick and dirty solution:
Instead of adding a border to the image view, you could just add another white view below the image view. Make the view extend 10 points in either direction and give it a corner radius of 20.0. Give the image view a corner radius of 10.0."
Please find the Swift implementation below:
import UIKit
class ViewController: UIViewController {
#IBOutlet var myUIImageView: UIImageView!
#IBOutlet var myUIViewBackground: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Making a circular UIView: cornerRadius = self.myUIImageView.frame.size.width / 2
// Making a rounded UIView: cornerRadius = 10.0
self.roundingUIView(self.myUIImageView, cornerRadiusParam: 10)
self.roundingUIView(self.myUIViewBackground, cornerRadiusParam: 20)
}
private func roundingUIView(let aView: UIView!, let cornerRadiusParam: CGFloat!) {
aView.clipsToBounds = true
aView.layer.cornerRadius = cornerRadiusParam
}
}
Second Solution
Would be to set a circle mask over a CALayer.
Please find the Objective-C implementation of this second solution below:
CALayer *maskedLayer = [CALayer layer];
[maskedLayer setFrame:CGRectMake(50, 50, 100, 100)];
[maskedLayer setBackgroundColor:[UIColor blackColor].CGColor];
UIBezierPath *maskingPath = [UIBezierPath bezierPath];
[maskingPath addArcWithCenter:maskedLayer.position
radius:40
startAngle:0
endAngle:360
clockwise:TRUE];
CAShapeLayer *maskingLayer = [CAShapeLayer layer];
[maskingLayer setPath:maskingPath.CGPath];
[maskedLayer setMask:maskingLayer];
[self.view.layer addSublayer:maskedLayer];
If you comment out from line UIBezierPath *maskingPath = [UIBezierPath bezierPath]; through [maskedLayer setMask:maskingLayer]; you will see that the layer is a square. However when these lines are not commented the layer is a circle.
Note: I neither tested this second solution nor provided the Swift implementation, so feel free to test it and let me know if it works or not through the comment section below. Also feel free to edit this post adding the Swift implementation of this second solution.
If I may suggest a quick and dirty solution:
Instead of adding a border to the image view, you could just add another white view below the image view. Make the view extend 10 points in either direction and give it a corner radius of 20.0. Give the image view a corner radius of 10.0.
I worked on improving the code but it keeps crashing. I'll work on it, but I appear to have got a (rough) version working:
Edit Updated with a slightly nicer version. I don't like the init:coder method but maybe that can factored out/improved
class RoundedImageView: UIView {
var image: UIImage? {
didSet {
if let image = image {
self.frame = CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
}
}
var cornerRadius: CGFloat?
private class func frameForImage(image: UIImage) -> CGRect {
return CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
override func drawRect(rect: CGRect) {
if let image = self.image {
image.drawInRect(rect)
let cornerRadius = self.cornerRadius ?? rect.size.width/10
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
UIColor.whiteColor().setStroke()
path.lineWidth = cornerRadius
path.stroke()
}
}
}
let image = UIImage(named: "big-teddy-bear.jpg")
let imageView = RoundedImageView()
imageView.image = image
Let me know if that's the sort of thing you're looking for.
A little explanation:
As I'm sure you've found, the "border" that iOS can apply isn't perfect, and shows the corners for some reason. I found a few other solutions but none seemed to work. The reason this is a subclass of UIView, and not UIImageView, is that drawRect: is not called for subclasses of UIImageView. I am not sure about the performance of this code, but it seems good from my (limited) testing
Original code:
class RoundedImageView: UIView {
var image: UIImage? {
didSet {
if let image = image {
self.frame = CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
}
}
private class func frameForImage(image: UIImage) -> CGRect {
return CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
override func drawRect(rect: CGRect) {
if let image = self.image {
self.image?.drawInRect(rect)
let path = UIBezierPath(roundedRect: rect, cornerRadius: 50)
UIColor.whiteColor().setStroke()
path.lineWidth = 10
path.stroke()
}
}
}
let image = UIImage(named: "big-teddy-bear.jpg")
let imageView = RoundedImageView()
imageView.image = image
imageView.layer.cornerRadius = 50
imageView.clipsToBounds = true

Resources