How do I use this circle drawing code in a UIView? - ios

I am making an app including some breathing techniques for a client. What he wants is to have a circle in the middle. For breathing in it becomes bigger, for breathing out tinier. The thing is, that he would like to have a cool animated circle in the middle, not just a standard one. I showed him this picture from YouTube:
The code used in the video looks like this:
func drawRotatedSquares() {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 512, height: 512), false, 0)
let context = UIGraphicsGetCurrentContext()
context!.translateBy(x: 256, y: 256)
let rotations = 16
let amount = M_PI_2 / Double(rotations)
for i in 0 ..< rotations {
context!.rotate(by: CGFloat(amount))
//context!.addRect(context, CGRect(x: -128, y: -128, width: 256, height: 256))
context!.addRect(CGRect(x: -128, y: -128, width: 256, height: 256))
}
context!.setStrokeColor(UIColor.black as! CGColor)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageView.image = img
}
But if I run it, my simulator shows just a white screen. How do I get this circle into my Swift 3 app and how would the code look like? And is it possible not to show it in an ImageView but simply in a view?
Thank you very much!

Here is an implementation as a UIView subclass.
To set up:
Add this class to your Swift project.
Add a UIView to your Storyboard and change the class to Circle.
Add an outlet to your viewController
#IBOutlet var circle: Circle!
Change the value of multiplier to change the size of the circle.
circle.multiplier = 0.5 // 50% of size of view
class Circle: UIView {
var multiplier: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// Calculate size of square edge that fits into the view
let size = min(bounds.width, bounds.height) * multiplier / CGFloat(sqrt(2)) / 2
// Move origin to center of the view
context.translateBy(x: center.x, y: center.y)
// Create a path to draw a square
let path = UIBezierPath()
path.move(to: CGPoint(x: -size, y: -size))
path.addLine(to: CGPoint(x: -size, y: size))
path.addLine(to: CGPoint(x: size, y: size))
path.addLine(to: CGPoint(x: size, y: -size))
path.close()
UIColor.black.setStroke()
let rotations = 16
let amount = .pi / 2 / Double(rotations)
for _ in 0 ..< rotations {
// Rotate the context
context.rotate(by: CGFloat(amount))
// Draw a square
path.stroke()
}
}
}
Here it is running in a Playground:

You posted a singe method that generates a UIImage and installs it in an image view. If you don't have the image view on-screen then it won't show up.
If you create an image view in your view controller and connect an outlet to the image view then the above code should install the image view into your image and draw it on-screen.
You could rewrite the code you posted as the draw(_:) method of a custom subclass of UIView, in which case you'd get rid of the context setup and UIImage stuff, and simply draw in the current context. I suggest you search on UIView custom draw(_:) methods for more guidance.

Related

How do I draw stuff under the text of a UILabel?

I created a custom UILabel subclass that has a circle in the middle, and the label's text (which is a number) will be on top of the circle.
I initially thought of doing this using layer.cornerRadius, but that will not create a circle when the label's width and height are not equal.
What I mean is, for a label with width 100 and height 50, I still want a circle with radius 50 and centre at (50, 25).
Therefore, I tried to use UIBezierPath to draw the circle. This is what I have tried:
override func draw(_ rect: CGRect) {
super.draw(rect)
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
}
I have put super.draw(rect) because I thought that would draw the label's text, but when I run the app, I only see the circle and not my label text.
I am very confused because why hasn't super.draw(rect) drawn the label's text?
The text is not seen because the "z-index" of UIBezierPaths depends on the order in which they are drawn. In other words, UIBezierPaths are drawn on top of each other.
super.draw(rect) indeed draws the text. But when you put it as the first statement, it will get drawn first, so everything you draw after that, goes on top of the text. To fix this, you should call super.draw(rect) last:
override func draw(_ rect: CGRect) {
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
super.draw(rect) // <------- here!
}
Alternatively, just subclass UIView, draw the circle in draw(_:), and add a UILabel as a subview of that. The advantage if this approach is that it does not depend on the implementation of super.draw(_:), which might change in the future,

Swift: UIBezierPath fill intersection of 2 paths

I'm working with 2 UIBezierPath to create a rectangle and a semicircle shape. It works fine but I am not able to fill the intersection with color.
That's my drawCircle and my addPaths functions:
private func drawCircle(selectedPosition: SelectionRangePosition, currentMonth: Bool) {
circleView.layer.sublayers = nil
let radius = contentView.bounds.height/2
let arcCenter = CGPoint(x: ceil(contentView.bounds.width/2), y: ceil(contentView.bounds.height/2))
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: .pi/2, endAngle: .pi * 3/2, clockwise: selectedPosition == .left)
let semiCircleLayerPath = UIBezierPath()
let semiCircleLayer = CAShapeLayer()
semiCircleLayer.fillColor = UIColor.tealish.cgColor
circleView.layer.addSublayer(semiCircleLayer)
switch (selectedPosition, currentMonth) {
case (.left, true):
let rectanglePath = UIBezierPath(rect: CGRect(x: contentView.bounds.width / 2, y: 0, width: ceil(contentView.bounds.width / 2), height: contentView.bounds.height))
addPaths(semiCircleLayerPath, rectanglePath, circlePath, semiCircleLayer)
case (.middle, true):
backgroundColor = .tealish
case (.right, true):
// Note: The + 10 is just to overlap the shapes and test if the filling works
let rectanglePath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: ceil(contentView.bounds.width / 2) + 10, height: contentView.bounds.height))
addPaths(semiCircleLayerPath, rectanglePath, circlePath, semiCircleLayer)
default:
backgroundColor = .clear
}
}
private func addPaths(_ semiCircleLayerPath: UIBezierPath, _ rectanglePath: UIBezierPath, _ circlePath: UIBezierPath, _ semiCircleLayer: CAShapeLayer) {
semiCircleLayerPath.append(circlePath)
semiCircleLayerPath.append(rectanglePath)
semiCircleLayer.path = semiCircleLayerPath.cgPath
}
The problem is that the semicircle path and the rectangle path are being added in opposite directions. This is a function of the way the Cocoa API adds them, unfortunately; in your case, it means that the intersection has a "wrap count" of 0.
Fortunately, there is an easy fix: create separate paths for the semicircle and the rectangle. When fill them both individually, you should get your desired result.
Unfortunately, this will mean that you can't use layer drawing only: you will want to use the drawRect method to draw the background.
If your dates are separate UILabels, you can skip this rendering & path filling problem entirely by dropping a UIView as a child of the calendar, under the labels holding the dates; give it a background color & rounded corners (using layer.cornerRadius as usual). That'll take care of your rounded rect for you.

