Centre animation on every screen size - ios

I created this animation in the centre of the screen on a 6-inch size view. How can I make sure the ratio stays the same when it adapts to let's say an iPhone 5s. I'm not talking about Auto-Layout or constraints. I added the animation code below. This works the way I want it in a 6-inch size, but again, When I resize everything for iPhone 5s, Everything looks fine, besides the animation itself. How can I fix this?
[
iphone 6 animation screen above (This is the correct animation and how I want it to be position on the other screen size.)
override func viewDidLoad()
super.viewDidLoad() {
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 300, height: 300)
self.view.addSubview(circleAnimationView)
}
The animation code is on on seperate viewcontroller, here it is:
import UIKit
class CirlceAnimationView: UIView {
var replicatorLayer1 = CAReplicatorLayer()
var dot = CALayer()
// Animation starts running
func animation2() {
// A layer that creates a specified number of copies of its sublayers (the source layer), each copy potentially having geometric, temporal, and color transformations applied to it.
replicatorLayer1 = CAReplicatorLayer()
// The layer’s bounds rectangle. Animatable.
replicatorLayer1.bounds = self.bounds
// The radius to use when drawing rounded corners for the layer’s background. Animatable.
replicatorLayer1.cornerRadius = 10.0
// The background color of the receiver. Animatable.
replicatorLayer1.backgroundColor = UIColor(white: 0.0, alpha: 0.0).cgColor
// The layer’s position in its superlayer’s coordinate space. Animatable.
replicatorLayer1.position = self.center
// calling this method creates an array for that property and adds the specified layer to it.
self.layer.addSublayer(replicatorLayer1)
// connectng the animation to the content
// An object that manages image-based content and allows you to perform animations on that content
dot = CALayer()
// The layer’s bounds rectangle. Animatable.
dot.bounds = CGRect(x: 0.0, y: 0.0, width: 12.0, height: 12.0)
//The layer’s position in its superlayer’s coordinate space. Animatable.
dot.position = CGPoint(x: 150.0, y: 40.0)
//The background color of the receiver. Animatable.
dot.backgroundColor = UIColor(white: 0.2, alpha: 1.0).cgColor
// The color of the layer’s border. Animatable.
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
// The width of the layer’s border. Animatable.
dot.borderWidth = 1.0
//The radius to use when drawing rounded corners for the layer’s background. Animatable.
dot.cornerRadius = 5.0
//Appends the layer to the layer’s list of sublayers.
replicatorLayer1.addSublayer(dot)
// number of copies of layer is instanceCount
let nrDots: Int = 1000
//The number of copies to create, including the source layers.
replicatorLayer1.instanceCount = nrDots
// The basic type for floating-point scalar values in Core Graphics and related frameworks.
let angle = CGFloat(2*M_PI) / CGFloat(nrDots)
// The transform matrix applied to the previous instance to produce the current instance. Animatable.
replicatorLayer1.instanceTransform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
// Type used to represent elapsed time in seconds.
let duration: CFTimeInterval = 10.0
// animation capabilities for a layer property.
// An object that provides basic, single-keyframe animation capabilities for a layer property.
let shrink = CABasicAnimation(keyPath: "transform.scale")
// Defines the value the receiver uses to start interpolation.
shrink.fromValue = 1.0
// Defines the value the receiver uses to end interpolation.
shrink.toValue = 0.1
// Specifies the basic duration of the animation, in seconds.
shrink.duration = duration
// Determines the number of times the animation will repeat.
shrink.repeatCount = Float.infinity
// Add the specified animation object to the layer’s render tree.
dot.add(shrink, forKey: "shrink")
// Specifies the delay, in seconds, between replicated copies. Animatable.
replicatorLayer1.instanceDelay = duration/Double(nrDots)
// The transform applied to the layer’s contents. Animatable.
dot.transform = CATransform3DMakeScale(0.01, 0.01, 0.01)
}
}

