CAAnimation: Syncing view's alpha with presentation layer's opacity - ios

I'm animating the opacity of a view's layer using CAKeyframeAnimation, when app goes to background, the animation's removed, but I need to make the view's alpha to be the same as the animation, should I do:
view.alpha = view.layer.presentationLayer.opacity
???
Thanks!
Update:
I have three labels overlapping with each other, I used key frame animation to animate their opacity with different key frame values (for opacity) to mimic a crossfade animation. The problem is when app goes to background, the animations are removed (according to https://forums.developer.apple.com/thread/15796) so they all have alpha 1 and overlap with each other, that's why I wanted to sync the view with their presentation layer.

If the goal is to capture the opacity when the app goes in background, you can add an observer for UIApplicationDidEnterBackground, capture the opacity, cancel the animation, and set the alpha. E.g., in Swift:
class ViewController: UIViewController {
#IBOutlet weak var viewToAnimate: UIView!
private var observer: NSObjectProtocol!
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: .UIApplicationDidEnterBackground, object: nil, queue: .main) { [weak self] notification in
if let opacity = self?.viewToAnimate.layer.presentation()?.opacity {
self?.viewToAnimate.layer.removeAllAnimations()
self?.viewToAnimate.alpha = CGFloat(opacity)
}
}
}
deinit {
NotificationCenter.default.removeObserver(observer)
}
// I'm just doing a basic animation, but the idea is the same whatever animation you're doing
#IBAction func didTapButton(_ sender: Any) {
UIView.animate(withDuration: 10) {
self.viewToAnimate.alpha = 0
}
}
}
If your goal is to remember it even if the app is terminated, then you'd need to save this in persistent storage. But if your goal is to merely set the alpha while the app is suspended and/or running in background, the above is sufficient.

Related

constraints are reapplied when timer is running and changes label text

I'm working on a swift camera app and trying to solve the following problem.
My camera app, which allows taking a video, can change the camera focus and exposure when a user taps somewhere on the screen. Like the default iPhone camera app, I displayed the yellow square to show where the user tapped. I can see exactly where the user tapped unless triggering the timer function and updating the total video length on the screen.
Here is the image of the camera app.
As you can see there is a yellow square and that was the point I tapped on the screen.
However, when the timer counts up (in this case, 19 sec to 20sec) and updates the text of total video length, the yellow square moves back to the center of the screen. Since I put the yellow square at the center of the screen on my storyboard, I guess when the timer counts up and updates the label text, it also refreshing the yellow square, UIView, so displaying at the center of the screen (probably?).
So if I'm correct, how can I display the yellow square at the user tapped location regardless of the timer function, which updates UIlabel for every second?
Here is the code.
final class ViewController: UIViewController {
private var pointOfInterestHalfCompletedWorkItem: DispatchWorkItem?
#IBOutlet weak var pointOfInterestView: UIView!
#IBOutlet weak var time: UILabel!
var counter = 0
var timer = Timer()
override func viewDidLayoutSubviews() {
pointOfInterestView.layer.borderWidth = 1
pointOfInterestView.layer.borderColor = UIColor.systemYellow.cgColor
}
#objc func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
#objc func timerAction() {
counter += 1
secondsToHoursMinutesSeconds(seconds: counter)
}
func secondsToHoursMinutesSeconds (seconds: Int) {
// format seconds
time.text = "\(strHour):\(strMin):\(strSec)"
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let focusPoint = touches.first!.location(in: lfView)
showPointOfInterestViewAtPoint(point: focusPoint)
}
func showPointOfInterestViewAtPoint(point: CGPoint) {
print("point is here \(point)")
pointOfInterestHalfCompletedWorkItem = nil
pointOfInterestComplatedWorkItem = nil
pointOfInterestView.center = point
pointOfInterestView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
let animation = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.pointOfInterestView.transform = .identity
self.pointOfInterestView.alpha = 1
}
animation.startAnimation()
let pointOfInterestHalfCompletedWorkItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
let animation = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.pointOfInterestView.alpha = 0.5
}
animation.startAnimation()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: pointOfInterestHalfCompletedWorkItem)
self.pointOfInterestHalfCompletedWorkItem = pointOfInterestHalfCompletedWorkItem
}
}
Since I thought it's a threading issue, I tried to change the label text & to show the yellow square in the main thread by writing DispatchQueue.main.asyncAfter, but it didn't work. Also, I was not sure if it becomes serial queue or concurrent queue if I perform both
loop the timer function and constantly updating label text
detect UI touch event and show yellow square
Since UI updates are performed in the main thread, I guess I need to figure out a way to share the main thread for the timer function and user touch event...
If someone knows a clue to solve this problem, please let me know.
It isn't a threading issue. It is an auto layout issue.
Presumably you have positioned the yellow square view in your storyboard using constraints.
You are then modifying the yellow square's frame directly by modifying the center property; this has no effect on the constraints that are applied to the view. As soon as the next auto layout pass runs (triggered by the text changing, for example) the constraints are reapplied and the yellow square jumps back to where your constraints say it should be.
You have a couple of options;
Compute the destination point offset from the center of the view and then apply those offsets to the constant property of your two centering constraints
Add the yellow view programatically and position it by setting its frame directly. You can then adjust the frame by modifying center as you do now.

