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.
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.
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
}
}
}