0

I am trying to create a UIButton that allows the selected text to be underlined. This is my current code:

func underline() {
if let textRange = selectedRange {
    let attributedString = NSMutableAttributedString(attributedString: textView.attributedText)
    textView.textStorage.addAttributes([.underlineStyle : NSUnderlineStyle.single.rawValue], range: textRange)
    }
}

It is currently underlining but the problem I am having is that I need to check if the current text is underlined already and if it is remove the underline. I can't seem to work this out with the NSMutableAttributedString.

I am doing this with an Italic UIButton like so:

func italic() {
    if let textRange = selectedRange {
        let attributedString = NSAttributedString(attributedString: textView.attributedText)
        attributedString.enumerateAttribute(.font, in: textRange, options: []) { (font, range, pointee) in
            let newFont: UIFont
            if let font = font as? UIFont {
                let fontTraits = font.fontDescriptor.symbolicTraits
                if fontTraits.contains(.traitItalic) {
                    newFont = UIFont.systemFont(ofSize: font.pointSize, weight: .regular)
                } else {
                    newFont = UIFont.systemFont(ofSize: font.pointSize).italic()
                }
                textView.textStorage.addAttributes([.font : newFont], range: textRange)
            }
        }
    }
}

How can I achieve the ability to check if the current text has the underlining attribute for my first function?

Code we have so far:

func isUnderlined(attrText: NSAttributedString) -> Bool {
    var contains: ObjCBool = false
    attrText.enumerateAttributes(in: NSRange(location: 0, length: attrText.length), options: []) { (dict, range, value) in
        if dict.keys.contains(.underlineStyle) {
            contains = true
        }
    }
    return contains.boolValue
}

func underline() {
    if let textRange = selectedRange {
        let attributedString = NSMutableAttributedString(attributedString: textView.attributedText)
        switch self.isUnderlined(attrText: attributedString) {
        case true:
            print("true")
            textView.textStorage.removeAttribute(.underlineStyle, range: textRange)
        case false:
            print("remove")
            textView.textStorage.addAttributes([.underlineStyle : NSUnderlineStyle.single.rawValue], range: textRange)
        }
    }
}
a.wip
  • 69
  • 12

1 Answers1

2

To check if a text is already underlined, you can simply run contains(_:) on the attributes of the text, i.e.

func isUnderlined(attrText: NSAttributedString) -> Bool {
    var contains: ObjCBool = false
    attrText.enumerateAttributes(in: NSRange(location: 0, length: attrText.length), options: []) { (dict, range, value) in
        if dict.keys.contains(.underlineStyle) {
            contains = true
        }
    }
    return contains.boolValue
}

Example:

let attrText1 = NSAttributedString(string: "This is an underlined text.", attributes: [.underlineStyle : NSUnderlineStyle.styleSingle.rawValue])
let attrText2 = NSAttributedString(string: "This is an underlined text.", attributes: [.font : UIFont.systemFontSize])

print(self.isUnderlined(attrText: attrText1)) //true
print(self.isUnderlined(attrText: attrText2)) //false

You can use the above logic in your UITextView as per your requirement.

To remove the attribute,

1. first of all it must be an NSMutableAttributedString.

2. Then to remove an attribute, use removeAttribute(_:range:) method on attributed string.

let attrText1 = NSMutableAttributedString(string: "This is an underlined text.", attributes: [.underlineStyle : NSUnderlineStyle.styleSingle.rawValue])

print(self.isUnderlined(attrText: attrText1)) //true
if self.isUnderlined(attrText: attrText1) {
    attrText1.removeAttribute(.underlineStyle, range: NSRange(location: 0, length: attrText1.string.count))
}
print(self.isUnderlined(attrText: attrText1)) //false

Handle textView on button tap

@IBAction func onTapButton(_ sender: UIButton) {
    if let selectedTextRange = self.textView.selectedTextRange {
        let location = self.textView.offset(from: textView.beginningOfDocument, to: selectedTextRange.start)
        let length = self.textView.offset(from: selectedTextRange.start, to: selectedTextRange.end)
        let range = NSRange(location: location, length: length)

        self.textView.attributedText.enumerateAttributes(in: range, options: []) { (dict, range, value) in
            if dict.keys.contains(.underlineStyle) {
                self.textView.textStorage.removeAttribute(.underlineStyle, range: range)
            } else {
                self.textView.textStorage.addAttributes([.underlineStyle : NSUnderlineStyle.styleSingle.rawValue], range: range)
            }
        }
    }
}
PGDev
  • 23,751
  • 6
  • 34
  • 88
  • Okay, I'll make an if else with this function. How should I go about removing the attribute though? – a.wip May 14 '19 at 06:14
  • Okay awesome, I had to change the first func (at: 0,) to 1 otherwise it's never calling the true part of the if statement... Is this correct? – a.wip May 14 '19 at 06:31
  • Which `func` are your talking about? I don't understand. – PGDev May 14 '19 at 06:32
  • The isUnderlined one. – a.wip May 14 '19 at 06:33
  • `func (at: 0,) to 1` what is this? I've given you the generic code that you can use to solve your problem statement. – PGDev May 14 '19 at 06:34
  • let attributes = attrText.attributes(at: 0, effectiveRange: nil).keys. I had to change 'at: 0' to 1 to allow the function to work. What is 'at: 0' referring to? – a.wip May 14 '19 at 06:35
  • 0 is referring to the index you want to check for attributes in the `NSAttributedString`. – PGDev May 14 '19 at 06:41
  • I've updated the answer to check for attributed in the whole range. – PGDev May 14 '19 at 06:44
  • It's only working sporadically. It seems there's a bug somewhere. It will underline but on certain ranges is not removing the underline... Do I need to use an enumerate function instead of contains? – a.wip May 14 '19 at 07:36
  • Try using that for detecting the attribute like you did for `italic()`. – PGDev May 14 '19 at 07:38
  • Sorry, I tried. But it's not working. Do you know what to do with the EnumerateAttribute Function? I'm afraid I am doing the wrong thing :( – a.wip May 14 '19 at 07:47
  • I've updated `isUnderlined()` with `enumerateAttributes`. – PGDev May 14 '19 at 07:58
  • It still isn't working, it works for a bit then stops... Here is a recording to show you: https://wetransfer.com/downloads/3549209f16a40153576aaf4ca412ed8e20190514080337/fa545a Thank you so much for your help. – a.wip May 14 '19 at 08:06
  • Yep, just added :) – a.wip May 14 '19 at 08:31
  • 1
    Updated the answer. I think this will solve your issue. Do accept and upvote if you get that working. – PGDev May 14 '19 at 08:40
  • Got it working!!! Thanks heaps. What is the reason I have to manually set 'location, length then convert it to a NSRange? Why wasn't the native NSRange working? – a.wip May 14 '19 at 10:16
  • `selectedTextRange` is of type `UITextRange` and not 'NSRange`. That's why the conversion was required. – PGDev May 14 '19 at 10:17
  • Yeah, it's weird though. Because the 'selectedRange' var I was using before was a NSRange and even trying with it now it is producing the bug. It only is working when I have your code in that gets the UITextRange then converts it. – a.wip May 14 '19 at 10:20