Adding the same layer in a subview changes its visual position

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?
#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.
Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

Views drawn from code (PaintCode) are pixelated, very pixelated when scaled

I am building an app that overlays views drawn with code (output from PaintCode) onto photos. I have added gesture recognizers to rotate and scale the views drawn with code.
There is some mild pixelation on the views drawn on top. If I do any rotation or scale the image larger (even a slight bit), there is a lot more pixelation.
Here is a comparison of the images:
No rotating or scaling:
A small amount of rotation/scaling:
Here is the UIView extension I'm using to output the composited view:
extension UIView {
func printViewToImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 2.0
let renderer = UIGraphicsImageRenderer(bounds: self.bounds, format: format)
return renderer.image { rendererContext in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
}
}
Even if I set the scale to something like 4.0, there is no difference.
Here is the code I'm using for the scale/rotation gesture recognizers:
#IBAction func handlePinch(recognizer: UIPinchGestureRecognizer) {
guard let view = recognizer.view else {
return
}
view.transform = view.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
recognizer.scale = 1
}
#IBAction func handleRotate(recognizer: UIRotationGestureRecognizer) {
guard let view = recognizer.view else {
return
}
view.transform = view.transform.rotated(by: recognizer.rotation)
recognizer.rotation = 0
}
I have experimented with making the canvasses very large in PaintCode (3000x3000), and there is no difference, so I don't think it has to do with that.
How can I draw/export these views so that they are not pixelated?
Edit: Here's what some of the drawing code looks like...
public dynamic class func drawCelebrateDiversity(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 3000, height: 3000), resizing: ResizingBehavior = .aspectFit, color: UIColor = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()!
//// Resize to Target Frame
context.saveGState()
let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 3000, height: 3000), target: targetFrame)
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 3000, y: resizedFrame.height / 3000)
//// Bezier 13 Drawing
let bezier13Path = UIBezierPath()
bezier13Path.move(to: CGPoint(x: 2915.18, y: 2146.51))
bezier13Path.addCurve(to: CGPoint(x: 2925.95, y: 2152.38), controlPoint1: CGPoint(x: 2919.93, y: 2147.45), controlPoint2: CGPoint(x: 2924.05, y: 2147.91))
When scaling UIViews (or custom CALayers), you should set their contentsScale to match the desired density of their content. UIViews set their layer contentsScale to screen scale (2 on retina), and you need to multiply this with the extra scale you do via transform.
view.layer.contentsScale = UIScreen.main.scale * gesture.scale;
Even if the drawing code is resolution independent, everything on screen must be converted to bitmap at some time. UIView allocates bitmap with size of bounds.size * contentsScale and then invokes -drawRect:/draw(_ rect:) method.
It is important to set contentsScale on that view that draws, even if that view is not scaled (but some of its parent is). A common solution is to recursively set contentsScale on all sublayers of the scaled view.
– PaintCode Support

I have custom UIButtons which are rotated 45 Degrees - how do I make it so that only the space within the actual shape is clickable?

So my custom button looks like this:
override func drawRect(rect: CGRect) {
let mask = CAShapeLayer()
mask.frame = self.layer.bounds
let width = self.layer.frame.size.width
let height = self.layer.frame.size.height
let path = CGPathCreateMutable()
CGPathMoveToPoint(path,nil,width/2, 0)
CGPathAddLineToPoint(path,nil,width, height/2)
CGPathAddLineToPoint(path,nil,width/2, height)
CGPathAddLineToPoint(path,nil, 0, height/2)
mask.path = path
self.layer.mask = mask
}
I've added the button to a UIView in Storyboard, and subclassed it to this UIButton subclass. I set the background to blue in Storyboard.
The result:
The problem is then I click around the corners (the white space, as if the button was still being recognised as a square), it is still clickable. This is an issue because I want to add multiple shapes like this next to each other, so I don't want any of the Buttons to block each other out.
You can override the pointInside function in your custom UIButton to control when a touch in your view's bounds should be considered a hit.
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let width = bounds.size.width
let height = bounds.size.height
// Make a UIBezierPath that contains your clickable area
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: width / 2, y: 0))
path.addLineToPoint(CGPoint(x: width, y: height / 2))
path.addLineToPoint(CGPoint(x: width / 2, y: height))
path.addLineToPoint(CGPoint(x: 0, y: height / 2))
path.closePath()
// Let a hit happen if the point touched is in the path
return path.containsPoint(point)
}
See also
Allowing interaction with a UIView under another UIView
How about adding UITapGestureRecognizer to the button view, get the coordinate of tap and convert the coordinates to parent view coordinate and compare if its in the twisted cube.
//sender being the UITapGestureRecognizer
let coordinate = containerView.convertPoint(sender.locationInView(ContainerView), toCoordinateFromView: containerView)

Resources