Weird behaviour touch ended called automatically - ios

I am rendering the current screen (map screen) into a smaller view with magnifying effect.
Problem illustration
My app taking 110 MB memory. As soon as I start dragging the annotation, memory reaches to 130-140 MB then touchesEnded is called forcefully by system.
Expected behaviour
App should not call touchesEnded automatically.
What I did in dragging of annotation is added a magnified view into smaller view. Below is my code of draw method.
public override func draw(_ rect: CGRect) {
UIGraphicsBeginImageContextWithOptions(self.magnifiedView!.bounds.size, false, 0.0);
guard let context = UIGraphicsGetCurrentContext() else { return }
context.translateBy(x: radius, y: radius)
context.scaleBy(x: scale, y: scale)
context.translateBy(x: -magnifiedPoint.x, y: -magnifiedPoint.y)
removeFromSuperview()
self.subviews.first?.removeFromSuperview()
magnifiedView?.layer.render(in: context)
let mapImage = UIGraphicsGetImageFromCurrentImageContext();
self.addSubview(UIImageView(image: mapImage))
magnifiedView?.addSubview(self)
UIGraphicsEndImageContext();
}
As soon as the user starts dragging the annotation I started calling setNeedDisplay which triggers the draw method again and again. I don't know what to do now. I am totally stuck at this point.

The biggest issue you have is that you shouldn't be overriding draw at all. All this code does is update an image view's image property. draw is not the correct place for this. draw should only be used to actually draw content into self. It shouldn't be used to update subviews. It shouldn't add or remove views. It definitely shouldn't be used to remove self from the view hierarchy.
Certainly the call to removeFromSuperview() is not helping. That is likely why the drag is being cancelled. You need to leave the view in place.
Leave the image view in place. Leave this view in place. Generate the new image and update the image view's image property with the new image. Don't remove any views. Don't add any views during that process.
Put the code to generate and update the new image into a function that you can call as the drag operation moves around. This could be in the handler for a pan gesture recognizer if that's what you are using. Or it could be in touchesMoved(with:) if you are overriding the various touches... methods.

Related

draw#rect core graphics, on top of a layer?

Say you have this in a UIView,
override func draw(_ rect: CGRect) {
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(10.0)
c?.move(to: CGPoint(x: 10.0, y: 10.0))
c?.addLine(to: CGPoint(x: 40.0, y: 40.0))
... lots of complicated drawing here, sundry paths, colors, widths etc ...
c?.strokePath()
}
Of course, it will draw the hell out of your drawing for you.
But say in the same UIView you do this ...
func setup() { // in inits, layout
if nil etc {
thingLayer = CAShapeLayer()
self.layer.insertSublayer(thingLayer, at: 0)
thingLayer.fillColor = UIColor.yellow.cgColor }
thingLayer.frame = bounds
let path = ... some fancy path
thingLayer.path = path.cgPath
}
Indeed, the new yellow layer is drawn over the drawing in draw#rect.
How do you draw - using core graphics commands - either on to thingLayer, or perhaps on to another layer on top of all??
Core graphics commands:
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(10.0)
c?.move(to: CGPoint(x: 10.0, y: 10.0))
c?.addLine(to: CGPoint(x: 40.0, y: 40.0))
c?.strokePath()
seem to draw to a place directly above or on the main .layer
(Well that appears to be the case, as far as I can see.)
How do you draw - using core graphics commands - either on to thingLayer, or perhaps on to another layer on top of all??
Surely in draw#rect you can specify which cgLayer to draw to?
In draw#rect, can you make another cgLayer and draw to context, that cgLayer?
Bottom line, the draw(_:) is for rendering the root view of a UIView subclass. If you add sublayers, they will be rendered on top of whatever is done in the root view’s draw(_:).
If you have a few paths currently performed in the draw(_:) that you want to render on top of the sublayer that you’ve added, you have a few options:
Move the paths to be stroked in their own CAShapeLayer instances and add them above the other sublayer that you’ve already added.
Consider the main UIView subclass as a container/content view, and add your own private UIView subviews (potentially with the “complicated” draw(_:) methods). You don’t have to expose these private subviews if you don’t want to.
Move the complicated drawing to the draw(in:) of a CALayer subclass and, again, add this sublayer above the existing sublayer that you’ve already created.
Given that the thingLayer is, effectively, only setting the background color, you could just set the background color of the view and not use a layer for this background color at all.
The CALayer hierarchy is a tree, where a root layer is first (e.g. the layer that comprises the background of a window), followed by its sublayers (an array of CALayer objects, stored and drawn in in back-to-front order), followed by their sublayers, and so on, recursively.
Your implementation of UIView.draw(_ rect: CGRect) is defining how the main layer of your view (self.layer) is drawn.
By adding your CAShapeLayer as a sublayer of self.layer, you're making it be draw after self.layer, which has the effect of it being drawn above self.layer, which contains the line you drew in UIView.draw(_ rect: CGRect).
To resolve this, you need to put your stroke in sublayer after your thingLayer.
self.layer.insertSublayer(thingLayer, at: 0)
self.layer.insertSublayer(lineLayer, above: thingLayer)
By the way, you forgot to delegate to super.draw(rect). It's not necessary for direct subclasses of UIView (since the base implementation doesn't do anything), but it is necessary for every other case, and it's generally a good habit to get into (lest you run into really obscure/frustrating drawing bugs).
If your yellow shape layer doesn't change or move around independently, you could get rid of the CAShapeLayer and just draw the shape yourself at the top of your implementation of draw(_:):
let path = // your fancy UIBezierPath
UIColor.yellow.setFill()
path.fill()
Otherwise, as other commenters have said, you'll need to move your drawing code into additional custom subclasses of UIView or CALayer, and stack them in the order you want.

