Start CAAnimation when view is not in view hierarchy - ios

I have a UI component (a loading spinner) that has an endless animation:
class MySpinner: UIView {
...
override var bounds: CGRect {
didSet {
updateLayers()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.animatedLayer)
self.updateLayers()
self.startAnimating()
}
private func updateLayers() {
//Set up path, stroke color and such
self.animatedLayer.frame = self.bounds
}
private func startAnimating() {
CATransaction.begin()
CATransaction.setDisableActions(true) //disable automatic animations by iOS
let endlessRotationAnimation = CAKeyframeAnimation(keyPath: "transform.rotation")
endlessRotationAnimation.values = [ 0.0, CGFloat(2.0 * .pi) ]
endlessRotationAnimation.keyTimes = [ 0.0, 1.0 ]
endlessRotationAnimation.repeatDuration = .infinity
endlessRotationAnimation.duration = 1.0
self.animatedLayer.add(endlessRotationAnimation, forKey: "rotationAnimation")
CATransaction.commit()
}
}
It turns out this is not working. I assume that if I call startAnimating() before the view is added to the window, iOS will immediately remove the animation. If I first add the view to the view hierarchy and then call startAnimating(), things start working.
Is there a way to prevent iOS removing my animation? Or is there some documentation about this behaviour, I couldn't find any?

I was able to work around this issue by overriding didMoveToWindow.
public override func didMoveToWindow() {
super.didMoveToWindow()
startAnimating()
}

Related

How to reduce blur effect on UIVisualEffectView

How can i reduce the blur effect on a UIVisualEffectView it gives me options of light, extraLight and dark which are not good enough for me i am trying to achieve something like this
We can do that absolutely natively and with correct expected look using animator
Usage:
let blurEffectView = BlurEffectView()
view.addSubview(blurEffectView)
BlurEffectView realisation:
class BlurEffectView: UIVisualEffectView {
var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
override func didMoveToSuperview() {
guard let superview = superview else { return }
backgroundColor = .clear
frame = superview.bounds //Or setup constraints instead
setupBlur()
}
private func setupBlur() {
animator.stopAnimation(true)
effect = nil
animator.addAnimations { [weak self] in
self?.effect = UIBlurEffect(style: .dark)
}
animator.fractionComplete = 0.1 //This is your blur intensity in range 0 - 1
}
deinit {
animator.stopAnimation(true)
}
}
There is no official way to change the blur level.
However, you can try something like this:
Create a custom blur view
Here's my riff on bodich's answer that fixes the problem of the blur resetting each time the app comes to the foreground. I'm still not convinced this hack is shippable since there may be other times that CoreAnimation decides to finish stale animations in which case the blur would suddenly become full intensity.
class BlurEffectView: UIVisualEffectView {
private var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
private var intensity: CGFloat = 0.25
init(intensity: CGFloat) {
self.intensity = intensity
super.init(effect: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
deinit {
animator.stopAnimation(true)
}
override func didMoveToSuperview() {
guard let superview = superview else { return }
backgroundColor = .clear
frame = superview.bounds
autoresizingMask = [.flexibleWidth, .flexibleHeight]
clipsToBounds = true
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterFG(_:)),
name:UIApplication.willEnterForegroundNotification,
object: nil
)
setUpAnimation()
}
private func setUpAnimation() {
animator.stopAnimation(true)
effect = nil
animator.addAnimations { [weak self] in
self?.effect = UIBlurEffect(style: .light)
}
animator.fractionComplete = intensity
}
#objc func appWillEnterFG(_ note: Notification) {
setUpAnimation()
}
}
I know this is late, but if you're just using blur for a modal presentations, apple has a built-in UIModalPresentationStyle called blurOverFullScreen, and it works pretty well.
Just set the parent controller's modalPresentationStyle to .blurOverFullScreen and present the new view controller. If the new view is smaller than the screen, the background should be blurred out like seen in your picture.

iOS Swift 3 - Clearing the UIView