Declare a variable for device width :
var DEVICE_WIDTH = ""
Then in ViewDidLoad :
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth = screenSize.width
print(screenWidth)
// Detect the screen width (format purpose)
switch screenWidth {
case 320.0:
DEVICE_WIDTH = "320"
case 375.0:
DEVICE_WIDTH = "375"
case 414.0:
DEVICE_WIDTH = "414"
default: //320.0
DEVICE_WIDTH = "320"
}
Then in viewDidAppear :
switch DEVICE_WIDTH {
case "375": // 4/5
// according to your need
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 250, height: 250)
case "414": //6
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 300, height: 300)
default: //6+ (414)
// according to your need
circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 350, height: 350)
}

circleAnimationView.frame = CGRect(x: 20.0, y: 90.0, width: 300, height: 300)
Instead of hard coding those numbers, calculate them based on the superview bounds.
(But it would be better to position the circle animation view using auto layout.)

Related

Invert simple UIView mask (cut hole instead of clip to circle) [duplicate]

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 1 year ago.
I'm trying to avoid CAShapeLayer because I would need to mess with CABasicAnimation, not UIView.animate. So instead, I'm just using UIView's mask property to mask views. Here's my code currently:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 300))
imageView.image = UIImage(named: "TestImage")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
view.addSubview(imageView)
let maskView = UIView(frame: CGRect(x: 100, y: 100, width: 80, height: 80))
maskView.backgroundColor = UIColor.blue /// ensure opaque
maskView.layer.cornerRadius = 10
imageView.mask = maskView /// set the mask
}
}
Without imageView.mask = maskView
With imageView.mask = maskView
It makes a portion of the image view visible. However, this is what I want:
Instead of making part of the image view visible, how can I cut a hole in it?
You can create an image view and set that as your mask. Note that this does not lend itself to animation. If you want to animate the mask to different shapes, you should add a mask to your view's CALayer and use CALayerAnimation, as you mention. It's not that bad.
Below I outline how to generate an image with a transparent part (a hole) that you can use as a mask in an image view. If your goal is to animate the size, shape, or position of the hole, however, this won't work. You'd have to regenerate the mask image for every frame, which would be really slow.
Here's how you would get the effect your are after for static views using an image view as a mask:
Use UIGraphicsBeginImageContextWithOptions() UIGraphicsImageRenderer to create an image that is opaque for most of your image, and has a transparent "hole" where you want a hole.
Then install that image in your image view, and make that image view your mask.
The code to create a mostly opaque image with a transparent rounded rect "hole" might look like this:
/**
Function to create a UIImage that is mostly opaque, with a transparent rounded rect "knockout" in it. Such an image might be used ask a mask
for another view, where the transparent "knockout" appears as a hole in the view that is being masked.
- Parameter size: The size of the image to create
- Parameter transparentRect: The (rounded )rectangle to make transparent in the middle of the image.
- Parameter cornerRadius: The corner radius ot use in the transparent rectangle. Pass 0 to make the rectangle square-cornered.
*/
func imageWithTransparentRoundedRect(size: CGSize, transparentRect: CGRect, cornerRadius: CGFloat) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { (context) in
let frame = CGRect(origin: .zero, size: size)
UIColor.white.setFill()
context.fill(frame)
let roundedRect = UIBezierPath(roundedRect: transparentRect, cornerRadius: cornerRadius)
context.cgContext.setFillColor(UIColor.clear.cgColor)
context.cgContext.setBlendMode(.clear)
roundedRect.fill()
}
return image
}
And a viewDidLoad method that installs a UIImageView with a mask image view with a hole in it might look like this:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .cyan
let size = CGSize(width: 200, height: 300)
let origin = CGPoint(x: 50, y: 50)
let frame = CGRect(origin: origin, size: size)
let imageView = UIImageView(frame: frame)
imageView.image = UIImage(named: "TestImage")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
view.addSubview(imageView)
imageView.layer.borderWidth = 2
//Create a mask image view the same size as the (image) view we will be masking
let maskView = UIImageView(frame: imageView.bounds)
//Build an opaque UIImage with a transparent "knockout" rounded rect inside it.
let transparentRect = CGRect(x: 100, y: 100, width: 80, height: 80)
let maskImage = imageWithTransparentRoundedRect(size: size, transparentRect: transparentRect, cornerRadius: 20)
//Install the image with the "hole" into the mask image view
maskView.image = maskImage
//Make the maskView the ImageView's mask
imageView.mask = maskView /// set the mask
}
}
I created a sample project using the code above. You can download it from Github here:
https://github.com/DuncanMC/UIImageMask.git
I just updated the project to also show how to do the same thing using a CAShapeLayer as a mask on the image view's layer. Doing it that way, it's possible to animate changes to the mask layer's path.
The new version has a segmented control that lets you pick whether to mask the image view using a UIImage in the view's mask property, or via a CAShapeLayer used as a mask on the image view's layer.
For the CAShapeLayer version, the mask layer's path is a rectangle the size of the whole image view, with a second, smaller rounded rectangle drawn inside it. The winding rule on the shape layer is then set to the "even/odd" rule, meaning that if you have to cross an even number of shape boundaries to get to a point, it is considered outside the shape. That enables you to create hollow shapes like we need here.
When you select the layer mask option, it enables an animation button that animates random changes to the "cutout" transparent rectangle in the mask.
The function that creates the mask path looks like this:
func maskPath(transparentRect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
let fullRect = UIBezierPath(rect: maskLayer.frame)
let roundedRect = UIBezierPath(roundedRect: transparentRect, cornerRadius: cornerRadius)
fullRect.append(roundedRect)
return fullRect
}
And the function that does the animation looks like this:
#IBAction func handleAnimateButton(_ sender: Any) {
//Create a CABasicAnimation that will change the path of our maskLayer
//Use the keypath "path". That tells the animation object what property we are animating
let animation = CABasicAnimation(keyPath: "path")
animation.autoreverses = true //Make the animation reverse back to the oringinal position once it's done
//Use ease-in, ease-out timing, which looks smooth
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = 0.3 //Make each step in the animation last 0.3 seconds.
let transparentRect: CGRect
//Randomly either animate the transparent rect to a different shape or shift it
if Bool.random() {
//Make the transparent rect taller and skinnier
transparentRect = self.transparentRect.inset(by: UIEdgeInsets(top: -20, left: 20, bottom: -20, right: 20))
} else {
//Shift the transparent rect to by a random amount that still says inside the image view's bounds.
transparentRect = self.transparentRect.offsetBy(dx: CGFloat.random(in: -100...20), dy: CGFloat.random(in: -100...100))
}
let cornerRadius: CGFloat = CGFloat.random(in: 0...30)
//install the new path as the animation's `toValue`. If we dont specify a `fromValue` the animation will start from the current path.
animation.toValue = maskPath(transparentRect: transparentRect, cornerRadius: cornerRadius).cgPath
//add the animation to the maskLayer. Since the animation's `keyPath` is "path",
//it will animate the layer's "path" property to the "toValue"
maskLayer.add(animation, forKey: nil)
//Since we don't actually change the path on the mask layer, the mask will revert to it's original path once the animation completes.
}
The results (using my own sample image) look like this:
A sample of the CALayer based mask animation looks like this:

