Search code examples
iosswiftuitextviewnsattributedstringnsmutableattributedstring

Enumerate over a Mutable Attributed String (Underline Button)


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

Solution

  • 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)
                }
            }
        }
    }