iOS - Custom UIView doesn't show up on device - ios

I have a custom UIView subclass, that I want to add as a subview on my UIViewController. The problem is, that the view doesn't show up, when I run the app, even though everything is set correctly (in viewDidLoad the view has a correct frame and is not hidden) and it shows up in Interface Builder (picture). On the device I only get a red screen, no triangle in the middle.
Here's the view subclass:
#IBDesignable
class TriangleView: UIView {
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let path = UIBezierPath()
let width = rect.size.width
let height = rect.size.height
let startingPoint = CGPoint(x: center.x, y: center.y - height / 2)
path.moveToPoint(startingPoint)
path.addLineToPoint(CGPoint(x: center.x + width / 2, y: center.y - height / 2))
path.addLineToPoint(CGPoint(x: center.x, y: center.y + height / 2))
path.addLineToPoint(CGPoint(x: center.x - width / 2, y: center.y - height / 2))
path.closePath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = rect
shapeLayer.position = center
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.whiteColor().CGColor
layer.mask = shapeLayer
layer.backgroundColor = UIColor.redColor().CGColor
}
}
I don't have any other code to show, I just add the view to the ViewController and set its constraints and class to TriangleView.

Swift 3
I worked on simplifying what you have. I know this is old but here is something that works which I believe achieves what you were trying to do:
#IBDesignable
class TriangleView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let width = rect.size.width
let height = rect.size.height
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: width, y: 0))
path.addLine(to: CGPoint(x: width/2, y: height))
path.close()
path.stroke()
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
layer.backgroundColor = UIColor.red.cgColor
}
}
A few differences:
I only use 3 points instead of 4 for the triangle then close it.
The first 2 points are in the corner of the UIView.
I added layer.addSublayer(shapeLayer). I believe this is why it wasn't showing up when running the app.
Removed some code that I don't think was needed but you can add it back if you believe so.

Have you tried,
[self.view bringSubviewToFront:TriangleView]
Put this after adding your TriangleView as a subview of your view controller.

Related

How to draw a UIView or a UIButton using UIBezierPath

