The build-in keys all works well, it will follow the existing text after edit (insert/replace/paste)
But when I doing it with custom NSAttributedString.Key, the attribute doesn't follow.
tagtest is a custom NSAttributedString.Key
extension NSAttributedString.Key {
static let drawTag: NSAttributedString.Key = .init("drawTag")
}
And I draw it by custom NSLayoutManager (similar style with drawing background, but I do need a custom one)
func drawTag(forGlyphRange glyphRange: NSRange,
fillColor:UIColor,
lineFragmentRect lineRect: CGRect,
lineFragmentGlyphRange lineGlyphRange: NSRange,
containerOrigin: CGPoint) {
if let textContainer = textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) {
let range = NSIntersectionRange(glyphRange, lineGlyphRange)
let lineRect = self.boundingRect(forGlyphRange: range, in: textContainer)
let path = UIBezierPath(roundedRect: lineRect, cornerRadius: 3)
fillColor.setFill()
path.fill()
}
}
I tried to override the NSTextStorage
The attribute can be read but still missed after edit
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
var str = str
var attrs: [NSAttributedString.Key:Any] = [:]
if range.location < string.count{
var r = range
attrs = attributes(at: range.location, effectiveRange: nil)
str = str + "\((attrs[NSAttributedString.Key.drawTag]))"
}
container.replaceCharacters(in: range, with: NSAttributedString(string: str, attributes: attrs))
//container.addAttributes(attrs, range: NSMakeRange(range.location, str.count))
edited([.editedAttributes, .editedCharacters], range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
}
Or override the UITextViewDelegate
This is the most closest one, but cursor will jump to the end, and there will be error when input texts at end.
func textView(_ uiView: UITextView, shouldChangeTextIn: NSRange, replacementText: String) -> Bool{
let attrs = attributedText.attributes(at: shouldChangeTextIn.location, effectiveRange: nil)
attributedText.replaceCharacters(in: shouldChangeTextIn, with: NSAttributedString(string: replacementText, attributes: attrs))
textView.configure(uiView)
return false
}
Related
I want to set width of each characters when user typing. I use NSAttributeString with key .Kern that works well but when the text length is bigger about 1000+, I touch on screen to place cursor position in front of first line and typing new characters that cause Performance issue, too slow and lag.
// Initialize once and reuse every time
lazy var paragraphStyle: NSMutableParagraphStyle = {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.alignment = .left
return paragraphStyle
}()
// Initialize once and reuse every time
lazy var alignTextAttributes: [NSAttributedString.Key: Any] = {
return [
.kern : 30,
.paragraphStyle : paragraphStyle,
.foregroundColor : UIColor.black,
]
}()
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// If I want to set each characters with different kern values, how can I do that?
alignTextAttributes[.kern] = textView.text.count.isMultiple(of: 2) ? 30 : 10
// This is the key part
textView.typingAttributes = alignTextAttributes
// Allow system to control typing
return true
}
Here's what you can try -
// 4 (or 6 or 8) spaces (depending on font size)
let padding: String = " "
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
var cursorLocation = textView.selectedRange.location
if text.isEmpty {
let deleteRange = NSRange(
location: max(0, range.location - padding.count),
length: range.length + padding.count
)
textView.textStorage.replaceCharacters(in: deleteRange, with: text)
cursorLocation -= deleteRange.length
}
else {
let insertRange = NSRange(
location: range.location,
length: range.length + padding.count
)
textView.textStorage.replaceCharacters(in: insertRange, with: text.appending(padding))
cursorLocation += insertRange.length
}
textView.selectedRange.location = cursorLocation
return false
}
I'm having a custom NSLayoutManager with these two methods overwritten:
override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
let characterRange = self.characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
textStorage?.enumerateAttribute(.blur, in: characterRange, options: .longestEffectiveRangeNotRequired, using: { (value, subrange, _) in
guard let key = value as? String, !key.isEmpty else { return }
let blurGlyphRange = glyphRange(forCharacterRange: subrange, actualCharacterRange: nil)
drawBlur(forGlyphRange: blurGlyphRange)
textStorage?.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.clear], range: blurGlyphRange)
})
}
private func drawBlur(forGlyphRange tokenGlypeRange: NSRange) {
guard let textContainer = textContainer(forGlyphAt: tokenGlypeRange.location, effectiveRange: nil) else { return }
let withinRange = NSRange(location: NSNotFound, length: 0)
enumerateEnclosingRects(forGlyphRange: tokenGlypeRange, withinSelectedGlyphRange: withinRange, in: textContainer) { (rect, _) in
let blurRect = rect.offsetBy(dx: self.textContainerOriginOffset.width, dy: self.textContainerOriginOffset.height)
UIColor.red.setFill()
UIBezierPath(roundedRect: blurRect, cornerRadius: 4).fill()
}
Everything works fine except when I set the UITextView isScrollingEnabled on false
I enter an endless loop caused by the textStorage enumerateAttribute method in drawGlyphs.
I don't understand why this happens and also I don't know how to prevent this.
Someone who knows more about this?
EDIT
If I remove the textStorage addAttributes with the foregroundColor then it works. So that's causing the loop for some reason.
I found the problem why it comes in and endless loop. The textstorage updates/add the attribute and then notifies the layoutmanager again.
Solution is to create your own textstorage like this:
class CustomTextStorage: NSTextStorage {
private let backingStore = NSMutableAttributedString()
override var string: String {
return backingStore.string
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
if let attrs = attrs, let _ = attrs[.blur] {
backingStore.addAttribute(.foregroundColor, value: UIColor.clear, range: range)
}
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
and remove the line under the draw method in the layoutmanager:
textStorage?.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.clear], range: blurGlyphRange)
This question already has answers here:
Replace UITextViews text with attributed string
(4 answers)
Closed 6 years ago.
I have a textView, and I am trying to give it an attributed text. I tried achieving it inside shouldChangeTextInRange, but it crashes for range out of index.
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
if myTextView {
textView.attributedText = addAttributedText(1, text: text, fontsize: 13)
let newText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
let numberOfChars = newText.characters.count
return numberOfChars < 20
}
return true
}
func addAttributedText(spacing:CGFloat, text:String, fontsize: CGFloat) -> NSMutableAttributedString {
let attributedString = NSMutableAttributedString(string: text, attributes: [NSFontAttributeName:UIFont(
name: "Font",
size: fontsize)!])
attributedString.addAttribute(NSKernAttributeName, value: spacing, range: NSMakeRange(0, text.characters.count))
return attributedString
}
I tried adding attributedString with empty text to textView in viewDidLoad, but that doesn't help. That's why I thought it would be appropriate to do it on shouldChangeTextInRange
(Please note that my addAttributedText method works perfectly for other textviews)
If I use this, in one character type-in, it writes 2x and crashes. What is the right way of handling that kind of converting textView's text to attributed text that is being typed.
Here is the code that I tried to convert from the link above, it might have bugs, but I hope it will be able to help you.
func formatTextInTextView(textView: UITextView)
{
textView.scrollEnabled = false
var selectedRange: NSRange = textView.selectedRange
var text: String = textView.text!
// This will give me an attributedString with the base text-style
var attributedString: NSMutableAttributedString = NSMutableAttributedString(string: text)
var error: NSError? = nil
var regex: NSRegularExpression = NSRegularExpression.regularExpressionWithPattern("#(\\w+)", options: 0, error: error!)
var matches: [AnyObject] = regex.matchesInString(text, options: 0, range: NSMakeRange(0, text.length))
for match: NSTextCheckingResult in matches {
var matchRange: NSRange = match.rangeAtIndex(0)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: matchRange)
}
textView.attributedText = attributedString
textView.selectedRange = selectedRange
textView.scrollEnabled = true
}
EDIT: didn't see that in the original post there was a Swift answer, here is the link: stackoverflow.com/a/35842523/1226963
I want NSTextView object to react to Tab key hit by changing NSParagraphStyle spacing. And it does but EXTREMELY slow!!! In fact if I do this changes too quick (hit Tab key too fast), I eventually got glitches that sometimes even lead to crash. Here's the video: https://drive.google.com/open?id=0B4aMQXnlIOvCUXNjWTVXVkR3NHc. And another one: https://drive.google.com/open?id=0B4aMQXnlIOvCUDJjSEN0bFdqQXc
Fragment of code from my NSTextStorage subclass:
override func attributesAtIndex(index: Int, effectiveRange range: NSRangePointer) -> [String : AnyObject] {
return storage.attributesAtIndex(index, effectiveRange: range)
}
override func replaceCharactersInRange(range: NSRange, withString str: String) {
let delta = str.characters.count - range.length
beginEditing()
storage.replaceCharactersInRange(range, withString:str)
edited([.EditedCharacters, .EditedAttributes], range: range, changeInLength: delta)
endEditing()
}
override func setAttributes(attrs: [String : AnyObject]!, range: NSRange) {
beginEditing()
storage.setAttributes(attrs, range: range)
edited(.EditedAttributes, range: range, changeInLength: 0)
endEditing()
}
Fragment of code from my NSTextView subclass:
override func shouldChangeTextInRange(affectedCharRange: NSRange, replacementString: String?) -> Bool {
super.shouldChangeTextInRange(affectedCharRange, replacementString: replacementString)
guard replacementString != nil else { return true }
if replacementString == "\t" {
defer {
let selectedRangesValues = self.selectedRanges
var selectedRanges = [NSRange]()
for value in selectedRangesValues {
selectedRanges.append(value.rangeValue)
}
textController.switchToAltAttributesInRange(selectedRanges)
}
return false
}
return true
}
Fragment of code from my TextController which creates and applies alternative attributes:
func switchToAltAttributesInRange(ranges : [NSRange]) {
// get paragraph indexes from the ranges
var indexes = [Int]()
for range in ranges {
for idx in textStorage.paragraphsInRange(range) {
indexes.append(idx)
}
}
// change attributes for all the paragraphs in those ranges
for index in indexes {
let paragraphRange = textStorage.paragraphRangeAtIndex(index)
let element = elementAtIndex(index)
let altElementType = altElementTypeForElementType(element.type)
// change the attributes
let newAttributes = paragraphAttributesForElement(type: altElementType.rawValue)
self.textStorage.beginUpdates()
self.textStorage.setAttributes(newAttributes, range: paragraphRange)
self.textStorage.endUpdates()
}
}
func paragraphAttributesForElement(type typeString: String) -> [String : AnyObject] {
let elementPreset = elementPresetForType(elementType)
// set font
let font = NSFont (name: elementPreset.font, size: CGFloat(elementPreset.fontSize))!
// set attributes
let elementAttributes = [NSFontAttributeName: font,
NSParagraphStyleAttributeName : paragraphStyleForElementPreset(elementPreset, font: font),
NSForegroundColorAttributeName: NSColor.colorFromHexValue(elementPreset.color),
NSUnderlineStyleAttributeName : elementPreset.underlineStyle,
ElementAttribute.AllCaps.rawValue : elementPreset.allCaps]
return elementAttributes
}
func paragraphStyleForElementPreset(elementPreset : TextElementPreset, font : NSFont) -> NSParagraphStyle {
let sceneParagraphStyle = NSMutableParagraphStyle()
let spacing = elementPreset.spacing
let spacingBefore = elementPreset.spacingBefore
let headIndent = elementPreset.headIndent
let tailIndent = elementPreset.tailIndent
let cFont = CTFontCreateWithName(font.fontName, font.pointSize, nil)
let fontHeight = CTFontGetDescent(cFont) + CTFontGetAscent(cFont) + CTFontGetLeading(cFont)
sceneParagraphStyle.paragraphSpacingBefore = CGFloat(spacingBefore)
sceneParagraphStyle.paragraphSpacing = fontHeight * CGFloat(spacing)
sceneParagraphStyle.headIndent = ScreenMetrics.pointsFromInches(CGFloat(headIndent))
sceneParagraphStyle.tailIndent = ScreenMetrics.pointsFromInches(CGFloat(tailIndent))
sceneParagraphStyle.firstLineHeadIndent = sceneParagraphStyle.headIndent
sceneParagraphStyle.lineBreakMode = .ByWordWrapping
return sceneParagraphStyle
}
Time Profiler instrument shows a high peak when I press the Tab key. It says that NSTextStorage attributesAtIndex takes up to 40 ms each time I press the Tab key.
I checked: if I remove NSParagraphStyle changes, everything becomes normal. So the question is: how should I update paragraph styles?
Hmm... Didn't found such a solution neither in Apple docs or in Google... Just have experimented and turns out if I add textView.display() after call of self.textStorage.setAttributes, everything works fine!
UPDATE: setNeedsDisplay(invalidRect) does a much better job because you might redraw just a dirty portion of text view's visible rect
I'm trying to color all occurrences (not just the first) of a textview string however when I get to the looping I get a cannot invoke rangeOfString argument.
I checked the rangeOfString:options:range documentation on Swift 2 and it looked pretty similar. I'm not really sure what I'm doing wrong.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
func textView(textView: UITextView){
if textView == self.textView {
let nsString = textView.text as NSString
let stringLength = textView.text.characters.count
var range = NSRange(location: 0, length: textView.text.characters.count)
let searchString = textView.text
let text = NSMutableAttributedString(string: textView.text)
text.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, stringLength))
textView.attributedText = text
while(range.location != NSNotFound) {
range = (textView.text as NSString).rangeOfString(searchString, options: NSStringCompareOptions, range: range)
text.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: nsString.rangeOfString("hello"))
}
}
}
}
It looks like you try to highlight user input from search bar. Here how I do so:
func highlightedText(text: NSString, inText: NSString, var withColor: UIColor?) -> NSMutableAttributedString {
var attributedString = NSMutableAttributedString(string:inText as String)
let range = (inText.lowercaseString as NSString).rangeOfString(text.lowercaseString as String)
if withColor == nil {
if let color = globalTint() {
withColor = color
} else {
withColor = UIColor.redColor()
}
}
attributedString.addAttribute(NSForegroundColorAttributeName, value: withColor! , range: range)
return attributedString
}
...
textView.attributedText = highlightedText("string", inText: titleText, color: nil)