How to animate drawing in Swift, but also change a UIImageView's scale?

I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:
func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations) // do some math to figure the best scale
UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale) // some animation prohibits changes!
// ... drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values
Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.
You'll need a special layer:
class MyLayer : CALayer {
#objc var spirality : CGFloat = 0
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(spirality) {
return true
}
return super.needsDisplay(forKey:key)
}
override func draw(in con: CGContext) {
print(self.spirality) // in real life, this is our signal to draw!
}
}
The layer must actually be in the interface, though it can be impossible for the user to see:
let lay = MyLayer()
lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.view.layer.addSublayer(lay)
Subsequently, we can initialize the spirality of the layer:
lay.spirality = 2.0
lay.setNeedsDisplay() // prints: 2.0
Now when we want to "animate" the spirality, this is what we do:
let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
ba.fromValue = lay.spirality
ba.toValue = 2.75
ba.duration = 1
lay.add(ba, forKey:nil)
CATransaction.setDisableActions(true)
lay.spirality = 2.75
The console shows the arrival of a succession of intermediate values over the course of 1 second!
2.03143266495317
2.04482554644346
2.05783333256841
2.0708108600229
2.08361491002142
2.0966724678874
2.10976020619273
2.12260236591101
2.13551922515035
2.14842618256807
2.16123360767961
2.17421661689878
2.18713565543294
2.200748950243
2.21360073238611
2.2268518730998
2.23987507075071
2.25273013859987
2.26560932397842
2.27846492826939
2.29135236144066
2.30436328798532
2.31764804571867
2.33049770444632
2.34330793470144
2.35606706887484
2.36881992220879
2.38163591921329
2.39440815150738
2.40716737508774
2.42003352940083
2.43287514150143
2.44590276479721
2.45875595510006
2.47169743478298
2.48451870679855
2.49806520342827
2.51120449602604
2.52407149970531
2.53691896796227
2.54965999722481
2.56257836520672
2.57552136480808
2.58910304307938
2.60209316015244
2.6151298135519
2.62802086770535
2.64094598591328
2.6540260463953
2.6669240295887
2.6798157542944
2.69264766573906
2.70616912841797
2.71896715462208
2.73285858333111
2.74564798176289
2.75
2.75
2.75
Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.
Personally, in more complicated animations I would use lottie the animation itself is built with Adobe After Effect and exported as a JSON file which you will manage using the lottie library this approach will save you time and effort when you port your app to another platform like Android as they also have an Android Lottie which means the complicated process of creating the animation is only done once.
Lottie Files has some examples animations as well for you to look.
#Matt provided the answer and gets the checkmark. I'll recap some points for emphasis:
UIView animation is great for commonly animated properties, but if
you need to vary a property not on UIView's animatable list, you can't use it. You must
create a new CALayer and add a CABasicAnimation(keyPath:) to it.
I tried but was unable to get my CABasicAnimations to fire by adding them to the default UIView.layer. I needed to create a custom CALayer
sublayer to the UIView.layer - something like
myView.layer.addSublayer(myLayer)
Leave the custom sublayer installed and re-add the CABasicAnimation to that sublayer when (and only when) you want to animate drawing.
In the custom CALayer object, be sure to override class func needsDisplay(forKey key: String) -> Bool with your key property (as #Matt's example shows), and also override func draw(in cxt: CGContext) to do your drawing. Be sure to decorate your key property with #objc. And reference the key property within the drawing code.
A "gotcha" to avoid: in the UIView object, be sure to null out the usual draw method (override func draw(_ rect: CGRect) { }) to avoid conflict between animated and non-animated drawing on the separate layers. For coordinating animated and non-animated content in the same UIView, it's good (necessary?) to do all your drawing from your custom layer.
When doing that, use myLayer.setNeedsDisplay() to update the non-animated content within the custom layer; use myLayer.add(myBasicAnimation, forKey:nil) to trigger animated drawing within the custom layer.
As I said above, #Matt answered - but these items seemed worth emphasizing.

How to clear and redraw contents of a UIView (present inside a view controller) with swift?

I've a UIView inside a View Controller in which I'm drawing few lines as required by my app. After a certain point of time, I want some of those lines to disappear and a few other to appear in the same view. Approach I'm using as of now is that I'm clearing the UIView and redrawing all the lines I want to draw in the updated view.
Can somebody tell me what's the right way to go about it? I've gone through various questions that sound similar but it hasn't helped much. Till now I've tried things like:-
outletView.setNeedsDisplay()
and
let context = UIGraphicsGetCurrentContext()
context?.clear(outletView.frame)
None of these seem to make any difference.
If I call viewDidLoad() again since all the lines are updated now. New lines to be drawn come up but the ones that were supposed to disappear don't go away. Variables for lines are updated correctly since other logic I have which checks line variable's values is working fine after the update is supposed to happen. Only problem is with the redraw part. In fact, if I understand this correctly, problem is only with cleaning the old uiview contents. If cleaning happens properly, redraw with viewDidLoad will show correct lines drawn.
P.S. I know that calling viewDidLoad() explicitly isn't a good practice. Hope to find a solution to this problem without having to call viewDidLoad again.
Maybe you could draw your lines in different layers of the view, delete the layer containing the lines that need to disappear and create a new layer for the new lines. You can draw in layoutSubviews() and use self.setNeedsLayout() when you need to update the view.
remove:
guard let sublayers = yourView.layer.sublayers else { return }
for layer in sublayers {
layer.removeFromSuperlayer()
}
add:
let linesPath = UIBezierPath()
let linesLayer = CAShapeLayer()
linesPath.move(to: CGPoint(x: 0, y: 0)
linesPath.addLine(to: CGPoint(x: 50, y: 100)
lineLayer.path = linesPath.cgPath
linesLayer.lineWidth = 1.0
linesLayer.strokeColor = UIColor.black
yourView.layer.addSublayer(linesLayer)

Drawing lines on top of CGImage

I am currently trying to develop a drawing app. The problem that I've come across is optimising the app's performance.
I have subclassed an UIView, in which I am detecting user's inputs.
At first I tried to draw all the lines with CoreGraphics in draw(_ rect: CGRect) method but when the number of lines was 10 000+, the lag was very noticeable. So then I thought of creating a CGImage that I would make after every draw(...) cycle and then when the next draw cycle came, I would put just the new lines on top of that CGImage, therefore not drawing ALL previous lines again and saving precious time.
I am registering new lines in touchesBegan() and touchesMoved() methods and then I am calling self.needsDisplay().
This is the code that I am currently using:
var mainImage: CGImage?
var newLines: [Line]
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
//context?.translateBy(x: 0, y: bounds.height)
//context?.scaleBy(x: 1, y: -1)
if let image = mainImage {
context?.draw(image, in: bounds)
}
for line in newLines {
context?.setStrokeColor(UIColor.black.cgColor)
context?.setLineWidth(1)
context?.move(to: line.previousLocation)
context?.addLine(to: line.location)
context?.strokePath()
}
newLines = []
mainImage = context?.makeImage()
}
Now the problem with it is that the lines I draw are divided in two parts and are mirrored vertically. (Image split vertically). I have drawn here two continuous lines - one straight line and one curved line. The weird thing is that if I flipped half of the UIView, the lines would be aligned continuously with no space between them.
If I uncommented the two lines from my previously mentioned code (context?.translateBy(x: 0, y: bounds.height) and context?.scaleBy(x: 1, y: -1)), the image would look like this.
The only wrong thing now is that I was drawing on the lower side of the screen and getting results on the upper one.
How do I correct it? What is the proper solution for my problem?
Thank you very much!
When you comment out those two lines, then you are NOT making any changes to the graphics context. If you un-comment them, then you ARE changing the context at each call and not saving it. So I think you're essentially flipping it each time. Not sure if it's that but you should definitely be pushing/saving your context before drawing (and obviously restoring/popping it).
Check out this post: using UIImage drawInRect still renders it upside down.

Draw UIBezierPath outside of UIView::drawRect()?

I'm trying to draw a set of small circles around the edge of a view to mirror physical lights (pixels) I'm controlling in my app. These circles will change color frequently. So I've created a PixelSimulator object to draw each circle into a custom UIView object.
Here's the relevant code
class PixelSimulator {
let size: CGSize;
let color: UIColor;
let pixelPath: UIBezierPath;
init (point: CGPoint, size: CGSize, pixel: Pixel) {
self.point = point;
self.size = size;
self.pixel = pixel;
pixelRect = CGRect(origin: point, size: size);
pixelPath = UIBezierPath(ovalInRect: pixelRect);
color = pixel.color;
}
func render () {
UIGraphicsBeginImageContext(size);
color.setFill();
pixelPath.fill();
UIGraphicsEndImageContext();
}
}
I've tried a different approach using CGContextAddEllipseInRect(context, pixelRect) to no avail. I've also tried declaring the pixelPath inside of the render() method, also to no avail.
What do I need to change to get my bezierPath drawn onto the screen at any time, inside of drawRect() or out of it?
I'd remove the image context and just directly draw. I'd call render only from inside draw rect.
When the colour changes I'd update the appropriate simulators and post a notification to let the 'system' know that it happened. In this case a notification is appropriate because you don't want to know what is interested about the change event and multiple things in the system may be interested.
When the notification is received by your view controller it simply calls setNeedsDisplay on the view which will result in the simulators being rendered on the next draw cycle.

Resources