I want to draw a UIView or a UIButton like this one which is show in the image.
How it is possible to draw in swift?
I have done a lot of R $ D on this what I get is UIBezierPath.
So, How to draw using UIBezierPath. I am unable to draw exactly this.
Please help me to draw like this.
I want create a separate class for this so that I can inherit any where.
Thank you in advance.
I have tried something like this.
extension UIButton {
func roundCorners(corners: UIRectCorner, radius: Int = 8) {
let maskPath1 = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
Result is
But I want a sharp cut.Like first image
To draw that shape, you want to use maskPath1.move(to: pt) and then a series of maskPath1.addLine(to: pt).
You'll need six points:
so you could code it like this:
extension UIButton {
func angledCorners() {
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
let pt1: CGPoint = CGPoint(x: bounds.minX, y: bounds.maxY)
let pt2: CGPoint = CGPoint(x: bounds.minX, y: bounds.midY)
let pt3: CGPoint = CGPoint(x: bounds.minX + bounds.midY, y: bounds.minY)
let pt4: CGPoint = CGPoint(x: bounds.maxX, y: bounds.minY)
let pt5: CGPoint = CGPoint(x: bounds.maxX, y: bounds.midY)
let pt6: CGPoint = CGPoint(x: bounds.maxX - bounds.midY, y: bounds.maxY)
let maskPath1: UIBezierPath = UIBezierPath()
maskPath1.move(to: pt1)
maskPath1.addLine(to: pt2)
maskPath1.addLine(to: pt3)
maskPath1.addLine(to: pt4)
maskPath1.addLine(to: pt5)
maskPath1.addLine(to: pt6)
maskPath1.close()
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
A couple problems with that approach though... the mask will not "auto-resize" when the button frame changes. So, you have to wait to call button.angledCorners() until auto-layout has finished setting the button's frame, and you have to call it again any time the frame changes.
If you have explicit frame sizes, that's not a problem.
But if you have buttons that change based on other views, or perhaps change size when the device is rotated, it's a hassle... and you probably want to make this a feature of a custom subclass.

How to add a custom shape to an UIImageView in Swift?

I'm trying to add a custom shape to an imageView. Please check the below images.
This is the required one:
This is what I have done so far:
I'm new to Core Graphics and I have done this so far:
private func customImageClipper(imageV: UIImageView){
let path = UIBezierPath()
let size = imageV.frame.size
print(size)
path.move(to: CGPoint(x: 0.0, y: size.height))
path.addLine(to: CGPoint(x: 0.8, y: size.height/2))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
imageV.layer.sublayers = [shape]
}
I'm creating a function to achieve a shape like this, but whenever I pass the imageView into this function, I can not see any change at all. I know that I have to move from points to another point to achieve this shape, but I have never done this. Any help would be appreciated. This is how I'm calling this function:
imageV.layoutIfNeeded()
customImageClipper(imageV: imageV)
P.S.: I'm not using Storyboard, I have created this programmatically.
There are many ways to create shapes using UIBezierPaths. This post here discusses the use of the draw function to create a shape.
Here is an example using your clip function within the cell.
func clip(imageView: UIView, withOffset offset: CGFloat) {
let path = UIBezierPath()
//Move to Top Left
path.move(to: .init(x: imageView.bounds.size.width * offset, y: 0))
//Draw line from Top Left to Top Right
path.addLine(to: .init(x: imageView.bounds.size.width, y: 0))
//Draw Line from Top Right to Bottom Right
path.addLine(to: .init(x: imageView.bounds.size.width * (1 - offset), y: imageView.bounds.size.height))
//Draw Line from Bottom Right to Bottom Left
path.addLine(to: .init(x: 0, y: imageView.bounds.size.height))
//Close Path
path.close()
//Create the Shape Mask for the ImageView
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.black.cgColor
imageView.layer.mask = shapeLayer
}
In this function, the offset is the amount of angle you would like on the shape, ranging from 0 to 1. (0.4) seems to work for your requirements.
This shares a lot of similarities with Apseri's answer, except I chose the route of percentages, rather than exact size. Nothing wrong with either approach, I just found it easier to understand with percentages. :)
One last note to point out, I used this function in the layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
imageView.layoutIfNeeded()
clip(imageView: self.imageView, withOffset: 0.4)
}
This output the following image:
Hope this helps.
Here is example of some path clipping. Of course path can be also put via parameters, and this can be applied to any view, as shown.
Before:
After (grey background is below ScrollView background):
func customImageClipper(imageV: UIView){
let path = UIBezierPath()
let size = imageV.frame.size
path.move(to: CGPoint(x: size.width/3.0, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0 + 50, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0 - 50, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0, y: 0))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
imageV.layer.mask = shape
}
1- Subclassing your UIImageView
2- implement your custom drawings inside setNeedsLayout using UIBezierPath
class MyCustomImageView: UIImageView {
override func setNeedsLayout() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height/2))
path.addLineToPoint(CGPoint(x: self.frame.size.width/2, y: 0))
path.addArcWithCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: self.frame.size.width/2, startAngle:-CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2), clockwise: false)
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.closePath()
UIColor.redColor().setFill()
path.stroke()
path.bezierPathByReversingPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
self.layer.mask = shapeLayer;
self.layer.masksToBounds = true;
}
}

UIBezierPath rounded corners in UIButton

