Learning From The Best: Replicating The Secret iOS App Text Animation

One of the best ways of really learning iOS and / or any other programming language is to replicate what some of the best app developers do in their app. Ever since I downloaded Secret, I’ve been pleasantly surprised by their awesome text animation when a new secret loads:

So over the past day, I’ve been working on replicating it. I create a UILabel and played around with setting an NSAttributedString at the label’s text, but got stuck there. So I posted a question on StackOverlow, and immediately got a new lead (love StackOverflow!).

To make this animation work, I needed to set two labels on top of each other and then have one label fade while the other one became visible. One person even provided some code on how to do part of it! So after playing around with different versions of this, I ended up with something that looks very close to Secret’s text animation (at least I hope so!):

Here is how I made it work:

The Setup

To get started, place two UILabels on top of each other, and create outlets for them in your ViewController.

iOS Storyboard Two Labels

In your ViewConroller, you also need to add the following additional properties:

  • attributedString – to keep track of the latest attributed string with randomely generated shades of white
  • numWhiteCharacters – we’re going to keep animating until all the colors in the attributed string are solid white (alpha = 1), so have to keep track of this number
  • topLabel – keep track of the label on the top
  • bottomLabel – keep track of the label on the bottom
@interface NTRViewController ()

@property (weak, nonatomic) IBOutlet UILabel *textLabel1;
@property (weak, nonatomic) IBOutlet UILabel *textLabel2;

@property (strong, nonatomic) NSAttributedString *attributedString;
@property (assign, nonatomic) NSUInteger numWhiteCharacters;

@property (strong, nonatomic) UILabel *topLabel;
@property (strong, nonatomic) UILabel *bottomLabel;

@end

Initial Animation

The first animation will simply generate an attributedString with letters that are each a different opacity of white to set up for subsequent animations:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.textLabel1.alpha = 0;
    self.textLabel2.alpha = 0;
    
    // this is based on the view hierarchy in the storyboard
    self.topLabel = self.textLabel2;
    self.bottomLabel = self.textLabel1;
    
    NSString *mySecretMessage = @"This is a my replication of Secret's text animation. It looks like one fancy label, but it's actually two UITextLabels on top of each other! What do you think?";
    
    self.numWhiteCharacters = 0;
    
    NSAttributedString *initialAttributedText = [self randomlyFadedAttributedStringFromString:mySecretMessage];
    self.topLabel.attributedText = initialAttributedText;
    
    __weak NTRViewController *weakSelf = self;
    [UIView animateWithDuration:0.1 animations:^{
        weakSelf.topLabel.alpha = 1;
    } completion:^(BOOL finished) {
      // continue the animation from here
    }];
}

- (void)updateNumWhiteCharactersForColor:(UIColor *)color
{
    CGFloat alpha = CGColorGetAlpha(color.CGColor);
    if (alpha == 1.0) {
        self.numWhiteCharacters++;
    }
}

- (UIColor *)whiteColorWithClearColorProbability:(NSInteger)probability
{
    UIColor *color;
    NSInteger colorIndex = arc4random() % probability;
    if (colorIndex != 0) {
        color = [UIColor clearColor];
    } else {
        color = [self whiteColorWithMinAlpha:0];
    }
    return color;
}

- (UIColor *)whiteColorWithMinAlpha:(CGFloat)minAlpha
{
    NSInteger randomNumber = minAlpha * 100 + arc4random_uniform(100 - minAlpha * 100 + 1);
    CGFloat randomAlpha = randomNumber / 100.0;
    return [UIColor colorWithWhite:1.0 alpha:randomAlpha];
}

Continue Animating

Finally, this is the harder part. Now that you have the initial letters on the screen, all with different opacities of white or a clear color, you need to keep doing this same animation over and over again until all the letters are fully white.

- (void)viewDidLoad
{
    // see above for full implementation
    
    __weak NTRViewController *weakSelf = self;
    [UIView animateWithDuration:0.1 animations:^{
        weakSelf.topLabel.alpha = 1;
    } completion:^(BOOL finished) {
        weakSelf.attributedString = [weakSelf randomlyFadedAttributedStringFromAttributedString:initialAttributedText];
        weakSelf.bottomLabel.attributedText = weakSelf.attributedString;
        [weakSelf performAnimation];
    }];
}

