Add text label to drawn shape - ios

I'm following along with this tutorial for drawing squares where-ever there is a gesture recognized touch on the screen in iOS.
https://www.weheartswift.com/bezier-paths-gesture-recognizers/
I am now wanting to extend the functionality and want to add text labels to my newly drawn shapes indicating their coordinates.
So touching the screen would draw a rectangle, which moves with the pan gesture (so far so good) but I would also like it to show numbers indicating the coordinates.
How can I go about accomplishing this?
class CircularKeyView: UIView {
// a lot of this code came from https://www.weheartswift.com/bezier-paths-gesture-recognizers/
//all thanks goes to we<3swift
let lineWidth: CGFloat = 1.0
let size: CGFloat = 44.0
init(origin: CGPoint) {
super.init(frame: CGRectMake(0.0, 0.0, size, size))
self.center = origin
self.backgroundColor = UIColor.clearColor()
initGestureRecognizers() //start up all the gesture recognizers
}
func initGestureRecognizers() {
let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
addGestureRecognizer(panGR)
}
//PAN IT LIKE u FRYIN.
func didPan(panGR: UIPanGestureRecognizer) {
self.superview!.bringSubviewToFront(self)
var translation = panGR.translationInView(self)
self.center.x += translation.x
self.center.y += translation.y
panGR.setTranslation(CGPointZero, inView: self)
}
// We need to implement init(coder) to avoid compilation errors
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
let path = UIBezierPath(roundedRect: rect, cornerRadius: 7)
//draws awesome curvy rectangle
UIColor.darkGrayColor().setFill()
path.fill()
//draws outline
path.lineWidth = self.lineWidth
UIColor.blackColor().setStroke()
path.stroke()
//////
//probably where I should draw the text label on this thing,
//although it needs to update when the thingy moves.
}
}

In your drawRect implementation you can draw the coordinates of the view with something like:
("\(frame.origin.x), \(frame.origin.y)" as NSString).drawAtPoint(.zero, withAttributes: [
NSFontAttributeName: UIFont.systemFontOfSize(14),
NSForegroundColorAttributeName: UIColor.blackColor()
])
Which simply creates a string of the coordinates, casts it to an NSString and then calls the drawAtPoint method to draw it in the view's context.
You can of course change .zero to any CGPoint depending on where you want to draw the string and can edit the attributes as desired.
To make sure that this gets updated when the user pans around you will want to also add:
self.setNeedsDisplay()
to the bottom of your didPan method.
Hope this helps :)

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.

iOS Swift draw to screen

I am new to iOS development and need help from someone a bit more experienced than me. I searched the internet and couldn't find any working solution.
I need to draw to the screen like canvas in Android. Currently I have a CADisplayLink to call a function every frame. And that's working well. The problem is: How do I actually draw anything, like a rectangle, a circle or a line to the screen every frame?
This is what I have (I linked this class to the view in the storyboard):
class Canvas: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setLineWidth(2.0)
context?.setStrokeColor(UIColor.green.cgColor)
context?.move(to: CGPoint(x: 30, y: 30))
context?.addLine(to: CGPoint(x: Double(xBall), y: Double(yBall)))
context?.strokePath()
}
}
With the following code I can actually draw a line to the screen:
let canvas = Canvas()
canvas.draw(CGRect())
The problem is, that this works exactly ONE time. When I have canvas.draw(CGRect()) in my loop which repeats every frame, it works for the first frame (the initial values of xBall and yBall) and never again. When I print the values in the draw method, it gets called every frame and the variables have the correct values. But it does not draw it to the screen. I tried adding the line setNeedsDisplay() in the draw method, with similar results.
Any help will be appreciated! Thanks!
If you refer to the draw(_:) documentation, it says:
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay() or setNeedsDisplay(_:) method instead.
The common approach would be to have your view controller viewDidLoad method add Canvas view:
override func viewDidLoad() {
super.viewDidLoad()
let canvas = Canvas()
canvas.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(canvas)
NSLayoutConstraint.activate([
canvas.leadingAnchor.constraint(equalTo: view.leadingAnchor),
canvas.trailingAnchor.constraint(equalTo: view.trailingAnchor),
canvas.topAnchor.constraint(equalTo: view.topAnchor),
canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
You don’t call draw(_:) yourself, but rather the OS will do so automatically. All you need to do is add it to your view hierarchy with addSubview(_:). And you can then just have your CADisplayLink update the properties and call setNeedsDisplay (or, better, add didSet observers to those properties that calls setNeedsDisplay for you).
By the way, if you don’t want to add this programmatically, like shown above, you can add Canvas right in Interface Builder. Just drag a UIView onto your storyboard scene, add all of the appropriate constraints, go to the “identity” inspector, and set the base class name to be Canvas:
And if you mark your class as #IBDesignable, you can actually see your path rendered right in Interface Builder, like shown above.
A number of refinements:
If you are going to implement draw(_:) yourself, instead of getting a graphics context with UIGraphicsGetCurrentContext, you might just stroke a UIBezierPath:
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 30))
path.addLine(to: CGPoint(x: xBall, y: yBall))
path.lineWidth = 2
UIColor.green.setStroke()
path.stroke()
}
Like your solution, this requires that after you update xBall and yBall, if you call setNeedsDisplay to have the view re-rendered with the updated path.
Sometimes we wouldn’t even implement draw(_:). We would just add a CAShapeLayer as a sublayer:
#IBDesignable
class Canvas: UIView {
var xBall = ...
var yBall = ...
let shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 2
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
layer.addSublayer(shapeLayer)
updatePath()
}
func updatePath() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 30))
path.addLine(to: CGPoint(x: xBall, y: yBall))
shapeLayer.path = path.cgPath
}
}
In this sort of approach, you just update the path of the shapeLayer and the OS will render your shape layer (and its path) for you.