Why UIView.animate performs different result on view.layer and sublayer

Here is my code:
let colorLayer = CALayer()
colorLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
colorLayer.backgroundColor = UIColor.blue.cgColor
let blockView = UIView.init(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
blockView.backgroundColor = UIColor.red
self.view.addSubview(blockView)
blockView.layer.addSublayer(colorLayer)
Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { (t) in
UIView.animate(withDuration: 1.0) { [self] in
// this animation lasts 1s
// let transform = blockView.layer.affineTransform()
// blockView.layer.setAffineTransform(transform.rotated(by: CGFloat(Double.pi / 2.0)))
// while this animation 0.25s
let transform2 = colorLayer.affineTransform()
colorLayer.setAffineTransform(transform2.rotated(by: CGFloat(Double.pi / 2.0)))
} completion: { (complete) in
}
})
Why my custom sublayer colorLayer rotates by 0.25s (default value), not set by withDuration: parameter, but blockView.layer works fine?
UIView animations are meant to animate changes to animatable properties of UIView objects. They are not meant to animate CALayer properties.
Your code to change the colorLayer is probably not being animated by the UIView animation call at all. It is just that a CALayer's transform property supports implicit animations, so changing it triggers an implicit animation. I would expect that line to have exactly the same effect outside of a UIView animation.
I suspect that your call to apply an affine transform to your block view works because it ends changing the same transform property that you would change by using code like
blockView.transform = blockView.transform.rotated(by: CGFloat(Double.pi / 2.0)
The short answer: Don't try to animate layer properties using UIView animation. It usually doesn't work.

How do I make everything outside of a CAShapeLayer black with an opacity of 50% with Swift?

I have the following code which draws a shape:
let screenSize: CGRect = UIScreen.main.bounds
let cardLayer = CAShapeLayer()
let cardWidth = 350.0
let cardHeight = 225.0
let cardXlocation = (Double(screenSize.width) - cardWidth) / 2
let cardYlocation = (Double(screenSize.height) / 2) - (cardHeight / 2) - (Double(screenSize.height) * 0.05)
cardLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cardWidth, height: 225.0), cornerRadius: 10.0).cgPath
cardLayer.position = CGPoint(x: cardXlocation, y: cardYlocation)
cardLayer.strokeColor = UIColor.white.cgColor
cardLayer.fillColor = UIColor.clear.cgColor
cardLayer.lineWidth = 4.0
self.previewLayer.insertSublayer(cardLayer, above: self.previewLayer)
I want everything outside of the shape to be black with an opacity of 50%. That way you can see the camera view still behind it, but it's dimmed, except where then shape is.
I tried adding a mask to previewLayer.mask but that didn't give me the effect I was looking for.
Your impulse to use a mask is correct, but let's think about what needs to be masked. You are doing three things:
Dimming the whole thing. Let's call that the dimming layer. It needs a dark semi-transparent background.
Drawing the white rounded rect. That's the shape layer.
Making a hole in the entire thing. That's the mask.
Now, the first two layers can be the same layer. That leaves only the mask. This is not trivial to construct: a mask affects its owner in terms entirely of its transparency, so we need a mask that is opaque except for an area shaped like the shape of the shape layer, which needs to be clear. To get that, we start with the shape and clip to that shape as we fill the mask — or we can clip to that shape as we erase the mask, which is the approach I prefer.
In addition, your code has some major flaws, the most important of which is that your shape layer has no size. Without a size, there is nothing to mask.
So here, with corrections and additions, is your code; I made this the entirety of a view controller, for testing purposes, and what I'm covering is the entire view controller's view rather than a particular subview or sublayer:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
}
private var didInitialLayout = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if didInitialLayout {
return
}
didInitialLayout = true
let screenSize = UIScreen.main.bounds
let cardLayer = CAShapeLayer()
cardLayer.frame = self.view.bounds
self.view.layer.addSublayer(cardLayer)
let cardWidth = 350.0 as CGFloat
let cardHeight = 225.0 as CGFloat
let cardXlocation = (screenSize.width - cardWidth) / 2
let cardYlocation = (screenSize.height / 2) - (cardHeight / 2) - (screenSize.height * 0.05)
let path = UIBezierPath(roundedRect: CGRect(
x: cardXlocation, y: cardYlocation, width: cardWidth, height: cardHeight),
cornerRadius: 10.0)
cardLayer.path = path.cgPath
cardLayer.strokeColor = UIColor.white.cgColor
cardLayer.lineWidth = 8.0
cardLayer.backgroundColor = UIColor.black.withAlphaComponent(0.5).cgColor
let mask = CALayer()
mask.frame = cardLayer.bounds
cardLayer.mask = mask
let r = UIGraphicsImageRenderer(size: mask.bounds.size)
let im = r.image { ctx in
UIColor.black.setFill()
ctx.fill(mask.bounds)
path.addClip()
ctx.cgContext.clear(mask.bounds)
}
mask.contents = im.cgImage
}
And here's what we get. I didn't have a preview layer but the background is red, and as you see, the red shows through inside the white shape, which is just the effect you are looking for.
The shape layer can only affect what it covers, not the space it doesn't cover. Make a path that covers the entire video and has a hole in it where the card should be.
let areaToDarken = previewLayer.bounds // assumes origin at 0, 0
let areaToLeaveClear = areaToDarken.insetBy(dx: 50, dy: 200)
let shapeLayer = CAShapeLayer()
let path = CGPathCreateMutable()
path.addRect(areaToDarken, ...)
path.addRoundedRect(areaToLeaveClear, ...)
cardLayer.frame = previewLayer.bounds // use frame if shapeLayer is sibling
cardLayer.path = path
cardLayer.fillRule = .evenOdd // allow holes
cardLayer.fillColor = black, 50% opacity

