wondering use of computed property - swift

before.
class DrawingView: UIView {
var arcCenter = CGPoint(x: frame.midX, y: frame.midY) // error
}
after.
class DrawingView: UIView {
var arcCenter: CGPoint {
return CGPoint(x: frame.midX, y: frame.midY)
}
}
I know why that error occur, but I can't understand why the second codes are not makes error.

The first form is not a computed property.
It is a stored property with a default value.
it uses self which causes the error because at the moment the default value is going to be assigned the instance is not guaranteed to be instantiated.
An alternative to a computed property – which is computed only at the moment it's called – is a lazy instantiated property.
lazy var arcCenter: CGPoint = CGPoint(x: self.frame.midX, y: self.frame.midY)
Unlike the stored property the default value is assigned when the property is accessed the first time.

Related

Swift - After adding a custom Curve extension, it does not render programmatically created views, only views created with Interface Builder

I have an extension to curve the bottom edge of views since this styling is used on multiple screens in the app I am trying to create.
However, I have noticed I can only make it work with views that I have added trough interface builder. If I try to apply it on view created programmatically they do not render.
I have created a simple example to illustrate the problem. The main storyboard contains two viewControllers with a single colored view in the middle: one created with Interface Builder while the other programmatically.
In StoryboardVC, the view with the curve is rendered correctly without any problem. The setBottomCurve() method is used to create the curve.
If you compare this to setting the entry point to ProgrammaticVC, running the app you can see a plain white screen. Comment this line out to see the view appear again.
This is the extension used:
extension UIView {
func setBottomCurve(curve: CGFloat = 40.0){
self.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - curve
let curveTo:CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I expect ProgrammaticVC to look identical to StoryboardVC (except for the difference in color)
The example project can be found here:
https://github.com/belamatedotdotipa/CurveTest2
I suggest to create a subclass instead of using an extension, this is a specific behaviour.
In this case you cannot see the result expected, when you are adding the view programmatically, because in the viewDidLoad you don't have the frame of your view, in this example you can use the draw function:
class BottomCurveView: UIView {
#IBInspectable var curve: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
override func draw(_ rect: CGRect) {
setBottomCurve()
}
private func setBottomCurve() {
let rect = bounds
let y: CGFloat = rect.size.height - curve
let curveTo: CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}

didSet observer does not get called when initialising property

Given the following class,
class Spaceship {
var position: CGPoint! {
didSet {
node.position = position
}
}
var node: SKSpriteNode!
init(frame: CGRect) {
node = SKSpriteNode(
color: UIColor.red,
size: CGSize(
width: frame.size.width / 5,
height: frame.size.width / 5
)
)
self.position = CGPoint(x: frame.midX, y: frame.midY)
}
}
it looks as though the didSet observer of the position property does not get called.
Since the didSet observer should set the SpriteKit node's position when the position property gets modified, I thought that instead of using the same line of code contained within the didSet block, I could trigger the latter instead, but it doesn't seem to work; when the scene gets created (in the main GameScene, which simply creates a Spaceship objects and adds the spaceship.node to the scene's node children), the node's position seems to be 0; 0.
Any clue on why does this happen? Thank you.
Update: although the defer statement works, the assignment does not allow to make use of it.
Rather than using didSet to update your node, directly forward queries to it:
class Spaceship {
let node: SKSpriteNode
init(frame: CGRect) {
self.node = SKSpriteNode(
color: UIColor.red,
size: CGSize(
width: frame.size.width / 5,
height: frame.size.width / 5
)
)
self.position = CGPoint(x: frame.midX, y: frame.midY)
}
var position: CGPoint {
get { return node.position }
set { node.position = newValue }
}
}
The willSet and didSet observers of superclass properties are called when a property is set in a subclass initializer, after the superclass initializer has been called. They are not called while a class is setting its own properties, before the superclass initializer has been called.
One possible solution could be to set the position property outside of the Spaceship's initialiser, or also set directly the node's position.
I personally would just subclass Spaceship as an SKSpriteNode. Spaceship does not seem to be a complex object that involves composition over inheritance.
class Spaceship : SKSpriteNode {
convenience init(frame: CGRect) {
self.init(
color: UIColor.red,
size: CGSize(
width: frame.size.width / 5,
height: frame.size.width / 5
)
)
self.position = CGPoint(x: frame.midX, y: frame.midY)
}
}
Now I am not sure why you are passing in frame like this.... I am assuming frame is your scene? If this is the case, you are going to come across issues when anchor point is not (0,0).

Swift 3 (SpriteKit): SKShapeNode not moving when its image is changed

I have come across an issue with SKShapeNodes that I'm struggling to understand. To help explain and show my problem, I have created a test program. At the start I create a line like this:
var points = [CGPoint(x: -372, y: 0), CGPoint(x: 372, y: 0)]
var Line = SKShapeNode(points: &points, count: points.count)
addChild(Line)
At this point, I am able to change the position of the node, and it prints the position I expect it to be:
Line.position.y = Line.position.y - 50
print("LINEPOS =", Line.position)
It prints:
LINEPOS = (0.0, -50.0)
This makes sense because the SKShapeNode's original position is always (0, 0). However, the next part of the program doesn't work in the same way. I make the new points equal the coordinates of where the Line is now on the screen, and let the Line equal them like this:
points = [CGPoint(x: -372, y: -50), CGPoint(x: 372, y: -50)]
Line = SKShapeNode(points: &points, count: points.count)
Then I change the position of the Line again (this time I moved it down by 200 so the change in position is more noticeable) like this:
Line.position.y = Line.position.y - 200
print("LINEPOS =", Line.position)
It prints:
LINEPOS = (0.0, -200.0)
Even though it prints the position correctly, it is clear that the Line hasn't moved at all on the screen. It is still at the same position as it was when I let the line equal the new points. This means that after letting the SKShapeNode equal a new set of points, it no longer moves on the screen but in the code it says it does.
How can I fix this? If anyone knows please help because I'm really confused.
Try this code:
import SpriteKit
class GameScene:SKScene {
override func didMove(to view: SKView) {
var points = [CGPoint(x: -372, y: 0), CGPoint(x: 372, y: 0)]
var Line = SKShapeNode(points: &points, count: points.count)
addChild(Line)
Line.position.y = Line.position.y - 50
print("LINEPOS =", Line.position)
points = [CGPoint(x: -372, y: -50), CGPoint(x: 372, y: -50)]
Line.removeFromParent()
Line = SKShapeNode(points: &points, count: points.count)
addChild(Line)
Line.position.y = Line.position.y - 200
print("LINEPOS =", Line.position)
}
}
From your code, when you for the second time do something like Line = SKShapeNode = ... the new shape is created, which doesn't have a parent yet. It is not in a node tree. What you are seeing is the old shape, which is not removed from a parent. So, remove that one, and add a new one... Or, don't create a new shape every time, and just change its position.
Also, this can be a bit confusing. Even if you remove the line which says:
Line.removeFromParent()
the code will compile (but you will see two lines).
So, even though you are working on a same variable and a fact that a node can have only one parent, the code still compiles if you don't remove the Line from parent, before you make another addChild(Line) call. How is that possible?
Well, that works because of this:
You create a Line which is a reference to a SKShapeNode which is added to a parent right after its creation. Now, because it is added to a tree, there is another reference pointing to it, because it is a part of a scene's children array.
Then you instantiate a Line variable again, so now it shows on a different SKShapeNode in memory (but the old SKShapeNode is still in a tree). This new SKShapeNode is not added to a tree, and its parent proprety is a nil. Because of this, you can add it again to a tree, without having compiler yelling at you.
EDIT:
To respond to your comments about how to change a path of an SKShapeNode, you could do something like this:
let path = CGMutablePath()
points = [CGPoint(x: -372, y: -250), CGPoint(x: 372, y: -250)]
path.move(to: CGPoint(x: points[0].x, y: points[0].y))
path.addLine(to: CGPoint(x: points[1].x, y: points[1].y))
Line.path = path
just check this once
import SpriteKit
var Line:SKShapeNode!
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
var points = [CGPoint(x: -372, y: 0), CGPoint(x: 372, y: 0)]
Line = SKShapeNode(points: &points, count: points.count)
addChild(Line)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
Line.position.y = Line.position.y + 200
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}

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.

Implementing anchorPoint in SKNode subclass

I have a custom class inheriting from SKNode and would like to implement SKSpriteNode's anchorPoint-like functionality on that class. Specifically, what I am trying to achieve is to center the custom node in the parent node automatically (anchorPoint at 0.5, 0.5) without having to offset it by half its width/height.
Any tips?
Edit: I ended up by first creating an anchorPoint variable and then overriding the position variable and set the super position modified by the anchorPoint.
var anchorPoint = CGPoint(x: 0.5, y: 0.5)
override var position: CGPoint {
set {
super.position = CGPoint(x: self.position.x - self.size.width * self.anchorPoint.x, y: self.position.y - self.size.height * self.anchorPoint.y)
}
get {
return super.position
}
}
You can over ride the setPosition function of the inherited class. In this function you would adjust for the offset-ed anchorPoint.
For example, if you sprite is 100x100. And you have set its anchor as (.5,.5), When someone sets the position, you should add or subtract the sprite dimensions from it and then set the position.
In this case, if user sets the sprite position as (0,0), you would add (100*.5,100*.5) = (50,50). And adding (0,0) to it would be (50,50).
This is the new position that you should set.
It might be easier to add a new "root" SKNode under your control's SKNode, add any child nodes to it, and then just move it when the anchorPoint property is changed. That's what I did.
var anchorPoint: CGPoint = CGPoint( x: 0.5, y: 0.5 )
{
didSet
{
let translateX = ( anchorPoint.x - 0.5 ) * controlSize.width
let translateY = ( anchorPoint.y - 0.5 ) * controlSize.height
rootNode!.position = CGPoint(x: translateX, y: translateY)
}
}