Why is my SKShapeNode(s) reversed? - swift

I'm training my SpritKit skills, but I can't figure out why my SKShapeNode are "reversed", I think I'm missing something.
I was trying example from this question: Question
So, I tried the example with 2 rounded corners, and another example which is a half-circle.
I don't understand why the two corners curved are not the one specified on the UIBezierPath object.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let rectangle = CGRect(x: 0, y: 0, width: 400, height: 400)
let path = UIBezierPath(roundedRect: rectangle, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 50, height: 50))
let shape = SKShapeNode(path: path.cgPath, centered: true)
shape.lineWidth = 3
addChild(shape)
}
}
And why, my half circle are not oriented in the good direction either.
While in the documentation - Apple documentation - It says:
For example, specifying a start angle of 0 radians, an end angle of π radians, and setting the clockwise parameter to true draws the bottom half of the circle.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let path = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 200, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
let shape = SKShapeNode(path: path.cgPath, centered: true)
shape.lineWidth = 3
addChild(shape)
}
}
Finally, can some one explain me how the array of 'corners' is considered as a mask?

Related

UIView With Pointed Edges

I am trying to make a UIView with pointed edges like this. Did some searching around and found some questions with slanting just one edge like this one but can't find an answer with intersecting (points) edges like the one in the picture that dynamically sizes based on the UIView height.
I used Rob's answer to create something like this:
#IBDesignable
class PointedView: UIView
{
#IBInspectable
/// Percentage of the slant based on the width
var slopeFactor: CGFloat = 15
{
didSet
{
updatePath()
}
}
private let shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 0
// with masks, the color of the shape layer doesn’t matter;
// it only uses the alpha channel; the color of the view is
// dictate by its background color
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
override func layoutSubviews()
{
super.layoutSubviews()
updatePath()
}
private func updatePath()
{
let path = UIBezierPath()
// Start from x = 0 but the mid point of y of the view
path.move(to: CGPoint(x: 0, y: bounds.midY))
// Calculate the slant based on the slopeFactor
let slantEndX = bounds.maxX * (slopeFactor / 100)
// Create the top slanting line
path.addLine(to: CGPoint(x: slantEndX, y: bounds.minY))
// Straight line from end of slant to the end of the view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
// Straight line to come down to the bottom, perpendicular to view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
// Go back to the slant end position but from the bottom
path.addLine(to: CGPoint(x: slantEndX, y: bounds.maxY))
// Close path back to where you started
path.close()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}
}
The end result should give you a view close to what you which can be modified on the storyboard
And can also be created using code, here is a frame example since the storyboard showed its compatibility with autolayout
private func createPointedView()
{
let pointedView = PointedView(frame: CGRect(x: 0,
y: 0,
width: 200,
height: 60))
pointedView.backgroundColor = .red
pointedView.slopeFactor = 50
pointedView.center = view.center
view.addSubview(pointedView)
}

WRONG LINE LENGHT shapeLayer.strokeEnd = 0.5 draws more than half of the circle [duplicate]

This question already has an answer here:
Stroke of CAShapeLayer stops at wrong point?
(1 answer)
Closed 1 year ago.
I want the line to end at the top with shapeLayer.strokeEnd = 1.0 and get a circle. the line must end here
but she goes on, it just can't be seen
full circle
when i specify a value of 0.5 i want to get half of the circle but i get much more
half circle
My code:
View
public func createCircleLine(progress: CGFloat, color: UIColor, width: CGFloat) {
let radius = (min(bounds.width, bounds.height) - circleLineWidth) / 2
let center = min(bounds.width, bounds.height) / 2
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: center, y: center),
radius: radius,
startAngle: -CGFloat.pi / 2,
endAngle: 2 * CGFloat.pi,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.fillColor = nil
shapeLayer.strokeColor = circleProgressLineColor.cgColor
shapeLayer.lineWidth = circleLineWidth
shapeLayer.lineCap = .round
shapeLayer.strokeEnd = progress
layer.addSublayer(shapeLayer)
}
ViewController
class ViewController: UIViewController {
#IBOutlet weak var progressView: CircleProgressView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(false)
progressView.createCircleLine(progress: 1.0, color: .green, width: 10)
} }
I don’t understand why I can’t get the correct line length, the coordinates are correct
can i get the correct line length without CABasicAnimation ()?
You need to change the endAngle of your circle to 3 * CGFloat.pi / 2, so it is a complete cycle with no overlapping. The current circle you have has π/2 (90 degrees) overlap
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: center, y: center),
radius: radius,
startAngle: -CGFloat.pi / 2,
endAngle: 3 * CGFloat.pi / 2,
clockwise: true)

How to cut edges of a uiview in swift

