I am trying to make use of path animation feature in CAShapeLayer that is not available in SpriteKit and hence having to combine objects drawn suing CAShapeLayer with SpriteKit objects within the same view.
The coordinate system seems to be inverse: CAShapeLayer seems to have +ve y-axis pointing downwards, whereas SKScene has it pointing upwards.
Below is a simple XCODE playground that tries to draw a yellow line from 0,0 to 200,100 and shadow it with a thicker red line.
import SpriteKit
import PlaygroundSupport
let bounds = CGRect(x: 0, y: 0, width: 400, height: 200)
let view = SKView(frame: bounds)
view.backgroundColor = UIColor.lightGray
PlaygroundPage.current.liveView = view
// Create SK Scene
let scene = SKScene(size: CGSize(width: 400, height: 200))
scene.scaleMode = SKSceneScaleMode.aspectFit
view.presentScene(scene);
// Define the path
let path: CGMutablePath = CGMutablePath();
path.move(to: CGPoint(x:0, y:0))
path.addLine(to: CGPoint(x:200, y:100))
// Use CAShapeLayer to draw the red line
var pathLayer: CAShapeLayer = CAShapeLayer()
pathLayer.path = path
pathLayer.strokeColor = UIColor.red.cgColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 4.0
pathLayer.lineJoin = kCALineJoinBevel
pathLayer.zPosition = 1;
view.layer.addSublayer(pathLayer);
//Use SKShapeNode to draw the yellow line
let pathShape: SKShapeNode = SKShapeNode(path: path);
pathShape.strokeColor = .yellow;
pathShape.lineWidth = 1.0;
pathShape.zPosition = 10;
scene.addChild(pathShape);
I expected the yellow line to coincide with the red line. Whereas, the yellow and red lines appeared as mirror images.
Is there anyway to redefine the CAShapeLayer coordinate system to point +ve Y-axis upwards?
No, as written in the docs this is how it works, you may inverse the coordinates to match your requirements..
Sprite kit docs:
The unit coordinate system places the origin at the bottom left corner of the frame and (1,1) at the top right corner of the frame. A sprite’s anchor point defaults to (0.5,0.5), which corresponds to the center of the frame.
The rest of iOS:
iOS. The default coordinate system has its origin at the upper left of the drawing area, and positive values extend down and to the right from it. You cannot change the default orientation of a view’s coordinate system in iOS—that is, you cannot “flip” it.
I have an image which at full size is 1800 x 2400 pixels. It's being displayed in a UIImageView at 450 x 600.
I have attached a UIPinchGestureRecognizer and a UIRotationGestureRecognizer to the UIImageView which allows the user to zoom and rotate the image which is working great.
I now need to export the image at the original size (1800 x 2400) with the rotations and transforms and I'm struggling to get this to work.
My code so far:
if let imageView = imageView, let image = imageView.image {
UIGraphicsBeginImageContext(image.size)
let context = UIGraphicsGetCurrentContext();
context?.translateBy(x: 0, y: image.size.height)
context?.scaleBy(x: 1.0, y: -1.0)
context?.rotate(by: -imageView.transform.b)
context?.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
translatedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
}
With the above, the rotation is correct but the image is drawn off center and I can't figure out how to include the zoom.
Graphics is very much my weak point in development and I've been at this for hours with no success.
Never zoom the image itself because it's time consuming and it's not the recommended way.
Use scrollView for zooming and you don't have to think about your image size.Put your large sized images in xcassets. ScrollView has delegate features for zooming. It has some beautiful properties too just like this.
scrollView.minimumZoomScale = 0.5;
scrollView.maximumZoomScale = 6.0;
So Better move your effort to UIScrollView and you will see you don't have to write so much code for that.
Introduction
I have a CGPath I create from a SVG file using PocketSVG API. It all works fine.
The Problem
The problem is that the shape stretches for some reason, please take a look on this picture (the blue color is just to make is more visible to you, please ignore it, it should be ClearColor):
The Target
What do I want to achieve? I want to achieve a shape that goes all over the screen's width (I don't care about the height, it should modify itself according to the width), and sticks to the bottom of the screen, please take a look on this picture as well (please ignore the circular button):
The Code
The important part ;)
I have a subclass of UIView that draws this shape from the SVG file, it called CategoriesBarView. Then, on my MainViewController (a subclass of UIViewController) I'm creating an object of CategoriesBarView and setting it programmatically as a subview.
CategoriesBarView:
class CategoriesBarView: UIView {
override func layoutSubviews() {
let myPath = PocketSVG.pathFromSVGFileNamed("CategoriesBar").takeUnretainedValue()
var transform = CGAffineTransformMakeScale(self.frame.size.width / 750.0, self.frame.size.height / 1334.0)
let transformedPath = CGPathCreateCopyByTransformingPath(myPath, &transform)
let myShapeLayer = CAShapeLayer()
myShapeLayer.path = transformedPath
let blur = UIBlurEffect(style: .Light)
let effectView = UIVisualEffectView(effect: blur)
effectView.frame.size = self.frame.size
effectView.frame.origin = CGPointMake(0, 0)
effectView.layer.mask = myShapeLayer
self.addSubview(effectView)
}
}
MainViewController:
override func viewDidLoad() {
super.viewDidLoad()
let testHeight = UIScreen.mainScreen().bounds.size.height / 6 // 1/6 of the screen’s height, that is the height in the target picture approximately, doesn’t it?
let categoriesBarView = CategoriesBarView(frame: CGRect(x: 0, y: UIScreen.mainScreen().bounds.size.height - testHeight , width: UIScreen.mainScreen().bounds.size.width, height: testHeight))
categoriesBarView.backgroundColor = UIColor.blueColor() // AS I said, it should be ClearColor
self.view.addSubview(categoriesBarView)
}
Does anyone of you know what is the problem here and why the shape is stretching like that? I'll really appreciate if someone could help me here.
Thank you very much :)
Consider following code which draws a Square of 100x100 dimension. What i have done here is taken 100x100 as a base dimension(Because its easy to calculate respective ratio or scale dimension), as you can see i have defined scaleWidth and scaleHeight variable which represents your current scale for path. Scale is 1.0 at the moment which means it draws a square of 100x100, if you change it to 0.5 and 0.75 respectively it will draw a rectangle of 50X75 pixels. Refer Images which clearly depicts difference between scale width and height as 1.0 and 0.5 and 0.75 respectively.
CGFloat scaleWidth = 0.50f;
CGFloat scaleHeight = 0.75f;
//// Square Drawing
UIBezierPath* bezierSquarePath = [UIBezierPath bezierPath];
// ***Starting point of path ***
[bezierSquarePath moveToPoint: CGPointMake(1, 1)];
// *** move to x and y position to draw lines, calculate respective x & y position using scaleWidth & scaleHeight ***
[bezierSquarePath addLineToPoint: CGPointMake(100*scaleWidth, 1)];
[bezierSquarePath addLineToPoint: CGPointMake(100*scaleWidth, 100*scaleHeight)];
[bezierSquarePath addLineToPoint: CGPointMake(1, 100*scaleHeight)];
// *** end your path ***
[bezierSquarePath closePath];
[UIColor.blackColor setStroke];
bezierSquarePath.lineWidth = 1;
[bezierSquarePath stroke];
Image 1 : Represents 100x100 square using scaleWidth = 1.0 and scaleHeight = 1.0
Image 2 : Represents 50x75 square using scaleWidth = 0.50 and scaleHeight = 0.75
Note: In given images all the drawing is done in UIView's - (void)drawRect:(CGRect)rect method as only UIView is capable to draw. I have placed a UIView which is highlighted with GrayColor in images.
I believe it gives you a perspective about scaling a path to solve your problem as you can not use the same code but you can generate one using it.
Helpful Tool : If you are not expert in Graphics coding you can recommend to use PaintCode software which generates Objective-C code with UI. Thought there might be other softwares you can opt for.
Happy coding :)
I've encountered a problem with code I'd written to cut off the corners of a UILabel (or, indeed, any UIView-derived object to which you can add sublayers) -- I do have to thank Kurt Revis for his answer to Use a CALayer to add a diagonal banner/badge to the corner of a UITableViewCell that pointed me in this direction.
I don't have a problem if the corner overlays a solid color -- it's simple enough to make the cut-off corner match that color. But if the corner overlays an image, how would you let the image show through?
I've searched SO for anything similar to this problem, but most of those answers have to do with cells in tables and all I'm doing here is putting a label on a screen's view.
Here's the code I use:
-(void)returnChoppedCorners:(UIView *)viewObject
{
NSLog(#"Object Width = %f", viewObject.layer.frame.size.width);
NSLog(#"Object Height = %f", viewObject.layer.frame.size.height);
CALayer* bannerLeftTop = [CALayer layer];
bannerLeftTop.backgroundColor = [UIColor blackColor].CGColor;
// or whatever color the background is
bannerLeftTop.bounds = CGRectMake(0, 0, 25, 25);
bannerLeftTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerLeftTop.position = CGPointMake(10, 10);
bannerLeftTop.affineTransform = CGAffineTransformMakeRotation(-45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerLeftTop];
CALayer* bannerRightTop = [CALayer layer];
bannerRightTop.backgroundColor = [UIColor blackColor].CGColor;
bannerRightTop.bounds = CGRectMake(0, 0, 25, 25);
bannerRightTop.anchorPoint = CGPointMake(0.5, 1.0);
bannerRightTop.position = CGPointMake(viewObject.layer.frame.size.width - 10.0, 10.0);
bannerRightTop.affineTransform = CGAffineTransformMakeRotation(45.0 / 180.0 * M_PI);
[viewObject.layer addSublayer:bannerRightTop];
}
I'll be adding similar code to do the BottomLeft and BottomRight corners, but, right now, those are corners that overlay an image. The bannerLeftTop and bannerRightTop are actually squares that are rotated over the corner against a black background. Making them clear only lets the underlying UILabel background color appear, not the image. Same for using the z property. Is masking the answer? Oo should I be working with the underlying image instead?
I'm also encountering a problem with the Height and Width being passed to this method -- they don't match the constrained Height and Width of the object. But we'll save that for another question.
What you need to do, instead of drawing an opaque corner triangle over the label, is mask the label so its corners aren't drawn onto the screen.
Since iOS 8.0, UIView has a maskView property, so we don't actually need to drop to the Core Animation level to do this. We can draw an image to use as a mask, with the appropriate corners clipped. Then we'll create an image view to hold the mask image, and set it as the maskView of the label (or whatever).
The only problem is that (in my testing) UIKit won't resize the mask view automatically, either with constraints or autoresizing. We have to update the mask view's frame “manually” if the masked view is resized.
I realize your question is tagged objective-c, but I developed my answer in a Swift playground for convenience. It shouldn't be hard to translate this to Objective-C. I didn't do anything particularly “Swifty”.
So... here's a function that takes an array of corners (specified as UIViewContentMode cases, because that enum includes cases for the corners), a view, and a “depth”, which is how many points each corner triangle should measure along its square sides:
func maskCorners(corners: [UIViewContentMode], ofView view: UIView, toDepth depth: CGFloat) {
In Objective-C, for the corners argument, you could use a bitmask (e.g. (1 << UIViewContentModeTopLeft) | (1 << UIViewContentModeBottomRight)), or you could use an NSArray of NSNumbers (e.g. #[ #(UIViewContentModeTopLeft), #(UIViewContentModeBottomRight) ]).
Anyway, I'm going to create a square, 9-slice resizable image. The image will need one point in the middle for stretching, and since each corner might need to be clipped, the corners need to be depth by depth points. Thus the image will have sides of length 1 + 2 * depth points:
let s = 1 + 2 * depth
Now I'm going to create a path that outlines the mask, with the corners clipped.
let path = UIBezierPath()
So, if the top left corner is clipped, I need the path to avoid the top left point of the square (which is at 0, 0). Otherwise, the path includes the top left point of the square.
if corners.contains(.TopLeft) {
path.moveToPoint(CGPoint(x: 0, y: 0 + depth))
path.addLineToPoint(CGPoint(x: 0 + depth, y: 0))
} else {
path.moveToPoint(CGPoint(x: 0, y: 0))
}
Do the same for each corner in turn, going around the square:
if corners.contains(.TopRight) {
path.addLineToPoint(CGPoint(x: s - depth, y: 0))
path.addLineToPoint(CGPoint(x: s, y: 0 + depth))
} else {
path.addLineToPoint(CGPoint(x: s, y: 0))
}
if corners.contains(.BottomRight) {
path.addLineToPoint(CGPoint(x: s, y: s - depth))
path.addLineToPoint(CGPoint(x: s - depth, y: s))
} else {
path.addLineToPoint(CGPoint(x: s, y: s))
}
if corners.contains(.BottomLeft) {
path.addLineToPoint(CGPoint(x: 0 + depth, y: s))
path.addLineToPoint(CGPoint(x: 0, y: s - depth))
} else {
path.addLineToPoint(CGPoint(x: 0, y: s))
}
Finally, close the path so I can fill it:
path.closePath()
Now I need to create the mask image. I'll do this using an alpha-only bitmap:
let colorSpace = CGColorSpaceCreateDeviceGray()
let scale = UIScreen.mainScreen().scale
let gc = CGBitmapContextCreate(nil, Int(s * scale), Int(s * scale), 8, 0, colorSpace, CGImageAlphaInfo.Only.rawValue)!
I need to adjust the coordinate system of the context to match UIKit:
CGContextScaleCTM(gc, scale, -scale)
CGContextTranslateCTM(gc, 0, -s)
Now I can fill the path in the context. The use of white here is arbitrary; any color with an alpha of 1.0 would work:
CGContextSetFillColorWithColor(gc, UIColor.whiteColor().CGColor)
CGContextAddPath(gc, path.CGPath)
CGContextFillPath(gc)
Next I create a UIImage from the bitmap:
let image = UIImage(CGImage: CGBitmapContextCreateImage(gc)!, scale: scale, orientation: .Up)
If this were in Objective-C, you'd want to release the bitmap context at this point, with CGContextRelease(gc), but Swift takes care of it for me.
Anyway, I convert the non-resizable image to a 9-slice resizable image:
let maskImage = image.resizableImageWithCapInsets(UIEdgeInsets(top: depth, left: depth, bottom: depth, right: depth))
Finally, I set up the mask view. I might already have a mask view, because you might have clipped the view with different settings already, so I'll reuse an existing mask view if it is an image view:
let maskView = view.maskView as? UIImageView ?? UIImageView()
maskView.image = maskImage
Finally, if I had to create the mask view, I need to set it as view.maskView and set its frame:
if view.maskView != maskView {
view.maskView = maskView
maskView.frame = view.bounds
}
}
OK, how do I use this function? To demonstrate, I'll make a purple background view, and put an image on top of it:
let view = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
view.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
let backgroundView = UIView(frame: view.frame)
backgroundView.backgroundColor = UIColor.purpleColor()
backgroundView.addSubview(view)
XCPlaygroundPage.currentPage.liveView = backgroundView
Then I'll mask some corners of the image view. Presumably you would do this in, say, viewDidLoad:
maskCorners([.TopLeft, .BottomRight], ofView: view, toDepth: 50)
Here's the result:
You can see the purple background showing through the clipped corners.
If I were to resize the view, I'd need to update the mask view's frame. For example, I might do this in my view controller:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.cornerClippedView.maskView?.frame = self.cornerClippedView.bounds
}
Here's a gist of all the code, so you can copy and paste it into a playground to try out. You'll have to supply your own adorable test image.
UPDATE
Here's code to create a label with a white background, and overlay it (inset by 20 points on each side) on the background image:
let backgroundView = UIImageView(image: UIImage(named: "Kaz-256.jpg"))
let label = UILabel(frame: backgroundView.bounds.insetBy(dx: 20, dy: 20))
label.backgroundColor = UIColor.whiteColor()
label.font = UIFont.systemFontOfSize(50)
label.text = "This is the label"
label.lineBreakMode = .ByWordWrapping
label.numberOfLines = 0
label.textAlignment = .Center
label.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
backgroundView.addSubview(label)
XCPlaygroundPage.currentPage.liveView = backgroundView
maskCorners([.TopLeft, .BottomRight], ofView: label, toDepth: 50)
Result:
We have a CALayer which shows image with edit information. Such as rotate, scale and translate. I want to make a CGPath which exactly match the layer bounds. When the layer rotate, my path should also rotate and its four corners should match the CALayer's four corners. The CGPath is actually used as a gray mask to show the clipped area of the image.
I try following code, but it does not work.
f = self.imageLayer.frame;
t = self.imageLayer.affineTransform;
CGPathAddRect(path, &t, f);
The CALayer has its own CGAffineTransform. All edit information are applied via the CGAffineTransform.
Any hint will be appreciated, Thanks a lot.
If I got your question right you could be using UIBezierPath's usesEvenOddFillRule to cut around your image layer bounds dimming the rest of the visible screen. Basically you create a very large rectangular path (for some reasons CGRectInfinite doesn't work here) and cut out the area around your image layer. The trick is to use kCAFillRuleEvenOdd which flips what is considered inside and outside.
Something like should work:
let cutPath = UIBezierPath(roundedRect: imageLayer.bounds, cornerRadius: 0)
let clipPath = UIBezierPath(rect: CGRectMake(-10e6, -10e6, 10e7, 10e7)) // CGRectInfinite isn't working here ?
clipPath.appendPath(cutPath)
clipPath.usesEvenOddFillRule = true
let shape = CAShapeLayer()
shape.contentsScale = UIScreen.mainScreen().scale
shape.lineWidth = 2
shape.fillColor = UIColor(red:0.1, green:0.1, blue:0.1, alpha:0.5).CGColor
shape.fillRule = kCAFillRuleEvenOdd
shape.strokeColor = UIColor.whiteColor().CGColor
shape.path = clipPath.CGPath
imageLayer.addSublayer(shape)
// do a transformations here
imageLayer.transform = CATransform3DMakeRotation(10.0, 1.0, 1.0, 0.8)
Which results