Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animate a change in part of an NSMutableAttributedString

I'm making an iOS app that has a UITextView. When closing a parenthesis in that UITextView, I want to highlight to the user which opening parenthesis it pairs to. So far I've done this using an NSMutableAttributedString and changing the font size of the paired parentheses, which works but is kind of ugly. What I really want is to animate this similarly to the way xcode does the same thing when I close a parenthesis in my code. Is there any way of doing this?

Any help is greatly appreciated, though I'm fairly new to this, so please don't assume I know too much :)

Here's my code:

    @IBAction func didPressClosingParentheses(sender: AnyObject) {
    appendStringToInputTextView(")")
    var count = 1
    let currentString = inputTextView.attributedText.string
    let characterArray = Array(currentString)
    let closingIndex = characterArray.count - 1
    for i in reverse(0...closingIndex-1) {
        if characterArray[i] == "(" {
            count--
        }
        else if characterArray[i] == ")" {
            count++
        }
        if count == 0 {
            let startingIndex = i
            var newString = NSMutableAttributedString(string: currentString)
            newString.addAttribute(NSFontAttributeName, value: UIFont(name: "HelveticaNeue-Thin", size: 28)!, range: NSMakeRange(0, newString.length))
            newString.addAttribute(NSForegroundColorAttributeName, value: UIColor(red: 243, green: 243, blue: 243, alpha: 1), range: NSMakeRange(0, newString.length))
            newString.addAttribute(NSFontAttributeName, value: UIFont(name: "HelveticaNeue-Thin", size: 35)!, range: NSMakeRange(startingIndex, 1))
            newString.addAttribute(NSFontAttributeName, value: UIFont(name: "HelveticaNeue-Thin", size: 35)!, range: NSMakeRange(closingIndex, 1))


            UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
                self.inputTextView.attributedText = newString

            }, nil)

            break
        }
    }
}

As you can see I've tried using the UIView.animateWithDuration to do this, which as I suspected, didn't work.

like image 888
Andreas Avatar asked Jan 19 '15 22:01

Andreas


3 Answers

Another solution which works for me (Swift 4) is to generate multiple attributed strings, assign one to label and then replace the content (attributedText) inside transition animation block. For example:

// MARK: Extension util which generates NSAttributedString by text,font,color,backgroundColor
extension NSAttributedString {
    class func generate(from text: String, font: UIFont = UIFont.systemFont(ofSize: 16), color: UIColor = .black, backgroundColor: UIColor = .clear) -> NSAttributedString {
        let atts: [NSAttributedStringKey : Any] = [.foregroundColor : color, .font : font, .backgroundColor : backgroundColor]
        return NSAttributedString(string: text, attributes: atts)
    }
}

// MARK: Sentence
let string1 = "Hi, i'm "
let string2 = "Daniel"

// MARK: Generate highlighted string
let prefixAttString = NSAttributedString.generate(from: string1)
let highlightedSuffixAttString = NSAttributedString.generate(from: string2, backgroundColor: .red)
let highlightedString = NSMutableAttributedString()
highlightedString.append(prefixAttString)
highlightedString.append(highlightedSuffixAttString)


// MARK: Generate regular string (Same prefix, different suffix)enter image description here
let regularSuffixAttString = NSAttributedString.generate(from: string2)
let regularString = NSMutableAttributedString()
regularString.append(prefixAttString)
regularString.append(regularSuffixAttString)

self.view.addSubview(label)
label.attributedText = regularString

// UIViewAnimationOptions.transitionCrossDissolve is necessary.
UIView.transition(with: self.label, duration: 4, options: [.transitionCrossDissolve], animations: {
    self.label.attributedText = highlightedString
}, completion: nil)

}

Don't forget to use .transitionCrossDissolve in the animation options.

like image 66
DanielH Avatar answered Nov 04 '22 04:11

DanielH


I achieved what I wanted to by getting the frames for the actual parentheses and creating new UILabels on top of my UITextView and animating those labels.

    @IBAction func didPressClosingParentheses(sender: AnyObject) {

    inputTextView.text = inputTextView.text + ")"
    var count = 1
    let currentString = inputTextView.attributedText.string
    let characterArray = Array(currentString)
    let closingIndex = characterArray.count - 1
    for i in reverse(0...closingIndex-1) {
        if characterArray[i] == "(" {
            count--
        }
        else if characterArray[i] == ")" {
            count++
        }
        if count == 0 {
            let startingIndex = i

            let openingRange = NSMakeRange(startingIndex, 1)
            let closingRange = NSMakeRange(closingIndex, 1)

            var openingFrame = inputTextView.layoutManager.boundingRectForGlyphRange(openingRange, inTextContainer: inputTextView.textContainer)

            openingFrame.origin.y += inputTextView.textContainerInset.top
            var openingLabel = UILabel(frame: openingFrame)
            openingLabel.text = "("
            openingLabel.font = UIFont(name: "HelveticaNeue-Thin", size: 28)
            openingLabel.textColor = whiteishColor
            openingLabel.backgroundColor = bluishColor

            var closingFrame = inputTextView.layoutManager.boundingRectForGlyphRange(closingRange, inTextContainer: inputTextView.textContainer)
            closingFrame.origin.y += inputTextView.textContainerInset.top
            var closingLabel = UILabel(frame: closingFrame)
            closingLabel.text = ")"
            closingLabel.font = UIFont(name: "HelveticaNeue-Thin", size: 28)
            closingLabel.textColor = whiteishColor
            closingLabel.backgroundColor = bluishColor

            inputTextView.addSubview(openingLabel)
            inputTextView.addSubview(closingLabel)

            UIView.animateWithDuration(0.4, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
                openingLabel.transform = CGAffineTransformMakeScale(1.25, 1.25)
                closingLabel.transform = CGAffineTransformMakeScale(1.25, 1.25)

                }, nil)

            UIView.animateWithDuration(0.4, delay: 0.2, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
                openingLabel.transform = CGAffineTransformMakeScale(1.0, 1.0)
                closingLabel.transform = CGAffineTransformMakeScale(1.0, 1.0)

                }, nil)
            UIView.animateWithDuration(0.25, delay: 0.4, options: nil, animations: {
                openingLabel.alpha = 0
                closingLabel.alpha = 0

                }, completion: { finished in
                    openingLabel.removeFromSuperview()
                    closingLabel.removeFromSuperview()
            })

            break
        }
    }

}
like image 34
Andreas Avatar answered Nov 04 '22 04:11

Andreas


There's a much easier way to do this, which you can see in this answer: Animate text change in UILabel.

You just have to set the label.attributedText property instead:

UIView.transition(
    with: label,
    duration: 0.15,
    options: .transitionCrossDissolve,
    animations: {
        label.attributedText = NSAttributedString(string: text, attributes: attributes)
    },
    completion: nil
)
like image 2
Patrick Lynch Avatar answered Nov 04 '22 04:11

Patrick Lynch