How do I avoid this while changing the character spacing of UITextView? - ios

I am trying to change the character spacing of uitextview. However it changes whole appearance.
Screenshots:
Before adding the code
After adding the code
Code (as extension):
extension UITextView {
func addCharacterSpacing(kernValue: Double = 1.15) {
if let textValue = text, textValue.count > 0 {
let attributedString = NSMutableAttributedString(string: textValue)
attributedString.addAttribute(NSAttributedStringKey.kern, value: kernValue, range: NSRange(location: 0, length: attributedString.length - 1))
attributedText = attributedString
}
}
}

If you are using NSMutableAttributedString, so you should customize it fully in the same way with paragraph style and fonts for it or any other settings.
From the screenshots I see a problem with paragraph style, setup it manually to the textView:
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
textView.attributedText = NSAttributedString(
string: "string",
attributes: [.paragraphStyle: paragraph])

Related

Emoji support for NSAttributedString attributes (kerning/paragraph style)

I am using a kerning attribute on a UILabel to display its text with some custom letter spacing. Unfortunately, as I'm displaying user-generated strings, I sometimes see things like the following:
ie sometimes some emoji characters are not being displayed.
If I comment out the kerning but apply some paragraph style instead, I get the same kind of errored rendering.
I couldn't find anything in the documentation explicitely rejecting support for special unicode characters. Am I doing something wrong or is it an iOS bug?
The code to reproduce the bug is available as a playground here: https://github.com/Bootstragram/Playgrounds/tree/master/LabelWithEmoji.playground
and copied here:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
extension NSAttributedString {
static func kernedSpacedText(_ text: String,
letterSpacing: CGFloat = 0.0,
lineHeight: CGFloat? = nil) -> NSAttributedString {
// TODO add the font attribute
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttribute(NSAttributedStringKey.kern,
value: letterSpacing,
range: NSRange(location: 0, length: text.count))
if let lineHeight = lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineHeight
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: text.count))
}
return attributedString
}
}
//for familyName in UIFont.familyNames {
// for fontName in UIFont.fontNames(forFamilyName: familyName) {
// print(fontName)
// }
//}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let myString = "1βš½πŸ“ΊπŸ»βšΎοΈπŸŒ―πŸ„β€β™‚οΈπŸ‘\n2 πŸ˜€πŸ’ΏπŸ’Έ 🍻"
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 100)
label.attributedText = NSAttributedString.kernedSpacedText(myString)
label.numberOfLines = 0
label.textColor = .black
view.addSubview(label)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Thanks.
TL, DR:
String.count != NSString.length. Any time you see NSRange, you must convert your String into UTF-16:
static func kernedSpacedText(_ text: String,
letterSpacing: CGFloat = 0.0,
lineHeight: CGFloat? = nil) -> NSAttributedString {
// TODO add the font attribute
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttribute(NSAttributedStringKey.kern,
value: letterSpacing,
range: NSRange(location: 0, length: text.utf16.count))
if let lineHeight = lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineHeight
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: text.utf16.count))
}
return attributedString
}
The longer explanation
Yours is a common problem converting between Swift's String and ObjC's NSString. The length of a String is the number of extended grapheme clusters; in ObjC, it's the number of UTF-16 code points needed to encode that string.
Take the thumb-up character for example:
let str = "πŸ‘"
let nsStr = str as NSString
print(str.count) // 1
print(nsStr.length) // 2
Things can get even weirder when it comes to the flag emojis:
let str = "πŸ‡ΊπŸ‡Έ"
let nsStr = str as NSString
print(str.count) // 1
print(nsStr.length) // 4
Even though this article was written all the way back in 2003, it's still a good read today:
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets.

How to set both spacing between characters (kern) and strikethrough style for `UILabel`?

I need to set two attributes to a text presented by a UILabel: spacing between letters (kern), and its strikethrough style. Based on the NSAttributedStringKey documentation I have created the following extension to the UILabel:
extension UILabel {
func setStrikeThroughSpacedText(text: String, kern: CGFloat?) {
var attributes: [NSAttributedStringKey : Any] = [:]
if let kern = kern {
attributes[.kern] = kern
}
attributes[.strikethroughStyle]
= NSNumber(integerLiteral: NSUnderlineStyle.styleSingle.rawValue)
self.attributedText = NSAttributedString(string: text,
attributes: attributes)
}
}
However, it seems that .kern key somehow collides with the .strikethroughStyle key, because if I specify kern, the kern is applied, but not the strikethrough style. If I don't specify kern (so the extension does not apply the .kern attribute), the strikethrough style works.
Anyone has a different way how to work around this bug (I assume this is a bug)?
Try this, it should work for you
Note: I tested in Swift 4
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
let attrString = NSMutableAttributedString(string: stringValue)
let style = NSMutableParagraphStyle()
style.lineSpacing = 24 // change line spacing between paragraph like 36 or 48
style.minimumLineHeight = 20 // change line spacing between each line like 30 or 40
attrString.addAttribute(NSAttributedStringKey.paragraphStyle, value: style, range: NSRange(location: 0, length: stringValue.count))
attrString.addAttribute(NSAttributedStringKey.strikethroughStyle, value: 2, range: NSMakeRange(0, attrString.length))
attrString.addAttribute(NSAttributedStringKey.kern, value: 2, range: NSMakeRange(0, attrString.length))
label.attributedText = attrString
Result:
Sim 1: Strike + LineSpacing
Sim 2: Strike + LineSpacing + Character Spacing

