I am working on a handwriting app and I am using three different UIBezierPath variables:
[paths] - an array of UIBezierPath's
temporaryPath - to help with smoothing
realPath - the path that will ultimately be added to the 'paths' array
I am using an array of paths rather than a single path because I have a pan tool to move the lines around the screen - so they need to be separate entities. The problem with this is that I have to redraw the whole array every time draw(:_) is called:
override func draw(_ rect: CGRect) {
strokeColor.setStroke()
for path in paths{
path.stroke()
}
realPath?.stroke()
temporaryPath?.stroke()
}
This is creating performance issues after drawing around 20 paths. Is there any solution to not redrawing the whole array every time draw(:_) is called?
I worked out that by only passing in the rectangle of the path I am drawing to setNeedsDisplay(), in conjunction with a condition that only draws the lines in the array when touchesEnded(_:), is an OK solution. However, other apps with similar functionality obviously don't do this so I assume there is a better solution.
override func draw(_ rect: CGRect) {
strokeColor.setStroke()
realPath?.stroke()
if yesnewline == true{
for path in paths{
path.stroke()
}
yesnewline=false
}
temporaryPath?.stroke()
}
Related
I have a Canvas UIView as below. (Following https://youtu.be/E2NTCmEsdSE)
class Canvas: UIView {
override func draw(_ rect: CGRect){
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
// Other codes
context.setLineWidth(10)
context.setLineCap(.round)
context.strokePath()
}
// Other codes
}
Looks like I can only setLineWidth and setLineCap inside the draw function. That means whenever a draw happens, they will get set again and again.
I wonder, is there a way for me to set the drawing attribute once per launch and not to set over and over again?
AFAIK UIKit uses contexts to draw almost everything, changing attributes globally would have a huge impact.
What you could do is to extend CGContext to either set the attributes you want or perform the stroke with the attributes you want.
extension CGContext {
func applyAppStyle() {
context.setLineWidth(10)
context.setLineCap(.round)
}
func strokeWithAppStyle() {
context.setLineWidth(10)
context.setLineCap(.round)
context.strokePath()
}
}
As you've been told, you can certainly move the multiple lines of code off to a utility function if you don't like having them march down the page inside the draw method.
Another possibility is to construct, in advance, in code, a UIImage (by drawing into its context), and then just draw that into the view context in the draw method. Surprisingly, though, that is less efficient than what you are doing, because it means storing all the pixels and then copying them all into the context at draw time.
The important thing to understand is that your code is correct, as it stands. What you are doing is what you do. Your code is not inefficient or slow; it may seem verbose, but that's just the way things are. Drawing commands are highly optimized; drawing is, after all, the very essence of app display.
Keep in mind that the context does not necessarily persist, and that calls to draw are not particularly frequent (indeed, it is not uncommon for a view to be told to draw just once in the lifetime of the app).
So just obey the rules: when you are told to draw, do the complete drawing.
Im trying to make an app which draws a fractal tree. I managed to make the code that generates all the start and end points from all the lines. I also managed to draw the lines but right now they are really boxy and want them to have rounded corners.
I using a UIView and using UIBezierPaths to draw the lines inside the view draw function. To retrieve the points I have an array of Branch objects inside a sigleton class. A branch object has among other things a startingPoint and a ending point which are both tuples( (x: Double, y: Double) ).
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setStroke()
for branch in Tree.shared.branches{
let path = UIBezierPath()
print(branch.startingPoint)
print(branch.endingPoint)
path.move(to: CGPoint(x: branch.startingPoint.x, y: branch.startingPoint.y))
path.addLine(to: CGPoint(x: branch.endingPoint.x, y: branch.endingPoint.y))
path.lineWidth = 3
path.stroke()
}
}
How could i make the corners rounded?
Also if someone knows a free library that could facilitate this Im also interested.
edit: Im not interested in how to generate the tree, I have done already done this part of the code I need help with drawing the lines.
You don't need a library, you just need to spend a little time learning how to draw curves with UIBezierPath, and curves are one of the things that that class is best at. A key to drawing curves is understanding how control points work. Here's an answer I wrote some time ago about how to smoothly connect curved lines, which I think will help. Play around with -addCurveToPoint:controlPoint1:controlPoint2:.
If you don't actually want curves, but really just want the corners to be rounded rather than pointy, then all you need to do is to set the lineJoinStyle to kCGLineJoinRound.
About rounded corners of some view, is just set the parameters below:
func adjustView(_ view: UIView){
view.layer.borderWidth = 2.0
view.layer.borderColor = UIColor.red
view.layer.cornerRadius = 10
view.clipsToBounds = true
view.backgroundColor = UIColor.white
}
If you wish more information check the current documentation about layer property:
https://developer.apple.com/documentation/uikit/uiview/1622436-layer
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.
I override draw rect in a custom UIView class:
override func draw(_ rect: CGRect) { ...
Then, I draw into it like this:
func drawWithGradient(path:UIBezierPath,linearGradient:LinearGradientAttributeSet) {
let context = self
context.saveGState()
path.addClip()
context.drawLinearGradient(
linearGradient.gradient,
start:linearGradient.start,
end:linearGradient.end,
options:linearGradient.options
)
context.restoreGState()
}
And that works great, the UIView fits the size of the UIBezierPath perfectly.
But, I wanted a bit more space above the path, to draw some more things. So, I moved the path down using an affine transform. (This is necessary because there are a lot of images and the amount of space will vary.)
let shiftdownAffine = CGAffineTransform(translationX: 0, y: roomAbove)
thePath.apply(shiftdownAffine)
I didn't change anything else, but the magic was gone. Although I passed the new (translated) path into drawWithGradient, the customUIView matches the size of the original UIBezierPath, while I expected it to behave exactly as though I changed the original UIBezierPath directly through drawing code. With the translated UIBezierPath, the path is drawn at the new location, but the size of the UIView is the same, the so the path gets cut off.
Can you explain why this is happening and/or how to make a translated path that would be treated the same as a drawn path?
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.