Correct way to subclass CAShapeLayer - ios

Inspired by this example, I have a created custom CALayer subclasses for wedges and arcs. The allow me to draw arcs and wedges and animate changes in them so that they sweep radially.
One of the frustrations with them, is that apparently when you go this route of having a subclass drawInContext() you are limited by the clip of the layer's frame. With stock layers, you have the masksToBounds which is by default false! But it seems once you the subclass route with drawing, that because implicitly and unchangeably true.
So I thought I would try a different approach, by subclassing CAShapeLayer instead. And instead of adding a custom drawInContext(), I would simply have my variables for start and sweep update the path of the receiver. This works nicely BUT it will no longer animate as it used to:
import UIKit
class WedgeLayer:CAShapeLayer {
var start:Angle = 0.0.rotations { didSet { self.updatePath() }}
var sweep:Angle = 1.0.rotations { didSet { self.updatePath() }}
dynamic var startRadians:CGFloat { get {return self.start.radians.raw } set {self.start = newValue.radians}}
dynamic var sweepRadians:CGFloat { get {return self.sweep.radians.raw } set {self.sweep = newValue.radians}}
// more dynamic unit variants omitted
// we have these alternate interfaces because you must use a type
// which the animation framework can understand for interpolation purposes
override init(layer: AnyObject) {
super.init(layer: layer)
if let layer = layer as? WedgeLayer {
self.color = layer.color
self.start = layer.start
self.sweep = layer.sweep
}
}
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePath() {
let center = self.bounds.midMid
let radius = center.x.min(center.y)
print("updating path \(self.start) radius \(radius)")
if self.sweep.abs < 1.rotations {
let _path = UIBezierPath()
_path.moveToPoint(center)
_path.addArcWithCenter(center, radius: radius, startAngle: self.start.radians.raw, endAngle: (self.start + self.sweep).radians.raw, clockwise: self.sweep >= 0.radians ? true : false)
_path.closePath()
self.path = _path.CGPath
}
else {
self.path = UIBezierPath(ovalInRect: CGRect(around: center, width: radius * 2, height: radius * 2)).CGPath
}
}
override class func needsDisplayForKey(key: String) -> Bool {
return key == "startRadians" || key == "sweepRadians" || key == "startDegrees" || key == "sweepDegrees" || key == "startRotations" || key == "sweepRotations" || super.needsDisplayForKey(key)
}
}
Is it not possible to make it regenerate the path and update as it animates the value? With the print() statement there, I can see it interpolating through the values as expected during an animation. I have tried adding setNeedsDisplay() in various locations, but to no avail.

There are a couple tricks to getting this to work properly:
Don't use the didSet. Rather, you should override display() and update self.path there.
Use (presentation() as! WedgeLayer? ?? self).sweep to access the property. (presentation, known as presentationLayer in Obj-C and Swift 2, allows you to access the currently-visible property values during animation.)
If you want implicit animation, implement actionForKey as described in this answer.

Related

Is it possible to group images by drawing lines on it in iOS swift?