CollectionViewCell Draw Rect

I'm trying to draw a simple outlined circle inside my collection view cells. For some reason, only the first cell is being draw, the rest are not showing.
class UserCell: UICollectionViewCell {
override func draw(_ rect: CGRect) {
let center = CGPoint(x: self.center.x - 1, y: 41)
let circularPath = UIBezierPath()
circularPath.addArc(withCenter: center, radius: 36, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: true)
UIColor.red.setStroke()
circularPath.lineWidth = 2
circularPath.stroke()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
What am I missing here?
Collection view cells are configured for display using UICollectionViewDataSource method cellForItemAt(). The cells are reused and won't automatically redraw for each 'new' cell. Instead of overriding draw(rect), add subviews to the cell and configure the subviews in cellForItemAt().
You might want to define your center point differently. The center point is specified in points in the coordinate system of its superview. Either try convert the cell's center point from its superview's coordinate system or use the bounds of the cell instead and adjust the x and y values accordingly.
let center = self.convert(self.center, from: self.superview)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
Created a new class conforming to UIView(), added the bezierPath info inside its draw function. Then subclassed this class inside the collectionView cell. Works as expected.

Erasing a stroke path with Quartz2D is deleting more than just the path

I'm trying to set an eraser tool for a drawing app. I've been following this tutorial for the basics, but the drawings happen in a white background so the deletion part is not covered (they draw in white color to delete)
I've been implementing a method to delete my drawings, and it works pretty well. I draw a circle, set the color and the blend mode to clear and set the path where I want to draw that circle. Then I get an image returned by the method UIGraphicsGetImageFromCurrentImageContext() that updates my current image, with the new drawing over it.
The drawn circle it's doing it's job and it deletes where it has been drawn. But the problem is the image begins to get deleted from the right to the left, covering all it's height, and this is certainly not set anywhere in my code.
I don't know if it's a bug.
I've tried everything and I can't get a new image from context without this new deletion line in the right. And the more I draw, the more the new line grows.
As you can see in the gif, it looks like I'm drawing in gray over a white background, but the white color is an image that has been filled with white color, and it's background color it's gray, so I'm deleting.
The code:
In a UIView (blue background) I initialize and add two white columns as subviews:
func addNewColumn(){
let column : Column = Column.init(frame: CGRect(x: self.frame.size.width*0.7, y: 0, width: self.frame.size.width*0.125, height: self.frame.size.height))
let column2 : Column = Column.init(frame: CGRect(x: self.frame.size.width*0.2, y: 0, width: self.frame.size.width*0.125, height: self.frame.size.height))
self.addSubview(column)
self.addSubview(column2)
}
The initialization of the column in its class:
import UIKit
class Column: UIImageView {
//Touches Positions
var firstPoint : CGPoint?
var lastPoint : CGPoint?
override init(frame: CGRect) {
super.init(frame: frame)
self.isUserInteractionEnabled = true
self.draw(frame)
self.backgroundColor = UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
//Drawing of the white color in the image, over the blue background
override func draw(_ rect: CGRect) {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.white.cgColor)
context?.setBlendMode(.normal)
context?.fill(self.bounds)
self.image = UIGraphicsGetImageFromCurrentImageContext()
self.alpha = 1
UIGraphicsEndImageContext()
}
//The method called from touchesMoved() to delete the white color
func eraseColumn() {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
let context = UIGraphicsGetCurrentContext()
self.image?.draw(in: self.bounds)
context?.beginPath()
context?.addEllipse(in: CGRect(x: (lastPoint?.x)!, y: (lastPoint?.y)!, width: 10, height: 10))
context?.setLineCap(.round)
context?.setLineWidth(10)
context?.setStrokeColor(UIColor.clear.cgColor)
context?.setBlendMode(.clear)
context?.strokePath()
context?.closePath()
self.image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
}
//Touches handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
firstPoint = touch?.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
lastPoint = touch?.location(in: self)
eraseColumn()
}
}
As you can see in the draw(_ rect: CGRect) method, I fill the image with white color and set it's background color to gray.
And in the eraseColumn() method, I delete the white color that the user touched. No matter what I try, the image is being deleted. I simply don't know why this is happening.
Any help would be very appreciated.
Project is in swift 3, X-Code 9.2.
Finally found the solution
changing
self.image?.draw(in: self.bounds)
to
self.image?.draw(at: self.bounds.origin)
fixes the problem. Hope it will help somebody.
Cheers

Drawing sublayers inside a bezier Arc

I'm trying to draw a series of vertical lines inside of an arc but I'm having trouble being able to do this. I'm trying to do this using CAShapeLayers The end result is something that looks like this.
I know how to draw the curved arc and the line segments using CAShapeLayers but what I can't seem to figure out is how to draw the vertical lines inside the CAShapeLayer
My initial approach is to subclass CAShapeLayer and in the subclass, attempt to draw the vertical lines. However, I'm not getting the desired results. Here is my code for adding a line to a bezier point and attempting to add the sub layers.
class CustomLayer : CAShapeLayer {
override init() {
super.init()
}
func drawDividerLayer(){
print("Init has been called in custom layer")
print("The bounds of the custom layer is: \(bounds)")
print("The frame of the custom layer is: \(frame)")
let bezierPath = UIBezierPath()
let dividerShapeLayer = CAShapeLayer()
dividerShapeLayer.strokeColor = UIColor.redColor().CGColor
dividerShapeLayer.lineWidth = 1
let startPoint = CGPointMake(5, 0)
let endPoint = CGPointMake(5, 8)
let convertedStart = convertPoint(startPoint, toLayer: dividerShapeLayer)
let convertedEndPoint = convertPoint(endPoint, toLayer: dividerShapeLayer)
bezierPath.moveToPoint(convertedStart)
bezierPath.addLineToPoint(convertedEndPoint)
dividerShapeLayer.path = bezierPath.CGPath
addSublayer(dividerShapeLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class DrawView : UIView {
var customDrawLayer : CAShapeLayer!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//drawLayers()
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
}
func drawLayers() {
let bezierPath = UIBezierPath()
let startPoint = CGPointMake(5, 35)
let endPoint = CGPointMake(100, 35)
bezierPath.moveToPoint(startPoint)
bezierPath.addLineToPoint(endPoint)
let customLayer = CustomLayer()
customLayer.frame = CGPathGetBoundingBox(bezierPath.CGPath)
customLayer.drawDividerLayer()
customLayer.strokeColor = UIColor.blackColor().CGColor
customLayer.opacity = 0.5
customLayer.lineWidth = 8
customLayer.fillColor = UIColor.clearColor().CGColor
layer.addSublayer(customLayer)
customLayer.path = bezierPath.CGPath
}
However this code produces this image:
It definitely seems that I have a coordinate space problem/bounds/frame issue but I'm not quite sure. The way I want this to work is to draw from the top of the superLayer to the bottom of the superLayer inside of the CustomLayer class. But not only that, this must work using the bezier path addArcWithCenter: method which I haven't gotten to yet because I'm trying to solve this problem first. Any help would be appreciated.
The easiest way to draw an arc that consists of lines is to use lineDashPattern:
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: false)
let arc = CAShapeLayer()
arc.path = path.CGPath
arc.lineWidth = 50
arc.lineDashPattern = [4,15]
arc.strokeColor = UIColor.lightGrayColor().CGColor
arc.fillColor = UIColor.clearColor().CGColor
view.layer.addSublayer(arc)
So this is a blue arc underneath the dashed arc shown above. Obviously, I enlarged it for the sake of visibility, but it illustrates the idea.

Resources