I'm building an iOS app where I have a class named "DrawableCanvas" that extends UIView. The idea is to enable users to draw on UIViews in the Storyboard that extend the class I made.
The drawing works fine and smooth at this point. Here's the code I used:
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=1.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: #selector(pan))
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func draw(_ rect: CGRect) {
// Drawing code
UIColor.black.setStroke()
path.stroke()
path.lineWidth=lineWidth
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void{
let currentPoint=panGestureRecognizer.location(in: self)
let midPoint=self.midPoint(p0: previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .began{
path.move(to: currentPoint)
}
else if panGestureRecognizer.state == .changed{
path.addQuadCurve(to: midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint, p1:CGPoint)->CGPoint{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}
Of course, I have to clear the view on a button press, and here's my button press action:
#IBAction func reset1(_ sender: AnyObject) {
print("reset hit!")
sig1View.setNeedsLayout()
sig1View.setNeedsDisplay()
}
where sig1View is the view I'm trying to reset.
This didn't work out, so what I tried instead was to write a reloadData() in the DrawableView class:
func reloadData() {
print("reload data hit")
self.setNeedsDisplay()
}
The function got called, but that too, didn't work. How do I clear or reset a UIView in iOS Swift 3?
Call your DrawableView's (instance's) path.removeAllPoints function to remove all the drawings/path.
So following your code, reset1 should look like this now:
#IBAction func reset1(_ sender: AnyObject) {
print("reset hit!")
sig1View.path.removeAllPoints()
sig1View.setNeedsDisplay()
}
The problem is that, although your question talks about "resetting", your code does not in fact reset anything. You are saying:
override func draw(_ rect: CGRect) {
// Drawing code
UIColor.black.setStroke()
path.stroke()
path.lineWidth=lineWidth
}
So that's what's happening. As long as path contains a drawable path, that is what is drawn in your view whenever the view redraws itself. If that's not what you want, change the path, or change the implementation of draw.
Add this method to DrawableView class and call it with sig1View instance:
func clear()
{
path.removeAllPoints()
setNeedsDisplay()
}
and call it from the reset method.
Usage:
sig1View.clear()

touchesBegan and touchesMoved not getting triggered on UIView

What I'm trying to achieve:
Trying to grab the coordinate of the currently touched area on the screen and draw the received coordinate on the screen. Simply put, a basic drawing app that's all written programmatically (for my own practice).
Problem
touchesBegan and touchesMoved for PaintingSubclass are not getting called at all.
Current setup
I have a PaintingViewController then I also have a PaintingSubclass.
PaintingViewController is a UIViewController and PaintingSubclass is a UIView.
PaintingViewController creates an instance of PaintingSubclass and adds it to the subview of PaintingViewController.
PaintingSubclass is where the actual drawing happens.
What I've tried so far
Put a breakpoint inside the touchesMoved and touchesBegan (didn't work)
Tried to add a UITapGestureRecognizer (didn't work)
Enabled self.isUserInteractionEnabled = true (didn't work)
Make PaintingSubclass inherit UIControl and call sendActions(for: .valueChanged) at the end of touchesMoved and touchesBegan (didn't work)
Current Code
(please ignore any the unnecessary variables)
import UIKit
class PaintingViewController: UIViewController{
var _paintView: PaintingSubclass? = nil
override func loadView() {
view = UILabel()
}
private var labelView: UILabel {
return view as! UILabel
}
override func viewDidLoad() {
_paintView = PaintingSubclass()
_paintView?.frame = CGRect(x: view.bounds.minX, y: view.bounds.minY, width: 400, height: 750)
_paintView?.backgroundColor = UIColor.white
_paintView?.setNeedsDisplay()
view.addSubview(_paintView!)
}
class PaintingSubclass: UIView{
private var context: CGContext? = nil
var styleSelection: String = "buttcap"
var linewidth: Float = 5
var lineCapStyle: CGLineCap? = nil
var lineJoinStyle: CGLineJoin? = nil
var lineWidthValue: CGFloat? = nil
var colorValue: UIColor? = nil
override init(frame: CGRect) {
super.init(frame: frame)
lineWidthValue = 0.5
colorValue = UIColor(cgColor: UIColor.black.cgColor)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
context = UIGraphicsGetCurrentContext()!
context?.move(to: CGPoint(x: 0.0, y: 110.0))
context?.setStrokeColor((colorValue?.cgColor)!)
context?.drawPath(using: CGPathDrawingMode.stroke)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch: UITouch = touches.first!
let touchPoint: CGPoint = touch.location(in: self)
let _xValue = touchPoint.x
let _yValue = touchPoint.y
NSLog("coordinate: \(_xValue), \(_yValue)")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let touch: UITouch = touches.first!
let touchPoint: CGPoint = touch.location(in: self)
let _xValue = touchPoint.x
let _yValue = touchPoint.y
NSLog("coordinate: \(_xValue), \(_yValue)")
}
}
}
What am I doing wrong here? Been stuck in this state for hours not making any progress. Any help/feedbacks will be much appreciated.
I don't know why you are setting your default view to UILabel(), but your default isUserInteractionEnabled of your view is false. Set it to true
override func loadView() {
view = UILabel()
view.isUserInteractionEnabled = true
}
According to your code, your PaintingViewController's view is a UILabel, the property isUserInteractionEnabled default is false.Try to set this property to true may help you.
The problem lies on:
// override func loadView() {
// view = UILabel()
// }
//
// private var labelView: UILabel {
// return view as! UILabel
// }
You turn the view controller's self view object to a UILabel object.
It would be better to have your painting view as the main view in the view controller and the UILabel as a subview of the painting view. The UILabel needs to have userInteractionEnabled in your current layout, but instead of changing that switch the layout around.
Apart from anything else, presumably you don't want the painting view to draw over the label, but rather the label to be drawn on top, so the label should be the subview.

View drawRect only called after delay

I made a UIView subclass that implements custom drawing. Here is the code (please don't mind that this could be done by a UIImageView. I stripped the code of all extras just to show the problem)
#IBDesignable
class TPMSkinnedImageView: UIView {
private var originalImage:UIImage?
private var skinnedImage:UIImage?
#IBInspectable var image: UIImage? {
set {
originalImage = newValue
if(newValue == nil) {
skinnedImage = nil
return
}
skinnedImage = originalImage!
self.invalidateIntrinsicContentSize()
self.setNeedsDisplay()
}
get {
return originalImage
}
}
override func draw(_ rect: CGRect) {
let context:CGContext! = UIGraphicsGetCurrentContext()
context.saveGState()
context.translateBy(x: 0, y: rect.height)
context.scaleBy(x: 1, y: -1)
context.draw(skinnedImage!.cgImage!, in: rect)
context.restoreGState()
}
override var intrinsicContentSize: CGSize {
if(skinnedImage != nil) {
return skinnedImage!.size
}
return CGSize.zero
}
}
I instantiate this view in a viewcontroller nib file and show the viewcontroller modally.
What happens is is that the draw method only gets called when the parent view has been on screen for about 20 seconds.
I checked the intrinsicContentSize and it does not return .zero
This is what the stack looks like once it is called:
Any idea what could be causing this?
Try calling setNeedsDisplay() on your view in your view controller's viewWillAppear()

CALayer with Animated Path Content, Make Inspectable/Designable

I made a custom circular progress view by subclassing UIView, adding a CAShapeLayer sublayer to its layer and overriding drawRect() to update the shape layer's path property.
By making the view #IBDesignable and the progress property #IBInspectable, I was able to edit its value in Interface Builder and see the updated bezier path in real time. Non-essential, but really cool!
Next, I decided to make the path animated: Whenever you set a new value in code, the arc indicating the progress should "grow" from zero length to whatever percentage of the circle is achieved (think of arcs in the Activity app in apple Watch).
To achieve this, I swapped my CAShapeLayer sublayer by a custom CALayer subclass that has a #dynamic (#NSManaged) property, observed as key for ananimation (I implemented needsDisplayForKey(), actionForKey(), drawInContext() etc. ).
My View code (the relevant parts) looks something like this:
// Triggers path update (animated)
private var progress: CGFloat = 0.0 {
didSet {
updateArcLayer()
}
}
// Programmatic interface:
// (pass false to achieve immediate change)
func setValue(newValue: CGFloat, animated: Bool) {
if animated {
self.progress = newValue
} else {
arcLayer.animates = false
arcLayer.removeAllAnimations()
self.progress = newValue
arcLayer.animates = true
}
}
// Exposed to Interface Builder's inspector:
#IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
private func updateArcLayer() {
arcLayer.frame = self.layer.bounds
arcLayer.progress = progress
}
And the layer code:
var animates: Bool = true
#NSManaged var progress: CGFloat
override class func needsDisplay(forKey key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == "progress" && animates == true {
return makeAnimation(forKey: event)
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
ctx.beginPath()
// Define the arcs...
ctx.closePath()
ctx.setFillColor(fillColor.cgColor)
ctx.drawPath(using: CGPathDrawingMode.fill)
}
private func makeAnimation(forKey event: String) -> CABasicAnimation? {
let animation = CABasicAnimation(keyPath: event)
if let presentationLayer = self.presentation() {
animation.fromValue = presentationLayer.value(forKey: event)
}
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.duration = animationDuration
return animation
}
The animation works, but now I can't get my paths to show in Interface Builder.
I have tried implementing my view's prepareForInterfaceBuilder() like this:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.topLabel.text = "Hello, Interface Builder!"
updateArcLayer()
}
...and the label text change is reflected in Interface Builder, but the path isn't rendered.
Am I missing something?
Well, isn't it funny... It turns out the declaration of my #Inspectable property had a very silly bug.
Can you spot it?
#IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
It should be:
#IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: newValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
That is, I was discarding the passed value (newValue) and using the current one (currentValue) to set the internal variable instead. "Logging" the value (always 0.0) to Interface Builder (via my label's text property!) gave me a clue.
Now it is working fine, no need for drawRect() etc.

Resources