For an example if i have multiple images on views in random position. Images are selected by drawing lines on it and group images by using gestures. Right now i can able to show images randomly but not able group images by drawing line on it.
Here screenshot 1 is result which i have getting now:
screenshot 2 which is exactly what i want.
For what you are trying to do I would start by creating a custom view (a subclass) that is able to handle gestures and draw paths.
For gesture recognizer I would use UIPanGestureRecognizer. What you do is have an array of points where the gesture was handled which are then used to draw the path:
private var currentPathPoints: [CGPoint] = []
#objc private func onPan(_ sender: UIGestureRecognizer) {
switch sender.state {
case .began: currentPathPoints = [sender.location(in: self)] // Reset current array by only showing a current point. User just started his path
case .changed: currentPathPoints.append(sender.location(in: self)) // Just append a new point
case .cancelled, .ended: endPath() // Will need to report that user lifted his finger
default: break // These extra states are her just to annoy us
}
}
So if this method is used by pan gesture recognizer it should track points where user is dragging. Now these are best drawn in drawRect which needs to be overridden in your view like:
override func draw(_ rect: CGRect) {
super.draw(rect)
// Generate path
let path: UIBezierPath = {
let path = UIBezierPath()
var pointsToDistribute = currentPathPoints
if let first = pointsToDistribute.first {
path.move(to: first)
pointsToDistribute.remove(at: 0)
}
pointsToDistribute.forEach { point in
path.addLine(to: point)
}
return path
}()
let color = UIColor.red // TODO: user your true color
color.setStroke()
path.lineWidth = 3.0
path.stroke()
}
Now this method will be called when you invalidate drawing by calling setNeedsDisplay. In your case that is best done on setter of your path points:
private var currentPathPoints: [CGPoint] = [] {
didSet {
setNeedsDisplay()
}
}
Since this view should be as an overlay to your whole scene you need some way to reporting the events back. A delegate procedure should be created that implements methods like:
func endPath() {
delegate?.myLineView(self, finishedPath: currentPathPoints)
}
So now if view controller is a delegate it can check which image views were selected within the path. For first version it should be enough to just check if any of the points is within any of the image views:
func myLineView(sender: MyLineView, finishedPath pathPoints: [CGPoint]) {
let convertedPoints: [CGPoint] = pathPoints.map { sender.convert($0, to: viewThatContainsImages) }
let imageViewsHitByPath = allImageViews.filter { imageView in
return convertedPoints.contains(where: { imageView.frame.contains($0) })
}
// Use imageViewsHitByPath
}
Now after this basic implementation you can start playing by drawing a nicer line (curved) and with cases where you don't check if a point is inside image view but rather if a line between any 2 neighbor points intersects your image view.

Swift: Can't subclass SCNNode? Fatalerror

Ok, I need to subclass SCNNode because I have different SCNNodes with different "abilities" in my game (I know people don't usually subclass SCNNode but I need to)
I have followed every other question like Subclassing SCNNode and Creating a subclass of a SCNNode
but continue to get this error:
fatal error: use of unimplemented initializer 'init()' for class 'LittleDude.Dude'
Where Dude is the name of my SCNNode subclass.
Following the second question, because of classing issues this is how I attempt to get the SCNNode from my .dae scene and assign it to my Dude():
var theDude = Dude(geometry: SCNSphere(radius: 0.1)) //random geometry first
var modelScene = SCNScene(named: "art.scnassets/ryderFinal3.dae")!
if let d = modelScene.rootNode.childNodes.first
{
theDude.transform = d.transform
theDude.geometry = d.geometry
theDude.rotation = d.rotation
theDude.position = d.position
theDude.boundingBox = d.boundingBox
theDude.geometry?.firstMaterial = d.geometry?.firstMaterial
}
print("DUDE: ", theDude)
Then in my Dude class:
class Dude: SCNNode {
init(geometry: SCNGeometry) {
super.init()
center(node: self)
self.scale = SCNVector3(x: modifier, y: modifier, z: modifier)
//theDude.transform = SCNMatrix4Mult(theDude.transform, SCNMatrix4MakeRotation(360, 0, 1, 0))
//theDude.worldOrientation = .
//self.theDude.position = SCNVector3Make(0, 0, -1)
for s in animScenes {
if let anim = animationFromSceneNamed(path: s)
{
animations.append(anim)
anim.usesSceneTimeBase = true
anim.repeatCount = Float.infinity
self.addAnimation(anim, forKey: anim.description)
}
}
}
}
/* Xcode required this */
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented222")
}
The error gets drawn on first line of this custom class and happens when I try to clone and add the custom SCNNode to my scene:
func makeDude(hitPosition: SCNVector3) {
//print("DUDE")
let clone = theDude.clone() as? SCNNode
clone?.position = hitPosition
self.sceneView.scene.rootNode.addChildNode(clone!)
}
Even though I cast to SCNNode to try to avoid an error. How can I just clone and use my custom SCNNode in my scene? What is wrong here?
Just to make clear, this answer is hidden on the comments of a previous answer, so to avoid the confusion here is the answer fully spelled out:
class NodeSubClass: SCNNode {
init(geometry: SCNGeometry?){
super.init()
self.geometry = geometry
}
...
}
If you subclass SCNNode and override its initializer init(geometry: SCNGeometry?) then you'll need to call the same initalizer of super during your init. Try changing
super.init()
to
super.init(geometry: geometry)

How to animate a custom property in iOS