NSMutableParagraphStyle is not called

I have a UILabel on xib file and I want to increase the line spacing of the texts.
I tried to write, but with the code below only text alignment is called and line spacing remains the same. Why "paragraphStyle.lineSpacing" is not called?
class PlaySheetCellLeft: UITableViewCell {
#IBOutlet var LBLTitle:UILabel!
var message:[String:Any]? {
didSet{
guard let msg = self.message else { return }
self.LBLTitle.text = title
}
override func awakeFromNib() {
super.awakeFromNib()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 10
let attrString = NSMutableAttributedString()
attrString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:NSMakeRange(0, attrString.length))
LBLTitle.attributedText = attrString
LBLTitle.textAlignment = NSTextAlignment.center
}
}
You're creating a range with an empty attributed string, so the style isn't being set on anything.
When you create your range, you're actually creating a range like this: NSMakeRange(0, 0).
You should pass your string into the creation of the mutable attributed string like this: NSMutableAttributedString(string: "your string")
You'll also need to set this when you set the text of your label, which by the looks of your code isn't in awakeFromNib

Is it possible to set a different colour for one character in a UILabel?

I've been working on a small project using Xcode. It uses a lot of labels, textfields, etc. I've finished with most of the layout, the constrains, and the forms, titles, etc. After which, the client announces that for all required fields, there should be a red asterisk next to the label.
Call me lazy, but I'd rather not go back to all of my forms, add in a lot of labels with asterisks on them, and re-do my auto-layout to accommodate the new labels.
So, is there a way to change the colour of a specific character (in this case, the asterisk) in a UILabel, while the rest of the text stays black?
You can use NSMutableAttributedString.
You can set specific range of your string with different color, font, size, ...
E.g:
var range = NSRange(location:2,length:1) // specific location. This means "range" handle 1 character at location 2
attributedString = NSMutableAttributedString(string: originalString, attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 18.0)!])
// here you change the character to red color
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: range)
label.attributedText = attributedString
Ref: Use multiple font colors in a single label - Swift
You can change a lot of attribute of String.
Ref from apple: https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSMutableAttributedString_Class/index.html
let text = "Sample text *"
let range = (text as NSString).rangeOfString("*")
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor() , range: range)
//Apply to the label
myLabel.attributedText = attributedString;
UILabel have an .attributedText property of type NSAttributedString.
Declaration
#NSCopying var attributedText: NSAttributedString?
You let your asterix * have a single .redColor() attribute (NSForegroundColorAttributeName), whereas the rest of the new label simply uses the same text as before, however also contained in an NSAttributedTextString.
An example follows below using a function to repeatedly update your existing labels to attributed strings prefixed with a red *:
class ViewController: UIViewController {
#IBOutlet weak var myFirstLabel: UILabel!
#IBOutlet weak var mySecondLabel: UILabel!
let myPrefixCharacter = "*"
let myPrefixColor = UIColor.redColor()
// ...
override func viewDidLoad() {
super.viewDidLoad()
// ...
/* update labels to attributed strings */
updateLabelToAttributedString(myFirstLabel)
updateLabelToAttributedString(mySecondLabel)
// ...
}
func updateLabelToAttributedString(label: UILabel) {
/* original label text as NSAttributedString, prefixed with " " */
let attr = [ NSForegroundColorAttributeName: myPrefixColor ]
let myNewLabelText = NSMutableAttributedString(string: myPrefixCharacter, attributes: attr)
let myOrigLabelText = NSAttributedString(string: " " + (label.text ?? ""))
/* set new label text as attributed string */
myNewLabelText.appendAttributedString(myOrigLabelText)
label.attributedText = myNewLabelText
}
// ...
}
Swift 4
(Note: notation for attributed string key is changed in swift 4)
Here is an extension for NSMutableAttributedString, that add/set color on string/text.
extension NSMutableAttributedString {
func setColor(color: UIColor, forText stringValue: String) {
let range: NSRange = self.mutableString.range(of: stringValue, options: .caseInsensitive)
self.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: range)
}
}
Now, try above extension with UILabel and see result
let label = UILabel()
label.frame = CGRect(x: 40, y: 100, width: 280, height: 200)
let red = "red"
let blue = "blue"
let green = "green"
let stringValue = "\(red)\n\(blue)\n&\n\(green)"
label.textColor = UIColor.lightGray
label.numberOfLines = 0
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: stringValue)
attributedString.setColor(color: UIColor.red, forText: red) // or use direct value for text "red"
attributedString.setColor(color: UIColor.blue, forText: blue) // or use direct value for text "blue"
attributedString.setColor(color: UIColor.green, forText: green) // or use direct value for text "green"
label.font = UIFont.systemFont(ofSize: 26)
label.attributedText = attributedString
self.view.addSubview(label)
func updateAttributedStringWithCharacter(title : String, uilabel: UILabel) {
let text = title + "*"
let range = (text as NSString).range(of: "*")
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red , range: range)
uilabel.attributedText = attributedString }
I know this is an old post, but i want to share my approach (which is based on the answer from dfri) i just made it a function for convenience.
func lastCharOnColor(label: UILabel, color: UIColor, length: Int) {
//First we get the text.
let string = label.text
//Get number of characters on string and based on that get last character index.
let characterCounter = string?.characters.count
let lastCharacterIndex = characterCounter!-1
//Set Range
let range = NSRange(location: lastCharacterIndex, length: length)
let attributedString = NSMutableAttributedString(string: string!, attributes: nil)
//Set label
attributedString.addAttribute(NSForegroundColorAttributeName, value: color, range: range)
label.attributedText = attributedString
}
I use this function to just set the last character from a label to a certain color like this:
lastCharOnColor(label: self.labelname, color: UIColor.red, length: 1)
Hope this helps anyone.
Here is an extension for simply making mandatory labels by appending a red *
Swift 5
extension UILabel {
func makeTextMandatory() {
let text = self.text ?? "" + " *"
let range = (text as NSString).range(of: " *")
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red , range: range)
self.attributedText = attributedString
}
}
Usage :
dobLabel.makeTextMandatory()