Why isn't the draw method of the custom CALayer called earlier?

Issue
I trigger random animations on a selection of UIViews using a Timer. All animations work as intended but one where the draw method of a CALayer is called after the exit of the timer selector.
Detailed Description
For the sake of clarity and simplification, let me schematise the actions performed.
All the animations I have created so far work as intended: they are a combination of CABasicAnimations on existing subviews/sublayers or new ones added to the selected view hierarchy. They are coded as an UIView extension, so that they can be called on any views, irrespectively of the view or the view controller the view is in. Well, all work except one.
I have indeed created a custom CALayer class which consists in drawing patterns on a CALayer. In an extension of this custom class, there is a method to animate those patterns (see hereafter the code). So all in all, when I reach the step/method animate selected view and run this particular animation, here is what should happen:
a method named animatePattern is called
this method adds the custom CALayer class to the selected view and then calls the animation extension of this layer
The issue: if with all the other animations, all the drawings are performed prior to the exit of the animate selected view step/method, in that particular case, the custom CALayer class draw method is called after the exit of the performAnimation method, which in turn results in the crash of the animation.
I should add that I have tried the custom CALayer class animation in a separate and simplified playground and it works well (I add the custom layer to a UIView in the UIViewController's viewDidLoad method and then I call the layer animation in the UIViewController's viewDidAppear method.)
The code
the method called by animate selected view step/method:
func animatePattern(for duration: TimeInterval = 1) {
let patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
patternLayer.play(for: duration)
}
(note that this method is in a UIView extension, therefore self here represents the UIView on which the animation has been called)
the simplified custom CALayer Class:
override var bounds: CGRect {
didSet {
self.setNeedsDisplay()
}
// Initializers
override init() {
super.init()
self.setNeedsDisplay()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(effectType: EffectType){
super.init()
// sets various properties
self.setNeedsDisplay()
}
// Drawing
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
guard self.frame != CGRect.zero else { return }
self.masksToBounds = true
// do the drawings
}
the animation extension of the custom CALayer class:
func play(for duration: Double, removeAfterCompletion: RemoveAfterCompletion = .no) {
guard self.bounds != CGRect.zero else {
print("Cannot animate nil layer")
return }
CATransaction.begin()
CATransaction.setCompletionBlock {
if removeAfterCompletion.removeStatus {
if case RemoveAfterCompletion.yes = removeAfterCompletion { self.fadeOutDuration = 0.0 }
self.fadeToDisappear(duration: &self.fadeOutDuration)
}
}
// perform animations
CATransaction.commit()
}
Attempts so far
I have tried to force draw the layer by inserting setNeedsDisplay / setNeedsLayout at various places in the code but it does not work: debugging the custom CALayer class's draw method is constantly reached after the exit of the performAnimation method, whilst it should be called when the layer's frame is modified in the animatePattern method. I must miss something quite obvious but I am currently running in circles and I'd appreciate a fresh pair of eyes on it.
Thank you in advance for taking the time to consider this issue!
Best,
You override the draw(_ context) as the UIView can be the delegate of CALayer.
UIView: {
var patternLayer : PatternLayer
func animatePattern(for duration: TimeInterval = 1) {
patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
}
func draw(_ layer: CALayer, in ctx: CGContext){
layer.draw(ctx)
if NECESSARY { patternLayer.play(for: duration)}
}
}
As it is frequently the case, when you take the time to write down your issue, new ideas start to pop up. In fact, I remembered that self.setNeedsDisplay() informs the system that the layer needs to be (re)drawn but it does not (re)draw it at once: it will be done during the next refresh. It seems then that the refresh occurs after the end of the cycle for that specific animation. In order to overcome this is issue, I first added a call for the display method right after the patternLayer bounds are set, and it worked, but given #Matt comment, I changed the solution by calling displayIfNeeded method instead. It works as well.
func animatePattern(for duration: TimeInterval = 1) {
let patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
// asks the system to draw the layer now
patternLayer.displayIfNeeded()
patternLayer.play(for: duration)
}
Again, should anyone come up with another more elegant solution/explanation, please do not refrain to share!

Interrupt UIView transition

Is there any possibility to interrupt a UIView.transition at its current state on iOS?
To give you a bit of context: I have a UIButton where I need to animate the title color - the animation can have different delays attached to it, though. Since UIView.transition does not support delays, I simply delayed execution of the entire block:
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
UIView.transition(with: button, duration: 0.4, options: [.beginFromCurrentState, .allowUserInteraction, .transitionCrossDissolve], animations: {
button.setTitleColor(selected ? .red : .black, for: .normal)
}, completion: nil)
}
The issue here is that, if this code is executed in quick succession with different delays, the outcome can be unexpected. So for example this could be called with selected=true, delay=0.5 and immediately after that with selected=false, delay=0.0. In this scenario, the button would end up red even though it should be black.
Therefore, I am looking for a method to either have UIView.transform with a delay, interrupt a UIView.transform or make setTitleColor() animatable through UIView.animate.
I actually created a framework to do this specifically, you can trigger animations relative to time progress, or value interpolation progress. Framework and documentation can be found here:
https://github.com/AntonTheDev/FlightAnimator
The following examples assume you a CATextLayer backed view, as this is not a question about how to set that up, here is a small example to get you started:
class CircleProgressView: UIView {
.....
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
}
Example 1:
Assuming you have a CATextLayer backed view, we can animate the it's foreground color as follows, and trigger another animation relative to time:
var midPointColor = UIColor.orange.cgColor
var finalColor = UIColor.orange.cgColor
var keyPath = "foregroundColor"
textView.animate { [unowned self] (a) in
a.value(midPointColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)
// This will trigger the interruption
// at 0.5 seconds into the animation
animator.triggerOnProgress(0.5, onView: self.textView, animator: { (a) in
a.value(finalColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)
})
}
Example 2:
Just as in the prior example, once you have a CATextLayer backed view, we can animate it's foregroundColor relative to the progress matrix of the RBG values, calculated based on the magnitude progress of the 3 values:
var midPointColor = UIColor.orange.cgColor
var finalColor = UIColor.orange.cgColor
var keyPath = "foregroundColor"
textView.animate { [unowned self] (a) in
a.value(midPointColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)
// This will trigger the interruption
// when the magnitude of the starting point
// is equal to 0.5 relative to the final point
a.triggerOnValueProgress(0.5, onView: self.textView, animator: { (a) in
a.value(finalColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)
})
}
Hope this helps :)