I have a custom UIView that draws its contents using Core Graphics calls. All working well, but now I want to animate a change in value that affects the display. I have a custom property to achieve this in my custom UView:
var _anime: CGFloat = 0
var anime: CGFloat {
set {
_anime = newValue
for(gauge) in gauges {
gauge.animate(newValue)
}
setNeedsDisplay()
}
get {
return _anime
}
}
And I have started an animation from the ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.emaxView.anime = 0.5
UIView.animate(withDuration: 4) {
DDLogDebug("in animations")
self.emaxView.anime = 1.0
}
}
This doesn't work - the animated value does change from 0.5 to 1.0 but it does so instantly. There are two calls to the anime setter, once with value 0.5 then immediately a call with 1.0. If I change the property I'm animating to a standard UIView property, e.g. alpha, it works correctly.
I'm coming from an Android background, so this whole iOS animation framework looks suspiciously like black magic to me. Is there any way of animating a property other than predefined UIView properties?
Below is what the animated view is supposed to look like - it gets a new value about every 1/2 second and I want the pointer to move smoothly over that time from the previous value to the next. The code to update it is:
open func animate(_ progress: CGFloat) {
//DDLogDebug("in animate: progress \(progress)")
if(dataValid) {
currentValue = targetValue * progress + initialValue * (1 - progress)
}
}
And calling draw() after it's updated will make it redraw with the new pointer position, interpolating between initialValue and targetValue
Short answer: use CADisplayLink to get called every n frames. Sample code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.preferredFramesPerSecond = 50
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
updateValues()
}
var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
self.emaxView.animate(0);
lastUpdateTime = CACurrentMediaTime()
animationComplete = false
}
func animationDidUpdate(displayLink: CADisplayLink) {
if(!animationComplete) {
let now = CACurrentMediaTime()
let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
self.emaxView.animate(min(CGFloat(interval), 1))
animationComplete = interval >= 1.0
}
}
}
The code could be refined and generalised but it's doing the job I needed.
You will need to call layoufIfNeeded() instead of setNeedsDisplay() if you modify any auto layout constraints in your gauge.animate(newValue) function.
https://stackoverflow.com/a/12664093/255549
If that is drawn entirely with CoreGraphics there is a pretty simple way to animate this if you want to do a little math. Fortunately you have a scale there that tells you the number of radians exactly to rotate, so the math is minimal and no trigonometry is involved. The advantage of this is you won't have to redraw the entire background, or even the pointer. It can be a bit tricky to get angles and stuff right, I can help out if the following doesn't work.
Draw the background of the view normally in draw(in rect). The pointer you should put into a CALayer. You can pretty much just move the draw code for the pointer, including the centre dark gray circle into a separate method that returns a UIImage. The layer will be sized to the frame of the view (in layout subviews), and the anchor point has to be set to (0.5, 0.5), which is actually the default so you should be ok leaving that line out. Then your animate method just changes the layer's transform to rotate according to what you need. Here's how I would do it. I'm going to change the method and variable names because anime and animate were just a bit too obscure.
Because layer properties implicitly animate with a duration of 0.25 you might be able to get away without even calling an animation method. It's been a while since I've worked with CoreAnimation, so test it out obviously.
The advantage here is that you just set the RPM of the dial to what you want, and it will rotate over to that speed. And no one will read your code and be like WTF is _anime! :) I have included the init methods to remind you to change the contents scale of the layer (or it renders in low quality), obviously you may have other things in your init.
class SpeedDial: UIView {
var pointer: CALayer!
var pointerView: UIView!
var rpm: CGFloat = 0 {
didSet {
pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
//draw background with values
//but not the pointer or centre circle
context.restoreGState()
}
override func layoutSubviews() {
super.layoutSubviews()
pointerView.frame = bounds
pointer.frame = bounds
pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pointer.contents = drawPointer(in: bounds)?.cgImage
}
func drawPointer(in rect: CGRect) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
// draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
// I'm not sure what your drawing code looks like, but if the pointer is pointing
// vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
// perform this context rotation before actually drawing the pointer
context.translateBy(x: rect.width/2, y: rect.height/2)
context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
context.translateBy(x: -rect.width/2, y: -rect.height/2)
context.restoreGState()
let pointerImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return pointerImage
}
}
The pointer's identity transform has it pointing at 0 RPM, so every time you up the RPM to what you want, it will rotate up to that value.
edit: tested it, it works. Except I made a couple errors - you don't need to change the layers position, I updated the code accordingly. Also, changing the layer's transform triggers layoutSubviews in the immediate parent. I forgot about this. The easiest way around this is to put the pointer layer into a UIView that is a subview of SpeedDial. I've updated the code. Good luck! Maybe this is overkill, but its a bit more reusable than animating the entire rendering of the view, background and all.