I attached the View image. I want to achieve the small cut in bottom between the buy button and above flight information view.
I think the easiest way would be to create 2 circles as plain UIView instances and set their center as the left and right edges of the parent view respectively.
Since you set clipsToBounds to true, they will be clipped and only half of them will be visible on the screen.
public class TestView: UIView {
private let leftCircle = UIView(frame: .zero)
private let rightCircle = UIView(frame: .zero)
public var circleY: CGFloat = 0
public var circleRadius: CGFloat = 0
public override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
addSubview(leftCircle)
addSubview(rightCircle)
}
public override func layoutSubviews() {
super.layoutSubviews()
leftCircle.frame = CGRect(x: -circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
leftCircle.layer.masksToBounds = true
leftCircle.layer.cornerRadius = circleRadius
rightCircle.frame = CGRect(x: bounds.width - circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
rightCircle.layer.masksToBounds = true
rightCircle.layer.cornerRadius = circleRadius
}
}
I've created a sample project demonstrating that. Here is how it looks in my simulator (iPhone SE 11.2):
I had to do this with shadows. I tried creating a layer and subtracting another layer from it using evenOdd fillRule, however that didn't work since I need a specific path for shadows and evenOdd applies to the filling in the path instead.
In the end I just created the path manually
func setShadowPath() {
let path = UIBezierPath()
path.move(to: bounds.origin)
path.addLine(to: CGPoint(x: cutoutView.frame.minX, y: bounds.minY))
path.addArc(withCenter: CGPoint(x: cutoutView.frame.midX, y: bounds.minY),
radius: cutoutView.bounds.width/2, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
layer.shadowPath = path.cgPath
}
I created a "cutoutView" in my xib so I could trace around it easily.
That makes the shadow the correct shape, and then in order to create the cut itself I just created a layer using that same path
func setupBackground() {
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = layer.shadowPath
backgroundLayer.fillColor = UIColor.white.cgColor
layer.insertSublayer(backgroundLayer, at: 0)
}

Rounded rectangle with SKShapeNode

I have the following code that rounds an already existing rectangle from the scene builder...
btOk.path = UIBezierPath(roundedRect: CGRect(x: -62.5,y:-25,width: 125, height: 50), cornerRadius: 10).cgPath
However, if i try to use the same init to round the corners of another rectangle that is much larger, it does not even come close to working. It just makes the width HUUUUGE (imagine Trump).
scene.enumerateChildNodes(withName: "//*"){
node,stop in
if let name = node.name{
if name.contains("round"){
if let shapeNode = node as? SKShapeNode{
print(shapeNode.frame.width) //500
shapeNode.path = UIBezierPath(roundedRect: CGRect(x: -250,y:-100,width: 500, height: 200), cornerRadius: 50).cgPath
print(shapeNode.frame.width) //5735
}
}
}
}
btOk is a SKShapeNode as well. What am i missing between the two that is so different? One thing to note is i am enumerating through the children of the scene like this because this scene is in a SKReferenceNode. Perhaps that has something to do with it?
EDIT
Taking direction from #Ron Myschuk, i've solved this and since it's such a PITA, i also created an extension. So now i can round the corners of any SKShapeNode very easily when needed. Even if it was created in the scene editor. Note, this should only be used if there are no children of the shape node. Otherwise those children will be removed also.
extension SKShapeNode {
func roundCorners(topLeft:Bool,topRight:Bool,bottomLeft:Bool,bottomRight:Bool,radius: CGFloat,parent: SKNode){
let newNode = SKShapeNode(rect: self.frame)
newNode.fillColor = self.fillColor
newNode.lineWidth = self.lineWidth
newNode.position = self.position
newNode.name = self.name
self.removeFromParent()
parent.addChild(newNode)
var corners = UIRectCorner()
if topLeft { corners = corners.union(.bottomLeft) }
if topRight { corners = corners.union(.bottomRight) }
if bottomLeft { corners = corners.union(.topLeft) }
if bottomRight { corners = corners.union(.topRight) }
newNode.path = UIBezierPath(roundedRect: CGRect(x: -(newNode.frame.width / 2),y:-(newNode.frame.height / 2),width: newNode.frame.width, height: newNode.frame.height),byRoundingCorners: corners, cornerRadii: CGSize(width:radius,height:radius)).cgPath
}
}
And use it like so...
aShapeNode.roundCorners(topLeft: true, topRight: true, bottomLeft: false, bottomRight: false, radius: 5,parent: popup)
Not what you're going to want to hear but it's because you cannot set the width of an SKShapeNode in the Scene editor (To my knowledge). In order to get that ShapeNode to have a width of 500 you would have had to adjust the xScale. The xScale then reapplies itself to the path when you adjust it (kind of growing exponentially). If you create the SKShapeNode in code there is no problem adjust the rounded corners
let round = SKShapeNode(rect: CGRect(x: 0, y: -150, width: 500, height: 200))
round.fillColor = .red
addChild(round)
print(round.frame.width)
round.path = UIBezierPath(roundedRect: CGRect(x: -250, y: -100, width: 500, height: 200), cornerRadius: 50).cgPath
print(round.frame.width)
Edit
If you have your heart set on using the Scene editor you can place your ShapeNode and stretch it to where you want it then you could just do a small conversion in code to get the results that you want
if let round = self.childNode(withName: "biground") as? SKShapeNode {
let biground = SKShapeNode(rect: round.frame)
biground.fillColor = round.fillColor
biground.position = round.position
addChild(biground)
round.removeFromParent()
print(biground.frame.width)
biground.path = UIBezierPath(roundedRect: CGRect(x: -250, y: -100, width: 500, height: 200), cornerRadius: 50).cgPath
print(biground.frame.width)
}
this just recreates the shape in code based on what you outlined in the Scene editor and rounds the edges perfectly
edit 2
I've always been under the impression that SKShapeNodes are really inefficient (i'm pretty sure they leaked memory as well). So i always setup my round rectangles as so.
let outsideTexture = SKTexture(imageNamed: "round_square_white")
let outside = SKSpriteNode(texture: outsideTexture)
let insetX: CGFloat = 20
let insetY: CGFloat = 20
let cellSize = CGSize(width: 500.0, height: 500.0)
outside.centerRect = CGRect(x: CGFloat(insetX / outside.size.width), y: CGFloat(insetY / outside.size.height), width: CGFloat((outside.size.width - insetX * 2) / outside.size.width), height: CGFloat((outside.size.height - insetY * 2) / outside.size.height))
//outside.position = CGPointMake(gameModel.gameWidth / 2, (outside.size.height) / 2);
outside.xScale = cellSize.width / outside.size.width
outside.yScale = cellSize.height / outside.size.height
outside.zPosition = 10
outside.position = CGPoint(x: CGFloat(0), y: CGFloat(-0.35 * self.size.height / 2))
self.addChild(outside)
worth noting that this lays out a rounded square/rectangle perfectly however similar to the scale issues from the scene editor you have to place an empty cell over this to add children to, otherwise they scale to the rounded square.

Cut a circle out of a UIView using mask [duplicate]

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 6 years ago.
In my app I have a square UIView and I want to cut a hole/notch out of the top of. All the tutorials online are all the same and seemed quite straightforward but every single one of them always delivered the exact opposite of what I wanted.
For example this is the code for the custom UIView:
class BottomOverlayView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawCircle()
}
fileprivate func drawCircle(){
let circleRadius: CGFloat = 80
let topMidRectangle = CGRect(x: 0, y: 0, width: circleRadius*2, height: circleRadius*2)
let circle: CAShapeLayer = CAShapeLayer()
circle.position = CGPoint(x: (frame.width/2)-circleRadius, y: 0-circleRadius)
circle.fillColor = UIColor.black.cgColor
circle.path = UIBezierPath(ovalIn: topMidRectangle).cgPath
circle.fillRule = kCAFillRuleEvenOdd
self.layer.mask = circle
self.clipsToBounds = true
}
}
Here is what I hope to achieve (the light blue is the UIView, the dark blue is the background):
But here is what I get instead. (Every single time no matter what I try)
I'm not sure how I would achieve this, aside from making a mask that is already the exact shape that I need. But if I was able to do that then I wouldn't be having this issue in the first place. Does anyone have any tips on how to achieve this?
EDIT: The question that this is supposedly a duplicate of I had already attempted and was not able to get working. Perhaps I was doing it wrong or using it in the wrong context. I wasn't familiar with any of the given methods and also the use of pointers made it seem a bit outdated. The accepted answer does a much better job of explaining how this can be implemented using much more widely used UIBezierPaths and also within the context of a custom UIView.
I'd suggest drawing a path for your mask, e.g. in Swift 3
// BottomOverlayView.swift
import UIKit
#IBDesignable
class BottomOverlayView: UIView {
#IBInspectable
var radius: CGFloat = 100 { didSet { updateMask() } }
override func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
private func updateMask() {
let path = UIBezierPath()
path.move(to: bounds.origin)
let center = CGPoint(x: bounds.midX, y: bounds.minY)
path.addArc(withCenter: center, radius: radius, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
Note, I tweaked this to set the mask in two places:
From layoutSubviews: That way if the frame changes, for example as a result of auto layout (or by manually changing the frame or whatever), it will update accordingly; and
If you update radius: That way, if you're using this in a storyboard or if you change the radius programmatically, it will reflect that change.
So, you can overlay a half height, light blue BottomOverlayView on top of a dark blue UIView, like so:
That yields:
If you wanted to use the "cut a hole" technique suggested in the duplicative answer, the updateMask method would be:
private func updateMask() {
let center = CGPoint(x: bounds.midX, y: bounds.minY)
let path = UIBezierPath(rect: bounds)
path.addArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
let mask = CAShapeLayer()
mask.fillRule = .evenOdd
mask.path = path.cgPath
layer.mask = mask
}
I personally find the path within a path with even-odd rule to be a bit counter-intuitive. Where I can (such as this case), I just prefer to just draw the path of the mask. But if you need a mask that has a cut-out, this even-odd fill rule approach can be useful.