- (void)performAnimation
{
    __weak NTRViewController *weakSelf = self;
    [UILabel animateWithDuration:0.1
                          delay:0
                        options:UIViewAnimationOptionCurveEaseIn
                     animations:^{
                         weakSelf.bottomLabel.alpha = 1;
                     } completion:^(BOOL finished) {
                         [weakSelf resetLabels];
                         
                         // keep performing the animation until all letters are white
                         if (weakSelf.numWhiteCharacters == [weakSelf.attributedString length]) {
                             [weakSelf.bottomLabel removeFromSuperview];
                         } else {
                             [weakSelf performAnimation];
                         }
                     }];
}

- (void)resetLabels
{
    [self.topLabel removeFromSuperview];
    self.topLabel.alpha = 0;
    
    // recalculate attributed string with the new white color values
    self.attributedString = [self randomlyFadedAttributedStringFromAttributedString:self.attributedString];
    self.topLabel.attributedText = self.attributedString;
    
    [self.view insertSubview:self.topLabel belowSubview:self.bottomLabel];
    
    //  the top label is now on the bottom, so switch
    UILabel *oldBottom = self.bottomLabel;
    UILabel *oldTopLabel = self.topLabel;
    
    self.bottomLabel = oldTopLabel;
    self.topLabel = oldBottom;
}

- (NSAttributedString *)randomlyFadedAttributedStringFromAttributedString:(NSAttributedString *)attributedString
{
    NSMutableAttributedString *mutableAttributedString = [attributedString mutableCopy];
    
    __weak NTRViewController *weakSelf = self;
    for (NSUInteger i = 0; i < attributedString.length; i ++) {
        [attributedString enumerateAttribute:NSForegroundColorAttributeName
                                     inRange:NSMakeRange(i, 1)
                                     options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
                                  usingBlock:^(id value, NSRange range, BOOL *stop) {
                                      UIColor *initialColor = value;
                                      UIColor *newColor = [weakSelf whiteColorFromInitialColor:initialColor];
                                      if (newColor) {
                                          [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:newColor range:range];
                                          [weakSelf updateNumWhiteCharactersForColor:newColor];
                                      }
                                  }];
        
    }
    
    return [mutableAttributedString copy];
}

- (UIColor *)whiteColorFromInitialColor:(UIColor *)initialColor
{
    UIColor *newColor;
    if ([initialColor isEqual:[UIColor clearColor]])
    {
        newColor = [self whiteColorWithClearColorProbability:4];
    } else {
        CGFloat alpha = CGColorGetAlpha(initialColor.CGColor);
        if (alpha != 1.0) {
            newColor = [self whiteColorWithMinAlpha:alpha];
        }
    }
    return newColor;
}

Well, what looks like a simple and elegant animation on the outside actually takes some serious brain power and time to actually make! Kudos to the Secret iOS developers and designers for making it happen!

Are there any other cool animations or UI elements you’ve noticed in an iOS app recently? Let me know in the comments and I’ll try to replicate it 🙂

You can view the full source code on Github.

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • Pingback: Good enough isn’t: Replicating the Secret iOS App Text View | Cogito Ergo Sum()

  • kenshin03

    Thanks for this wonderful post Natasha. I was inspired to re-create Secret’s Text View after seeing this. Please check it out and comment! https://github.com/kenshin03/KTSecretTextView

  • Anton Bukov

    Hello Natasha, nice replication!

    It will be perfect to implement this behavior in separate class called for example **LabelPerCharAppearIntention. So it will be possible to configure this animation right from Storyboard.

    To know more about intentions read this:
    http://bendyworks.com/geekville/articles/2014/2/single-responsibility-principle-ios

    And this:
    http://chris.eidhof.nl/posts/intentions.html
    You are welcome to add your own intentions to repo:
    https://github.com/k06a/ABIntentions

    • Thanks. I’ll take a look at the intentions blog post when I have a bit more time this weekend.

  • Pedro Góes

    Nice animation!

  • Jilian Scott

    this is working wonderfully for me. event changed animation from using white font to my custom color. the only issue is at some point the alignment changes from leftAlignment to centered. I can’t tell where this change is happening and my attempts at programmatically fixing it is not working. any advice?

  • Nathan Rima

    Hi Natasha – I like this animation and have a follow up question – is this a similar approach you would use to create a typewriter effect and a blinking cursor?

    I have started teaching myself mobile app and have jumped straight to learning Swift.

    Cheers

    Nathan

  • Mike

    Hi Natasha, will there also a Swift version of your code? I like your idea really much, but as a beginner of iOS developing it seems hard to understand how the code is written in Swift.

    • Usman Amjed

      did you find a swift version?

  • Usman Amjed

    Hi natasha,

    Have you created a swift version of your code?

  • Manish Kumar

    Hii.. Do you have any swift implementation for the above code?