I want to draw a tappable bubble shape around text. In order to do that I decided to add a shapelayer to UIButton like this:
// Button also has this
bubbleButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
bubbleButton.setTitle("Gutp", for: .normal)
// In my subclass of UIButton
override func layoutSubviews() {
super.layoutSubviews()
self.bubbleLayer?.removeFromSuperlayer()
let maskLayer = CAShapeLayer.init()
let bezierPath = UIBezierPath.init(roundedRect: self.bounds,
byRoundingCorners: [.topRight, .topLeft, .bottomLeft],
cornerRadii: CGSize(width: 20, height: 20))
maskLayer.path = bezierPath.cgPath
maskLayer.strokeColor = UIColor.red.cgColor
maskLayer.lineWidth = 2
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.isOpaque = false
self.bubbleLayer = maskLayer
if let layers = self.layer.sublayers {
self.layer.insertSublayer(self.bubbleLayer!, at: UInt32(layers.count))
} else {
self.layer.addSublayer(self.bubbleLayer!)
}
}
Please don't look at the performance at the moment.
I have 2 buttons like this added into a UIStackView.
I get an interesting artefact (a tail if one can say so) in some cases of text (usually when it is short) and a normal bubble in case of longer text:
How can I fix this? And why do I get such behavior?
EDIT: Linking other possibly related questions about broken cornerRadii parameter in bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:. Maybe it will help someone with similar issues.
Why is cornerRadii parameter of CGSize type in -[UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:]?
Crazy rounded rect UIBezierPath behavior on iOS 7. What is the deal?
For using these rounded rect methods you must ensure that the view size is larger then the radius used. In your case both width and height must be larger then 40 (Radii size is 20x20 so max(20*2, 20*2) = 40).
In general I prefer having a custom method to generate such paths. Using lines and arcs usually give you better flexibility. You may try the following:
/// Returns a path with rounded corners
///
/// - Parameters:
/// - frame: A frame at which the path is drawn. To fit in view "bounds" should be used
/// - maximumRadius: A maximum corner radius used. For smaller views radius will be min(width/2, height/2)
/// - Returns: Returns a new path
func roundedRectPath(inRect frame: CGRect, radiusConstrainedTo maximumRadius: CGFloat) -> UIBezierPath {
let radisu = min(maximumRadius, min(frame.size.width*0.5, frame.size.height*0.5))
let path = UIBezierPath()
path.move(to: CGPoint(x: frame.origin.x + radisu, y: frame.origin.y)) // Top left
path.addLine(to: CGPoint(x: frame.origin.x + frame.size.width - radisu, y: frame.origin.y)) // Top right
path.addQuadCurve(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height - radisu), controlPoint: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y)) // Top right arc
path.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height - radisu)) // Bottom right
path.addQuadCurve(to: CGPoint(x: frame.origin.x + frame.size.width - radisu, y: frame.origin.y + frame.size.height), controlPoint: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height)) // Bottom right arc
path.addLine(to: CGPoint(x: frame.origin.x + radisu, y: frame.origin.y + frame.size.height)) // Bottom left
path.addQuadCurve(to: CGPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height - radisu), controlPoint: CGPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height)) // Bottom left arc
path.addLine(to: CGPoint(x: frame.origin.x, y: frame.origin.y + radisu)) // Top left
path.addQuadCurve(to: CGPoint(x: frame.origin.x + radisu, y: frame.origin.y), controlPoint: CGPoint(x: frame.origin.x, y: frame.origin.y)) // Top left arc
path.close()
return path
}
When using this with stroke you need to also inset the frame by half of the line width. This is a snippet from "draw rect" procedure but can be applied anywhere:
UIColor.red.setStroke()
let lineWidth: CGFloat = 5.0
let path = roundedRectPath(inRect: bounds.insetBy(dx: lineWidth*0.5, dy: lineWidth*0.5), radiusConstrainedTo: 30.0)
path.lineWidth = lineWidth
path.stroke()
Notice the bounds.insetBy(dx: lineWidth*0.5, dy: lineWidth*0.5).

CATransform3DMakeRotation anchorpoint and position in Swift

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}
To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

Why drawing straight lines using Core Graphics I get "dashed" lines?

I have tried to draw an icon with the following swift code but I got "dashed" lines. I have just used moveToPoint and addLineToPoint functions. I got the same graphic glitch with other drawing too as you can see in the under images. I use Xcode 7.2 and run code on iOS 9.2.1.
#IBDesignable class DeleteButton: UIButton {
override func drawRect(rect: CGRect) {
UIColor.blackColor().setStroke()
let lineWidth = CGFloat(1)
// Draw empty circle
let frame = CGRect(x: rect.origin.x + lineWidth, y: rect.origin.y + lineWidth, width: rect.width - (lineWidth * 2), height: rect.height - (lineWidth * 2))
let path = UIBezierPath(ovalInRect: frame)
path.lineWidth = lineWidth
path.stroke()
// Draw X
let radius = (rect.width / 2) * 0.5
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let π = CGFloat(M_PI)
let centerWidth = radius * cos(π/4)
let centerHeight = radius * sin(π/4)
path.lineWidth = lineWidth
path.moveToPoint(CGPoint(x: CGFloat(center.x + centerWidth), y: CGFloat(center.y + centerHeight)))
path.addLineToPoint(CGPoint(x: CGFloat(center.x - centerWidth), y: CGFloat(center.y - centerHeight)))
path.stroke()
path.lineWidth = lineWidth
path.moveToPoint(CGPoint(x: center.x - centerWidth, y: center.y + centerHeight))
path.addLineToPoint(CGPoint(x: center.x + centerWidth, y: center.y - centerHeight))
path.stroke()
}
}
This is the result:
I apologize for my bad english.
I believe you are seeing the result of the button's title text drawing over the image. Check out the following demo using your code in a Playground:
We need to know the context in which that code runs. You can't just start drawing.
The usual way to draw in a view is to override the drawRect method. When you do that the system has the graphics context set up correctly with a mask that clips your drawing to the view's bounds, the coordinate system set up to the view's coordinates, etc.
If your drawing code is not in a view's drawRect method then that is your problem.

Resources