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.
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)
}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)
}
}
}
}
}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?
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
Swift; Delegate Embedded View Controller and Parent
Implicitlyunwrappedoptional in Init VS Later
How to Add Floating Button on Top of the Uitableview
Convert JSON Anyobject to Int64
How to Turn Off Color Literals in Xcode
Swift 3:Appdelegate Does Not Conform to Protocol Gidsignindelegate
Refreshing Auth Token with Moya
How to Set Realtime Thread in Swift
How to Initialise a New Nsdocument Instance in Swift
How to Create a Multiline Textfield in Swiftui? Like the Notes App
How to Get the Kvc-String from Swift 4 Keypath
Swift/Scenekit Problems Getting Touch Events from Scnscene and Overlayskscene
How to Suppress a Specific Warning in Swift
How to Have Arview and Arscnview Coexist
How Does Let X Where X.Hassuffix("Pepper") Work