Uitextview Draw Invisible/Whitespace Characters

UITextView Draw Invisible/Whitespace Characters

I found a better solution than setting the showsInvisibleCharacters property of NSLayoutManager to true, by subclassing NSLayoutManager and overriding the method drawBackgroundForGlyphRange(NSRange, CGPoint), which allows for custom drawings for each whitespace character, for example:

class LayoutManager : NSLayoutManager {

var text: String? { return textStorage?.string }

var font: UIFont = UIFont.systemFontOfSize(UIFont.systemFontSize()) {
didSet {
guard let text = self.text else { return }
let textRange = NSMakeRange(0, (text as NSString).length)
invalidateGlyphsForCharactersInRange(textRange, actualCharacterRange: nil)
invalidateCharacterAttributesForCharactersInRange(textRange, actualCharacterRange: nil)
}
}

override func drawBackgroundForGlyphRange(glyphsToShow: NSRange, atPoint origin: CGPoint) {

super.drawBackgroundForGlyphRange(glyphsToShow, atPoint:origin)

guard let text = self.text else { return }

enumerateLineFragmentsForGlyphRange(glyphsToShow)
{ (rect: CGRect, usedRect: CGRect, textContainer: NSTextContainer, glyphRange: NSRange, stop: UnsafeMutablePointer<ObjCBool>) -> Void in

let characterRange = self.characterRangeForGlyphRange(glyphRange, actualGlyphRange: nil)

// Draw invisible tab space characters

let line = (self.text as NSString).substringWithRange(characterRange)

do {

let expr = try NSRegularExpression(pattern: "\t", options: [])

expr.enumerateMatchesInString(line, options: [.ReportProgress], range: line.range)
{ (result: NSTextCheckingResult?, flags: NSMatchingFlags, stop: UnsafeMutablePointer<ObjCBool>) in

if let result = result {

let range = NSMakeRange(result.range.location + characterRange.location, result.range.length)
let characterRect = self.boundingRectForGlyphRange(range, inTextContainer: textContainer)

let symbol = "\u{21E5}"
let attrs = [NSFontAttributeName : Font]
let height = (symbol as NSString).sizeWithAttributes(attrs).height
symbol.drawInRect(CGRectOffset(characterRect, 1.0, height * 0.5, withAttributes: attrs)

}

}

} catch let error as NSError {
print(error.localizedDescription)
}

}

}

}

Extra whitespace that causes a newline by UITextView at end is ignored

Use this:

‏‏‎ ‏‏‎

There is some invisible characters inside the quote that iOS is not count them as whiteSpace

NSLayoutManager hides new line characters no matter what I do

As I figured out, the default implementation of NSTypesetter's setNotShownAttribute: of the class doesn't change already generated glyphs in its glyph storage. So, call of super doesn't produce any effect. I just have to replace glyphs manually before calling super.

So, the most efficient implementation of showing invisible characters (you will see the difference while zooming the view) is this:

Limitations of this approach: if your app has to have multiple fonts in text view, then this approach might not be such a good idea, because the font of those displayed invisible characters will be different as well. And that's not what you might want to achieve.

  1. Subclass NSLayoutManager and override setGlyphs to show space chars:

    public override func setGlyphs(_ glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSGlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: Font, forGlyphRange glyphRange: NSRange) {
    var substring = (self.currentTextStorage.string as NSString).substring(with: glyphRange)

    // replace invisible characters with visible
    if PreferencesManager.shared.shouldShowInvisibles == true {
    substring = substring.replacingOccurrences(of: " ", with: "\u{00B7}")
    }

    // create a CFString
    let stringRef = substring as CFString
    let count = CFStringGetLength(stringRef)

    // convert processed string to the C-pointer
    let cfRange = CFRangeMake(0, count)
    let fontRef = CTFontCreateWithName(aFont.fontName as CFString?, aFont.pointSize, nil)
    let characters = UnsafeMutablePointer<UniChar>.allocate(capacity: MemoryLayout<UniChar>.size * count)
    CFStringGetCharacters(stringRef, cfRange, characters)

    // get glyphs for the pointer of characters
    let glyphsRef = UnsafeMutablePointer<CGGlyph>.allocate(capacity: MemoryLayout<CGGlyph>.size * count)
    CTFontGetGlyphsForCharacters(fontRef, characters, glyphsRef, count)

    // set those glyphs
    super.setGlyphs(glyphsRef, properties:props, characterIndexes: charIndexes, font: aFont, forGlyphRange: glyphRange)
    }
  2. Subclass NSATSTypesetter and assign it to your NSLayoutManager subclas. The subclass will display the new line characters and make sure that every invisible character will be drawn with a different color:

    class CustomTypesetter: NSATSTypesetter {