How to prevent an animation from completing when user leaves and returns to app

I am making an app where I have a button who is moving from one side of the screen to the other. I have created a pause button which pauses the animation once that button is selected. This button and function works fine. I also added a function which determines if the user has exited the application. In this function, I added the pause button function. However, when the user returns to the app after exiting, it shows the pause screen, but the animation is completed. Here is the code:
#IBAction func pauseButton(sender: UIButton) {
let button = self.view.viewWithTag(1) as? UIButton!
let layer = button!.layer
pauseLayer(layer) // Pausing the animation
view2.hidden = false // Showing pause screen
}
func pauseLayer(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
#IBAction func startButton(sender: UIButton) {
let button = UIButton()
button.tag = 1 // so I can retrieve it in the pause function
// Extra code to make the button look nice
button.translatesAutoresizingMaskIntoConstraints = false // Allow constraints
let topAnchorForButton = button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 400)
NSLayoutConstraint.activateConstraints([ // Adding constraints
topAnchorForButton,
button.leadingAnchor.constraintEqualToAnchor(view.topAnchor, constant: 120),
button.widthAnchor.constraintEqualToConstant(65),
button.heightAnchor.constraintEqualToConstant(65)
])
self.view.layoutIfNeeded()
[UIView.animateWithDuration(5.0, delay: 0.0, options: [.CurveLinear, .AllowUserInteraction], animations: {
topAnchorForButton.constant = 0
self.view.layoutIfNeeded()
}, completion: nil )] // the animation
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseButton:", name: UIApplicationWillResignActiveNotification, object: nil) // When exists app
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseButton:", name: UIApplicationDidEnterBackgroundNotification, object: nil) // when goes to home screen
}
When I re-enter the app, the animation is completed.
Here's is a code I've tried which only plays and stops the animation. I've also added something in the App Delegate but it still doesn't work:
ViewController:
class Home: UIViewController {
func pauseAnimation(){
let button = self.view.viewWithTag(1) as? UIButton!
let layer = button!.layer
pauseLayer(layer) // Pausing the animation
}
#IBAction func pauseButton(sender: UIButton) {
pauseAnimation()
}
func pauseLayer(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
#IBAction func startButton(sender: UIButton) {
let button = UIButton()
button.tag = 1 // so I can retrieve it in the pause function
// Extra code to make the button look nice
button.setTitle("Moving", forState: .Normal)
button.setTitleColor(UIColor.redColor(), forState: .Normal)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false // Allow constraints
let topAnchorForButton = button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 400)
NSLayoutConstraint.activateConstraints([ // Adding constraints
topAnchorForButton,
button.leadingAnchor.constraintEqualToAnchor(view.topAnchor, constant: 120),
button.widthAnchor.constraintEqualToConstant(65),
button.heightAnchor.constraintEqualToConstant(65)
])
self.view.layoutIfNeeded()
[UIView.animateWithDuration(10.0, delay: 0.0, options: [.CurveLinear, .AllowUserInteraction], animations: {
topAnchorForButton.constant = 0
self.view.layoutIfNeeded()
}, completion: nil )] // the animation
}
App Delegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var vc = Home()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
}
func applicationWillResignActive(application: UIApplication) {
vc.pauseAnimation()
}
Just in case you want to know, here is the code for resuming the animation:
#IBAction func resume(sender: UIButton) {
let button = self.view.viewWithTag(100) as? UIButton!
let layer = button!.layer
resumeLayer(layer)
}
func resumeLayer(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
layer.beginTime = timeSincePause
}
When I exit the app, it says that button or/and layer is nil (fatal error... optional value is nil), so it crashes.
Please help. Thanks in advance... Anton
You need to know a little about how animations work under the hood. This is very well explained in WWDC session 236 'Building Interruptible and Responsive Interactions' (the part you are interested in starts at 17:45). In the meantime, read about CABasicAnimation, and checkout the presentationLayer property on CALayer (you will need to drop down to CALayer for what you want to achieve, and maybe you will need to implement the animations using CA(Basic)Animation, although maybe UIKit's animation methods might still be able to do the trick, if you've structured your code well).
Basically, a view or layer will get the end-values that you set as part of an animation immediately. Then the animation starts, from begin to end. If the animation gets interrupted, say by leaving the app, upon return the view gets drawn with the values it has (which are the end-values, as the animation is gone).
What you want is that you capture the value of the view at the point in the animation when you leave the app, save it, and start a new animation from that point to the original endpoint when your app comes back to the foreground. You can get the current position of a CALayer (not UIView) by asking for the frame, or transform, whatever you need, from the CALayer's presentationLayer property.
I had this problem when building a custom refreshControl for use in any `UIScrollView``. This code is on Github, the relevant code for you is here (its Objective-C, but it is not very complicated, so you should be able to follow along and re-implement what you need in Swift):
JRTRefreshControlView.m, specifically:
-(void)willMoveToWindow:(UIWindow *)newWindow This gets called when the user moves the app to the background, or when the view is removed from the superView (newWindow will be nil).
And the actual implementation of an animation in a subclass is here:
JRTRotatingRefreshControlView.m, specifically the
- (void) resumeRefreshingState method is what you want to look at.
(there is also saveRefreshingState, which is where you can save the presentation-values from the presentationLayer, but it is not implemented for my animations).
Try putting this code in:
View controller:
pauseAnimation(){
let button = self.view.viewWithTag(1) as? UIButton!
let layer = button!.layer
pauseLayer(layer) // Pausing the animation
view2.hidden = false // Showing pause screen
}
AppDelegate:
func applicationWillResignActive(application: UIApplication) {
vc.pauseAnimation()
}
To be able to access functions from view controllers in the appDelegate go here: link
Have a look here. Listing 5-4 shows an example of how to pause and resume animation. You should move the code out of AppDelegate, and instead the view controller (Home) should handle going into and out of the background. Within Home's viewDidLoad or viewWillAppear:
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "resigningActive",
name: UIApplicationWillResignActiveNotification,
object: nil
)
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "becomeActive",
name: UIApplicationDidBecomeActiveNotification,
object: nil
)
You will need to removeObserver, probably in Home's deinit. Then you have:
func resigningActive() {
pauseLayer(button!.layer)
}
func becomeActive() {
resumeLayer(button!.layer)
}
This code doesn't need another button to pause/resume, but you could also do it that way if you want. You will probably need the observers to be defined in viewWillAppear, since it looks like your code will reference outlets than don't yet exist in viewDidLoad. This will mean you must code to only hook up the observers the first time viewWillAppear is called, or you will be adding multiple observers for the same event.
Your pauseLayer should also write pausedTime to NSUserDefaults, and resumeLayer should read it back, along the lines of (untested):
// Write
NSUserDefaults.standarduserDefaults().setDouble(Double(pausedTime), forKey: "pausedTime")
// Read
pausedTime = CFTimeInterval(NSUserDefaults.standarduserDefaults().doubleForKey("pausedTime"))

Why isn't my UISlider animating?

According to Apple's documentation, it is possible to programmatically set the value of a UISlider with a smooth animation. I'm attempting to do so from a custom view controller, the UI is being defined from a storyboard.
The Context
In my example I'm attempting to update the slider value from a custom view controller, the UI is being defined from a storyboard. The example only renders a single slider.
When the user releases the slider, the value is reset to 0.
The Code
import UIKit
class ViewController: UIViewController {
#IBOutlet var mySlider: UISlider!
#IBAction func resetSlider() {
mySlider.setValue(0, animated:true)
NSLog("Reset!")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
resetSlider is linked to the Touch Up Inside event.
The Problem
When resetSlider is called the value does change on the interface, but it does not animate (the value simply "jumps" to 0). My goal is to have the value gracefully shift back to zero.
Note: "Reset!" only displays once (per click), which indicates that resetSlider is not being called multiple times.
Why isn't this UISlider animating?
The Video
Since IB is so visual, here is a video of the situation, password is code
the setValue animated parameter doesn't actually perform an animation, but rather it enables animation.
To trigger the animation, you need to use UIView.animateWithDuration, and pass in the setValue command as the animation:
UIView.animateWithDuration(0.2, animations: {
self.mySlider.setValue(0, animated:true)
})
Swift 5
UIView.animate(withDuration: 0.2, animations: {
self.mySlider.setValue(0, animated:true)
})
The solution of #Slifty is correct. But if you want UISlider is as smooth as defauft, so you can decrease the time animation to below 0.2. It may work such as 0.1/2 or 0.1/3.

Resources