UITextView cuts off text on iPhone 5/5s but not in iPhone 6

I am resizing a UITextView for an attributed string that I create based on a response from my API that can have up to three different attributes; general terms, shipping terms, and return terms. If one of the attributes is missing, I don't show that attribute in the final text. On the iPhone 6, everything shows up exactly the way it should, but on the iPhone 5/5s, the last few lines of text get cutt off. The textView is resizing properly, but it doesn't show all of the text up until the last few lines. I wish I could show photos, but apparently I don't have enough rep to do so. Every other similar issue that I've searched isn't quite the same, and none of those solutions seem to fix the problem. Here is my code, in Swift, for creating the attributed string and resizing the textView. Any help would be greatly appreciated.
This is the code for formatting the attributed string
var termStrings = String()
if (!generalString.isEqualToString("")) {
termStrings += "GENERAL:\n\(generalString)\n\n"
}
if (!shippingString.isEqualToString("")) {
termStrings += "SHIPPING:\n\(shippingString)\n\n"
}
if (!returnString.isEqualToString("")) {
termStrings += "RETURNS:\n\(returnString)"
}
var newNSString = termStrings as NSString
var attTermString = NSMutableAttributedString(string: termStrings as String)
let boldAttribute = [NSForegroundColorAttributeName: UIColor.blackColor(), NSFontAttributeName: UIFont.boldSystemFontOfSize(14)]
let subtitleAttribute = [NSForegroundColorAttributeName: UIColor.blackColor(), NSFontAttributeName: UIFont.systemFontOfSize(12)]
let underlineAttribute = [NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle]
var paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5.0
if (!generalString.isEqualToString("")) {
attTermString.addAttributes(boldAttribute, range: newNSString.rangeOfString("GENERAL:"))
attTermString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: newNSString.rangeOfString("GENERAL:"))
attTermString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: newNSString.rangeOfString("GENERAL:\n"))
attTermString.addAttributes(subtitleAttribute, range: newNSString.rangeOfString("\(generalString)"))
}
if (!shippingString.isEqualToString("")) {
attTermString.addAttributes(boldAttribute, range: newNSString.rangeOfString("SHIPPING:"))
attTermString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: newNSString.rangeOfString("SHIPPING:"))
attTermString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: newNSString.rangeOfString("SHIPPING:\n"))
attTermString.addAttributes(subtitleAttribute, range: newNSString.rangeOfString("\(shippingString)"))
}
if (!returnString.isEqualToString("")) {
attTermString.addAttributes(boldAttribute, range: newNSString.rangeOfString("RETURNS:"))
attTermString.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: newNSString.rangeOfString("RETURNS:"))
attTermString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: newNSString.rangeOfString("RETURNS:\n"))
attTermString.addAttributes(subtitleAttribute, range: newNSString.rangeOfString("\(returnString)"))
}
changeTextViewHeight(attTermString, width: textView.frame.size.width)
This is my code for resizing the textView
func changeTextViewHeight(attributedString: NSMutableAttributedString, width: CGFloat) {
textView.attributedText = attributedString
var newSize: CGSize = textView.sizeThatFits(CGSizeMake(width, CGFloat(FLT_MAX)))
var newFrame: CGRect = textView.frame
newFrame.size = CGSizeMake(CGFloat(fmaxf((Float)(newSize.width), (Float)(width))), newSize.height)
textView.frame = newFrame
}
Steps to resolve (This will set the UITextView scroll to the top in 5/5s)
Disable the UITextView scroll view
set scrollRectToVisible Enable
UITextView scroll
Swift 3:
self.yourTextView.isScrollEnabled = false
let rect:CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
self.yourTextView.scrollRectToVisible(rect, animated: false)
self.yourTextView.isScrollEnabled = true
This Worked for me.

Resources