    override func setNotShownAttribute(_ flag: Bool, forGlyphRange glyphRange: NSRange) {
    var theFlag = flag

    if PreferencesManager.shared.shouldShowInvisibles == true {
    theFlag = false

    // add new line glyphs into the glyph storage
    var newLineGlyph = yourFont.glyph(withName: "paragraph")
    self.substituteGlyphs(in: glyphRange, withGlyphs: &newLineGlyph)

    // draw new line char with different color
    self.layoutManager?.addTemporaryAttribute(NSForegroundColorAttributeName, value: NSColor.invisibleTextColor, forCharacterRange: glyphRange)
    }

    super.setNotShownAttribute(theFlag, forGlyphRange: glyphRange)
    }

    /// Currently hadn't found any faster way to draw space glyphs with different color
    override func setParagraphGlyphRange(_ paragraphRange: NSRange, separatorGlyphRange paragraphSeparatorRange: NSRange) {
    super.setParagraphGlyphRange(paragraphRange, separatorGlyphRange: paragraphSeparatorRange)

    guard PreferencesManager.shared.shouldShowInvisibles == true else { return }

    if let substring = (self.layoutManager?.textStorage?.string as NSString?)?.substring(with: paragraphRange) {
    let expression = try? NSRegularExpression.init(pattern: "\\s", options: NSRegularExpression.Options.useUnicodeWordBoundaries)
    let sunstringRange = NSRange(location: 0, length: substring.characters.count)

    if let matches = expression?.matches(in: substring, options: NSRegularExpression.MatchingOptions.withoutAnchoringBounds, range: sunstringRange) {
    for match in matches {
    let globalSubRange = NSRange(location: paragraphRange.location + match.range.location, length: 1)
    self.layoutManager?.addTemporaryAttribute(NSForegroundColorAttributeName, value: Color.invisibleText, forCharacterRange: globalSubRange)
    }
    }
    }
    }
    }
  3. To show/hide invisible characters just call:

    let storageRange = NSRange(location: 0, length: currentTextStorage.length)
    layoutManager.invalidateGlyphs(forCharacterRange: storageRange, changeInLength: 0, actualCharacterRange: nil)
    layoutManager.ensureGlyphs(forGlyphRange: storageRange)

Bordered UITextView

#import <QuartzCore/QuartzCore.h>

....

// typically inside of the -(void) viewDidLoad method
self.yourUITextView.layer.borderWidth = 5.0f;
self.yourUITextView.layer.borderColor = [[UIColor grayColor] CGColor];

Clicking on NSURL in a UITextView

For me it only highlights the link... Am I missing something?

Sample Image

Update:

Here's a really hacky solution via dummy URL's:

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSMutableAttributedString* attributedString = [[NSMutableAttributedString alloc] init];
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, vim iuvaret blandit intellegebat ut. Solet diceret interpretaris eos cu, magna dicat explicari mei ex, cibo adversarium eu pro. Ei odio saepe eloquentiam cum, nisl case nec ut. Harum habemus definiebas et vix, est cu aeque sonet, in his salutatus repudiare deterruisset. Quo duis autem intellegat an, regione propriae et vis."]];

NSAttributedString* dummyUrl = [[NSAttributedString alloc] initWithString:@" " attributes:@{ NSLinkAttributeName : @"http://dummy.com" }];
NSAttributedString* url = [[NSAttributedString alloc] initWithString:@"http://stackoverflow.com" attributes:@{ NSLinkAttributeName : @"http://stackoverflow.com" }];
[attributedString appendAttributedString:dummyUrl];
[attributedString appendAttributedString:url];
[attributedString appendAttributedString:dummyUrl];
self.textView.attributedText = attributedString;
}

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
return ![URL.absoluteString isEqualToString:@"http://dummy.com"];
}

Basically you force the UITextView to recognise the tap before and after the stackoverflow link as the dummy link. Since it's just a space it's invisible, however unfortunately if you tap and hold before/after the stackoverflow link you'll see the space highlighted with gray, despite shouldInteractWithURL returning NO.
Unfortunately it seems you cannot circumvent this behaviour unless you implement your own UITextField from scratch...

UITextView/UILabel with background but with spacing between lines

After some research I found the best solution for what I needed.
The solution below is only iOS7+.

First we add this to - (void)drawRect:(CGRect)rect of your UITextView subclass.

- (void)drawRect:(CGRect)rect    

/// Position each line behind each line.
[self.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {

/// The frame of the rectangle.
UIBezierPath *rectanglePath = [UIBezierPath bezierPathWithRect:CGRectMake(usedRect.origin.x, usedRect.origin.y+3, usedRect.size.width, usedRect.size.height-4)];

/// Set the background color for each line.
[UIColor.blackColor setFill];

/// Build the rectangle.
[rectanglePath fill];
}];
}];

Then we set the line spacing for the UITextView:

- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
{
return 15;
}

The method above is only called if you set the NSLayoutManagerDelegate. You could do that in your init, initWithFrame and initWithCode methods like this:

self.layoutManager.delegate = self;

Also don't forget to declare that your subclass is a delegate in your .h file:

@interface YOUR_SUBCLASS_OF_UITEXTVIEW : UITextView <NSLayoutManagerDelegate>


Related Topics



Leave a reply



Submit