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!

McKinley
  • 1,123
  • 1
  • 8
  • 18
Chris
  • 471
  • 1
  • 8
  • 23
  • You could try to mask the string with a UIView object, then change its alpha with animation. – Dino Tw Jan 30 '15 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? – Chris Jan 30 '15 at 22:41
  • Or just use `transitionWithView` instead of `animateWithDuration` and no extra view is needed. – Rob Jan 30 '15 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? – Chris Jan 30 '15 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. – Rob Jan 30 '15 at 23:37
  • Fair enough. Seeing as I've only been learning to code for a couple months I will take your advice and just use the color fade animation. I'll alter my question and accept your answer! – Chris Jan 30 '15 at 23:39
  • For the sake of future readers, I’ve added example of the growing/shrinking animation to my answer below. – Rob Feb 06 '19 at 21:36

1 Answers1

9

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 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) – Chris Jan 30 '15 at 23:26
  • 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. – Chris Feb 02 '15 at 22:46
  • 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. – Rob Feb 02 '15 at 23:36
  • 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 – Chris Feb 03 '15 at 00:13
  • 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](http://stackoverflow.com/a/24457815/1271826) (except use `transitionWithView` instead of `animateWithDuration`). – Rob Feb 03 '15 at 04:36
  • Perfect! Thanks for sharing – Senocico Stelian Jul 02 '20 at 17:11