How to position Layers in swift programmatically?

I am trying to position a circular progress bar but I am failing constantly. I guess I am not understanding the concept of frames and views and bounds. I found this post on stack overflow that shows me exactly how to construct a circular progress bar.
However, in the post, a circleView was created as UIView using CGRect(x: 100, y: 100, width: 100, height: 100)
In my case, manually setting the x and y coordinates is obv a big no. So the circleView has to be in the centre of it's parent view.
Here is my view hierarchy:
view -> view2ForHomeController -> middleView -> circleView
So everything is positioned using auto layout. Here is the problem:The circleView is adding properly and it positions itself at the x and y value I specify but how can I specify the x and y values of the center of middleView. I tried the following but the center values are returned as 0.0, 0.0.
self.view.addSubview(self.view2ForHomeController)
self.view2ForHomeController.fillSuperview()
let xPosition = self.view2ForHomeController.middleView.frame.midX
let yPostion = self.view2ForHomeController.middleView.frame.midY
print(xPosition) // ------- PRINTS ZERO
print(yPostion) // ------- PRINTS ZERO
let circle = UIView(frame: CGRect(x: xPosition, y: yPostion, width: 100, height: 100))
circle.backgroundColor = UIColor.green
self.view2ForHomeController.middleView.addSubview(circle)
var progressCircle = CAShapeLayer()
progressCircle.frame = self.view.bounds
let lineWidth: CGFloat = 10
let rectFofOval = CGRect(x: lineWidth / 2, y: lineWidth / 2, width: circle.bounds.width - lineWidth, height: circle.bounds.height - lineWidth)
let circlePath = UIBezierPath(ovalIn: rectFofOval)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.white.cgColor
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.lineWidth = 10.0
progressCircle.frame = self.view.bounds
progressCircle.lineCap = CAShapeLayerLineCap(rawValue: "round")
circle.layer.addSublayer(progressCircle)
circle.transform = circle.transform.rotated(by: CGFloat.pi/2)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.1
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
progressCircle.add(animation, forKey: nil)
Frame is in terms of superview/superlayer coordinates. If you are going to say
circle.layer.addSublayer(progressCircle)
then you must give progressCircle a frame in terms of the bounds of circle.layer.
As for centering, in general, to center a sublayer in its superlayer you say:
theSublayer.position = CGPoint(
x:theSuperlayer.bounds.midX,
y:theSuperlayer.bounds.midY)
And to center a subview in its superview you say:
theSubview.center = CGPoint(
x:theSuperview.bounds.midX,
y:theSuperview.bounds.midY)
The bounds are the layer/view's coordinates in its local coordinate system. The frame is the layer/view's coordinates in its parents coordinate system. Since layers do not participate in auto layout, you should implement UIViewController.viewDidLayoutSubviews (or UIView.layoutSubLayers) and set the frame of the layer to the bounds of its super layer's view (the backing layer of a UIView essentially is the CoreGraphics version of that view).
This also means you need to recompute your path in they layout method. if that is expensive then draw your path in unit space and use the transform of the layer to scale it up to the view's dimensions instead.
The solution is to first add the subviews and then create the entire circular progress view at
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
// Add and position the circular progress bar and its layers here
}

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

Resources