Design pattern for setAnimated and IBInspectable property in Swift

I can’t figure out how to incorporate a design pattern in to my Swift class which has a property with the following requirements:
Property with default value and is IBInspectable
accompanying setProperty:animated: method
All of the ways I’ve tried require a separate private ‘instance' variable e.g. _property, like below:
#IBInspectable var progress: CGFloat = 0.5 {
didSet {
_progress = progress
setNeedsDisplay()
}
}
private var _progress: CGFloat = 0.5
func setProgress(progress: CGFloat, animated: Bool)
{
if _progress == progress
{
return
}
if animated
{
// Animate changes
// [Animation code]
}
else
{
// No need to animate changes
setNeedsDisplay()
}
titleLabel?.text = "\(NSInteger(progress * 100))%"
// Update value
_progress = progress
}
This doesn’t account for getting the property value though. As a property cannot have a get and didSet method.
So what is the correct way of having a property which is IBInspectable and can be set in the setAnimated method without automatically updating, bypassing the animation?

Adding Array to move group of images in for loop in Swift : SpriteKit

How can I add my starsSqArray to a for loop in my update function that grabs all the SKSpriteNodesfor _starsSq1 so that all of the stars move together and not separately?
Right now my Swift class keeps returning an error saying that _starsSqArray doesn't have a position (my code is bugged out). My goal is to grab the plotted stars and move them downward all at once.
import SpriteKit
class Stars:SKNode {
//Images
var _starsSq1:SKSpriteNode?
//Variables
var starSqArray = Array<SKSpriteNode>()
var _starSpeed1:Float = 5;
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init() {
super.init()
println("stars plotted")
createStarPlot()
}
/*-------------------------------------
## MARK: Update
-------------------------------------*/
func update() {
for (var i = 0; i < starSqArray.count; i++) {
_starsSq1!.position = CGPoint(x: self.position.x , y: self.position.y + CGFloat(_starSpeed1))
}
}
/*-------------------------------------
## MARK: Create Star Plot
-------------------------------------*/
func createStarPlot() {
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
for (var i = 0; i < 150 ; i++) {
_starsSq1 = SKSpriteNode(imageNamed: "starSq1")
addChild(_starsSq1!)
//starSqArray.addChild(_starsSq1)
var x = arc4random_uniform(UInt32(screenSize.width + 400))
var y = arc4random_uniform(UInt32(screenSize.height + 400))
_starsSq1!.position = CGPoint(x: CGFloat(x), y: CGFloat(y))
}
}
}
A couple of suggestions by a design point of view:
You already have all your stars (I guess so) grouped togheter inside a common parent node (that you correctly named Stars). Then you just need to move your node of type Stars and all its child node will move automatically.
Manually changing the coordinates of a node inside an update method does work but (imho) it is not the best way to move it. You should use SKAction instead.
So, if you want to move the stars forever with a common speed and direction you can add this method to Stars
func moveForever() {
let distance = 500 // change this value as you prefer
let seconds : NSTimeInterval = 5 // change this value as you prefer
let direction = CGVector(dx: 0, dy: distance)
let move = SKAction.moveBy(direction, duration: seconds)
let moveForever = SKAction.repeatActionForever(move)
self.runAction(moveForever)
}
Now you can remove the code inside the update method and call moveForever when you want the stars to start moving.
Finally, at some point the stars will leave the screen. I don't know the effect you want to achieve but you will need to deal with this.
for (SKSpriteNode *spriteNode in starSqArray) {
spriteNode.position = CGPoint(x: spriteNode.position.x , y: spriteNode.position.y + GFloat(_starSpeed1))
}
Use the above code in the update() function.
Hope this helps...

Resources