Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I animate fadeIn fadeOut effect in NSTextField when changing its text?

I am trying to write a category for NSTextField which will add a new method setAnimatedStringValue. This method is supposed to nicely fade-out the current text, then set the new text and then fade that in.

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 above code does not work at all. The only effect I see is the flickering of a progress bar on the same window. That is possibly because I am blocking the main runloop while trying to "animate" it.

Please suggest what is wrong with the above code.

Additional note:

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

like image 843
AppleGrew Avatar asked Dec 02 '22 20:12

AppleGrew


2 Answers

I was poking around a bit after posting the previous answer. I'm leaving that answer because it corresponds closely to the code you posted, and uses NSViewAnimation. I did, however, come up with a considerably more concise, albeit slightly harder to read (owing to block parameter indentation) version that uses NSAnimationContext instead. Here 'tis:

#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 get access to the CAMediaTimingFunction class used here for specifying non-default timing functions using this API, you'll need to include QuartzCore.framework in your project.

Also on GitHub.

like image 98
ipmcc Avatar answered Dec 19 '22 11:12

ipmcc


For Swift 3, here are two convenient setText() and setAttributedText() extension methods that fade over 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 as 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)
like image 21
Lars Blumberg Avatar answered Dec 19 '22 12:12

Lars Blumberg