How do I animate the fadeIn fadeOut effect in an NSTextField when its text changes?

I am trying to write a category for NSTextField that will add a new setAnimatedStringValue method. This method is supposed to beautifully disappear from the current text, then install new text and then disappear.

Below is my implementation: -

 - (void) setAnimatedStringValue:(NSString *)aString { if ([[self stringValue] isEqualToString:aString]) { return; } NSMutableDictionary *dict = Nil; NSViewAnimation *fadeOutAnim; dict = [NSDictionary dictionaryWithObjectsAndKeys:self, NSViewAnimationTargetKey, NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, nil]; fadeOutAnim = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects: dict, nil]]; [fadeOutAnim setDuration:2]; [fadeOutAnim setAnimationCurve:NSAnimationEaseOut]; [fadeOutAnim setAnimationBlockingMode:NSAnimationBlocking]; NSViewAnimation *fadeInAnim; dict = [NSDictionary dictionaryWithObjectsAndKeys:self, NSViewAnimationTargetKey, NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, nil]; fadeInAnim = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects: dict, nil]]; [fadeInAnim setDuration:3]; [fadeInAnim setAnimationCurve:NSAnimationEaseIn]; [fadeInAnim setAnimationBlockingMode:NSAnimationBlocking]; [fadeOutAnim startAnimation]; [self setStringValue:aString]; [fadeInAnim startAnimation]; } 

Needless to say, but the code above does not work at all. The only effect that I see is the flickering of the progress bar in the same window. Perhaps because I block the main runloop when I try to “revive” it.

Please report what is wrong with the code above.

Note:

setAnimatedStringValue always called by NSTimer , which is added to the main NSRunLoop .

+4
source share
3 answers

I'll take a hit:

I found a couple of problems here. Firstly, all this is configured to block, so it blocks the main thread for 5 seconds. This will translate to user as SPOD / freezes. You probably want this to be non-blocking, but this will require a little extra mechanism.

In addition, you are using NSAnimationEaseOut for a fading effect that occurs with a known bug that NSAnimationEaseOut animation to run in the opposite direction. (Google is for “NSAnimationEaseOut back,” and you can see that many have run NSAnimationEaseIn this problem.) I used NSAnimationEaseIn for both curves for this example.

I got this work for a trivial example with non-blocking animations. I'm not going to say that this is an ideal approach (I posted a second answer, which is probably better), but it works and, I hope, will be a jump for you. Here's the gist:

 @interface NSTextField (AnimatedSetString) - (void) setAnimatedStringValue:(NSString *)aString; @end @interface SOTextFieldAnimationDelegate : NSObject <NSAnimationDelegate> - (id)initForSettingString: (NSString*)newString onTextField: (NSTextField*)tf; @end @implementation NSTextField (AnimatedSetString) - (void) setAnimatedStringValue:(NSString *)aString { if ([[self stringValue] isEqual: aString]) { return; } [[[SOTextFieldAnimationDelegate alloc] initForSettingString: aString onTextField: self] autorelease]; } @end @implementation SOTextFieldAnimationDelegate { NSString* _newString; NSAnimation* _fadeIn; NSAnimation* _fadeOut; NSTextField* _tf; } - (id)initForSettingString: (NSString*)newString onTextField: (NSTextField*)tf { if (self = [super init]) { _newString = [newString copy]; _tf = [tf retain]; [self retain]; // we'll autorelease ourselves when the animations are done. _fadeOut = [[NSViewAnimation alloc] initWithViewAnimations: @[ (@{ NSViewAnimationTargetKey : tf , NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect})] ]; [_fadeOut setDuration:2]; [_fadeOut setAnimationCurve: NSAnimationEaseIn]; [_fadeOut setAnimationBlockingMode:NSAnimationNonblocking]; _fadeOut.delegate = self; _fadeIn = [[NSViewAnimation alloc] initWithViewAnimations: @[ (@{ NSViewAnimationTargetKey : tf , NSViewAnimationEffectKey : NSViewAnimationFadeInEffect})] ]; [_fadeIn setDuration:3]; [_fadeIn setAnimationCurve:NSAnimationEaseIn]; [_fadeIn setAnimationBlockingMode:NSAnimationNonblocking]; [_fadeOut startAnimation]; } return self; } - (void)dealloc { [_newString release]; [_tf release]; [_fadeOut release]; [_fadeIn release]; [super dealloc]; } - (void)animationDidEnd:(NSAnimation*)animation { if (_fadeOut == animation) { _fadeOut.delegate = nil; [_fadeOut release]; _fadeOut = nil; _tf.hidden = YES; [_tf setStringValue: _newString]; _fadeIn.delegate = self; [_fadeIn startAnimation]; } else { _fadeIn.delegate = nil; [_fadeIn release]; _fadeIn = nil; [self autorelease]; } } @end 

It would be very nice if the API block were implemented for this ... it would save the need to implement this delegate object.

I put the whole project on GitHub .

+1
source

I’ve already scrambled out several times after posting the previous answer. I leave this answer because it matches your code and uses NSViewAnimation . However, I came up with a much more concise, albeit slightly more difficult to read (due to indentation in the block parameter) version that uses the NSAnimationContext . Here:

 #import <QuartzCore/QuartzCore.h> @interface NSTextField (AnimatedSetString) - (void) setAnimatedStringValue:(NSString *)aString; @end @implementation NSTextField (AnimatedSetString) - (void) setAnimatedStringValue:(NSString *)aString { if ([[self stringValue] isEqual: aString]) { return; } [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { [context setDuration: 1.0]; [context setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]]; [self.animator setAlphaValue: 0.0]; } completionHandler:^{ [self setStringValue: aString]; [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { [context setDuration: 1.0]; [context setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]]; [self.animator setAlphaValue: 1.0]; } completionHandler: ^{}]; }]; } @end 

Note. . To access the CAMediaTimingFunction class used here to specify default synchronization functions using this API, you need to include QuartzCore.framework in your project.

Also on GitHub .

+8
source

For Swift 3, here are two convenient extension methods setText() and setAttributedText() that disappear from one text to another:

 import Cocoa extension NSTextField { func setStringValue(_ newValue: String, animated: Bool = true, interval: TimeInterval = 0.7) { guard stringValue != newValue else { return } if animated { animate(change: { self.stringValue = newValue }, interval: interval) } else { stringValue = newValue } } func setAttributedStringValue(_ newValue: NSAttributedString, animated: Bool = true, interval: TimeInterval = 0.7) { guard attributedStringValue != newValue else { return } if animated { animate(change: { self.attributedStringValue = newValue }, interval: interval) } else { attributedStringValue = newValue } } private func animate(change: @escaping () -> Void, interval: TimeInterval) { NSAnimationContext.runAnimationGroup({ context in context.duration = interval / 2.0 context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) animator().alphaValue = 0.0 }, completionHandler: { change() NSAnimationContext.runAnimationGroup({ context in context.duration = interval / 2.0 context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) self.animator().alphaValue = 1.0 }, completionHandler: {}) }) } } 

Call them the following:

 var stringTextField: NSTextField var attributedStringTextField: NSTextField ... stringTextField.setStringValue("New Text", animated: true) ... let attributedString = NSMutableAttributedString(string: "New Attributed Text") attributedString.addAttribute(...) attributedStringTextField.setAttributedStringValue(attributedString, animated: true) 
+3
source

All Articles