3

I'm new to programming - but I've made strides learning Swift for iOS in the last two months. I'm making a simple typing game - and the way I've structured my project is that I have a hidden UITextView that detects the character pressed by the player, and I match that character string with a visible UITextView's character string.

What I'm looking to do now is to add some sort of animation - I'd like the individual letters to fade in/out

I've created an attributed string, added it to a UITextView, and I just can't figure out a way to animate a specific range in the string. I've tried using something along the lines of this:

UIView.animateWithDuration(1, delay: 0.5, options: .CurveEaseOut, animations: {
    self.stringForPlayer.addAttribute(
        NSForegroundColorAttributeName,
        value: UIColor.greenColor(),
        range: NSRange(location: self.rangeOfText, length: 1))
    self.textViewForPlayer.attributedText = self.textForPlayer
}, completion: { finished in
    println("FINISHED")
})

with no luck. I figure maybe UIView animations are only on the view object itself and can't be used on the attributed string. Any ideas or even workarounds to make this happen? Any help is appreciated, thanks a lot!

7
  • You could try to mask the string with a UIView object, then change its alpha with animation. Commented Jan 30, 2015 at 22:14
  • @DinoTw thanks for the reply. Could you elaborate a bit on how I'd mask the string? Sorry I'm quite new at this. You're saying I could create a new UIView object, and position it on top of the UITextView, and animate that instead right? Commented Jan 30, 2015 at 22:41
  • Or just use transitionWithView instead of animateWithDuration and no extra view is needed. Commented Jan 30, 2015 at 23:01
  • Thanks! transitionWithView works for the color. I put that in the example because I couldn't figure out how to scale the size of a character in the string - so i put the closest example i could think of which was the color. Could you suggest how I'd go about making the letters "pop" (scale up and back down) as well? Commented Jan 30, 2015 at 23:12
  • That would require, IMHO, a non-trivial amount of code, figuring out the CGRect for the relevant parts to be animated, taking snapshot of before and after appearance, animating the transition of snapshots, etc. You certainly can do it, but you should ask yourself how much work you want to engage in for a simple effect. Commented Jan 30, 2015 at 23:37

1 Answer 1

10

You can use transition(with:...) to do an animation. In this case, fading the word ipsum into green. E.g. in Swift 3 and later:

let range = (textView.text as NSString).range(of: "ipsum")
if range.location == NSNotFound { return }

let string = textView.attributedText.mutableCopy() as! NSMutableAttributedString
string.addAttribute(.foregroundColor, value: UIColor.green, range: range)

UIView.transition(with: textView, duration: 1.0, options: .transitionCrossDissolve, animations: {
    self.textView.attributedText = string
})

Originally, you also asked about having the text grow and shrink during this animation and that’s more complicated. But you can search for the text, find the selectionRects, take snapshots of these views, and animate their transform. For example:

func growAndShrink(_ searchText: String) {
    let beginning = textView.beginningOfDocument
    
    guard
        let string = textView.text,
        let range = string.range(of: searchText),
        let start = textView.position(from: beginning, offset: string.distance(from: string.startIndex, to: range.lowerBound)),
        let end = textView.position(from: beginning, offset: string.distance(from: string.startIndex, to: range.upperBound)),
        let textRange = textView.textRange(from: start, to: end)
    else {
        return
    }
    
    textView.selectionRects(for: textRange)
        .forEach { selectionRect in
            guard let snapshotView = textView.resizableSnapshotView(from: selectionRect.rect, afterScreenUpdates: false, withCapInsets: .zero) else { return }
            
            snapshotView.frame = view.convert(selectionRect.rect, from: textView)
            view.addSubview(snapshotView)
            
            UIView.animate(withDuration: 1, delay: 0, options: .autoreverse, animations: {
                snapshotView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
            }, completion: { _ in
                snapshotView.removeFromSuperview()
            })
    }
}

And

growAndShrink("consectetaur cillium”)

Will result in:

grow

If you are animating just the color of the text, you may want to fade it to clear before fading it to the desired color (making it "pop" a little more), you could use the completion block:

func animateColor(of searchText: String) {
    let range = (textView.text as NSString).range(of: searchText)
    if range.location == NSNotFound { return }
    
    let string = textView.attributedText.mutableCopy() as! NSMutableAttributedString
    string.addAttribute(.foregroundColor, value: UIColor.clear, range: range)
    
    UIView.transition(with: textView, duration: 0.25, options: .transitionCrossDissolve, animations: {
        self.textView.attributedText = string
    }, completion: { _ in
        string.addAttribute(.foregroundColor, value: UIColor.red, range: range)
        UIView.transition(with: self.textView, duration: 0.25, options: .transitionCrossDissolve, animations: {
            self.textView.attributedText = string
        })
    })
}

Resulting in:

red

For previous versions of Swift, see prior revision of this answer.

Sign up to request clarification or add additional context in comments.

5 Comments

great! Could you post an answer of how to animate the size of the characters to make them scale up and back down? (a popping animation)
just elaborating on the solution - is there a way to ensure that each run of the animation block is completed before the next one begins? These animations are triggered quickly in succession.
Increase the duration of each if you want. And make sure the second one is inside the completion block of the first, as shown above.
sorry what i meant was - what if I wanted the whole animation to finish before the next time both these blocks are called? As in, both these blocks are in a function, and the function is called quickly in succession. The second call always interrupts the first resulting in an instant completion of the first call's animations. Thanks in advance
The easy solution is to continue putting subsequent transitionWithView calls inside the prior one's completion block. A more complicated solution would entail wrapping your animation in a NSOperation subclass like this answer (except use transitionWithView instead of animateWithDuration).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.