Adding a stroke to a UILabel text in SWIFT - swift

I am trying to add a stroke to a UILabel text in Swift. This is the text I have used:
var congratsLabel = UILabel(frame: CGRectMake(10, popUpView.frame.size.height * 0.540, popUpView.frame.size.width, 70))
congratsLabel.font = UIFont(name: Font.Daydreamer, size: 70)
congratsLabel.text = "Snap!"
congratsLabel.numberOfLines = 0
congratsLabel.textColor = UIColor.whiteColor()
congratsLabel.textAlignment = NSTextAlignment.Center
popUpView.addSubview(congratsLabel)
I have also tried using the NSMutableAttributedString althoough it is coming out a bit ropey and not how I want, the code I used for that also was:
var String = "Snap!"
var strokeString = NSMutableAttributedString(string: String, attributes: [NSFontAttributeName:UIFont(name: Font.Daydreamer, size: 70.0)!])
strokeString.addAttribute(NSForegroundColorAttributeName, value: UIColor.whiteColor(), range: NSRange(location:0,length:5))
strokeString.addAttribute(NSStrokeColorAttributeName, value: UIColor.blackColor(), range: NSRange(location: 0, length: 5))
strokeString.addAttribute(NSStrokeWidthAttributeName, value: 1, range: NSRange(location: 0, length: 5))
This is the look I am trying to recreate.
But it is coming out like this (The backgrounds are different) but as you can see the stroke is taking over the entire word:

Related

Text Wrapping not happening as it should in NSTextField Attributed String

I am having an issue with text not wrapping correctly if there is a single quote, or macOS ASCII Extended Character #213 (shift+opt.+]) in a string.
Apple does not escape the media item title string when it is retrieved through the iTunesLibrary framework.
As you can see in the example below, the first string is exactly how it come from the iTunesLibrary using the framework API call. The second string is is the single quote is escaped, the third string is if I use macOS Extended ASCII Character code 213, and the fourth string is if I use a tilde. The tilde is not the right character to use in this situation, but it is the only one that correctly wraps the text in the cell.
I've been working on this for the past 6-8 hours to figure it out and I'm just throwing it out there to see if someone can help me.
ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.frame.size = NSSize(width: 616, height: 184)
// Strings
let string1 = "I Keep Forgettin' (Every Time You're Near)"
let string2 = "I Keep Forgettin\' (Every Time You're Near)"
let string3 = "I Keep Forgettin’ (Every Time You're Near)"
let string4 = "I Keep Forgettin` (Every Time You're Near)"
// Formatting
let foreground = NSColor.purple.cgColor
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.tabStops = .none
paragraphStyle.baseWritingDirection = .leftToRight
guard let font = NSFont(name: "Helvetica", size: 28.0) else { return }
// Labels
let label1 = NSTextField(frame: NSRect(x: 20, y: self.view.frame.minY+20, width: 144, height: 144))
label1.cell = VerticallyCenteredTextFieldCell()
label1.wantsLayer = true
label1.layer?.borderColor = NSColor.purple.cgColor
label1.layer?.borderWidth = 0.5
label1.layer?.backgroundColor = NSColor.lightGray.cgColor
label1.alphaValue = 1
var fontSize = bestFontSize(attributedString: NSAttributedString(string: string1, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label1.attributedStringValue = NSAttributedString(string: string1, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label1)
let label2 = NSTextField(frame: NSRect(x: 164, y: self.view.frame.minY+20, width: 144, height: 144))
label2.cell = VerticallyCenteredTextFieldCell()
label2.wantsLayer = true
label2.layer?.borderColor = NSColor.purple.cgColor
label2.layer?.borderWidth = 0.5
label2.layer?.backgroundColor = NSColor.lightGray.cgColor
label2.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string2, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label2.attributedStringValue = NSAttributedString(string: string2, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label2)
let label3 = NSTextField(frame: NSRect(x: 308, y: self.view.frame.minY+20, width: 144, height: 144))
label3.cell = VerticallyCenteredTextFieldCell()
label3.wantsLayer = true
label3.layer?.borderColor = NSColor.purple.cgColor
label3.layer?.borderWidth = 0.5
label3.layer?.backgroundColor = NSColor.lightGray.cgColor
label3.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string3, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label3.attributedStringValue = NSAttributedString(string: string3, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label3)
let label4 = NSTextField(frame: NSRect(x: 452, y: self.view.frame.minY+20, width: 144, height: 144))
label4.cell = VerticallyCenteredTextFieldCell()
label4.wantsLayer = true
label4.layer?.borderColor = NSColor.purple.cgColor
label4.layer?.borderWidth = 0.5
label4.layer?.backgroundColor = NSColor.lightGray.cgColor
label4.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string4, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label4.attributedStringValue = NSAttributedString(string: string4, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label4)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func bestFontSize(attributedString: NSAttributedString, size: CGSize) -> CGFloat {
// Create a property to hold the font and size
var font: NSFont?
// Get the font information from the string attibutes
attributedString.enumerateAttribute(.font, in: NSRange(0..<attributedString.length)) { value, range, stop in
if let attrFont = value as? NSFont {
font = attrFont
}
}
if font == nil {
return 0
}
// Get any paragraph styling attributes
var paragraphStyle: NSMutableParagraphStyle?
attributedString.enumerateAttribute(.paragraphStyle, in: NSMakeRange(0, attributedString.length)) { value, range, stop in
if let style = value as? NSMutableParagraphStyle {
paragraphStyle = style
}
}
if paragraphStyle == nil {
return 0
}
// Create a sorted list of words from the string in descending order of length (chars) of the word
let fragment = attributedString.string.split(separator: " ").sorted() { $0.count > $1.count }
// Create a bounding box size that will be used to check the width of the largest word in the string
var width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
// Create a bounding box size that will be used to check the height of the string
var height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
while height >= size.height || width >= size.width {
guard let pointSize = font?.pointSize else {
return 0
}
font = font?.withSize(pointSize-0.25)
width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
}
return font!.pointSize
}
}
VerticallyCenteredTextFieldCell.swift
import Cocoa
class VerticallyCenteredTextFieldCell: NSTextFieldCell {
// https://stackoverflow.com/questions/11775128/set-text-vertical-center-in-nstextfield/33788973 - Sayanti Mondal
func adjustedFrame(toVerticallyCenterText rect: NSRect) -> NSRect {
// super would normally draw from the top of the cell
var titleRect = super.titleRect(forBounds: rect)
let minimumHeight = self.cellSize(forBounds: rect).height
titleRect.origin.y += (titleRect.height - minimumHeight) / 2
titleRect.size.height = minimumHeight
return titleRect
}
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawInterior(withFrame: adjustedFrame(toVerticallyCenterText: cellFrame), in: controlView)
}
}
This is the result I get:
Anyone else get the same result running this?

NSTextAttachment.bounds does not work on iOS 15

To add an image attachment to an attributed string I used the following code that works fine before iOS 15:
let textAttachment = NSTextAttachment()
textAttachment.image = UIImage(named:"imageName")!
textAttachment.bounds = CGSize(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
let textAttachmentString = NSMutableAttributedString(attachment: textAttachment)
let attributedText = NSMutableAttributedString(string: "base text", attributes: [:])
attributedText.append(textAttachmentString)
The problem is that after iOS 15 textAttachment.bounds.origin.x does not work: there is no space between text/image and the origin of the attachment is unchanged.
Probably NSTextAttachmentViewProvider should be used, but I don't know how to procede.
A while back I got around the problem with an inelegant workaround, I wrote a function that inserts an empty text attachment into the content to create an horizontal space between the text and the icon:
func addIconToText(_ toText: NSAttributedString, icon: UIImage, iconOrigin: CGPoint, iconSize: CGSize, atEnd: Bool)->NSAttributedString{
// Inserts a textAttachment containing the given icon into the attributed string (at the beginning or at the end of the text depending on the value of atEnd)
// Constructs an empty text attachment to separate the icon from the text instead of using NSTextAttachment.bounds.origin.x because it does not work with iOS 15
let iconTextAttachment = NSTextAttachment(data: icon.withRenderingMode(.alwaysOriginal).pngData(), ofType: "public.png")
iconTextAttachment.bounds = CGRect(origin: CGPoint(x: 0, y: iconOrigin.y), size: iconSize)
let iconString = NSMutableAttributedString(attachment: iconTextAttachment)
let emptyIconSize = CGSize(width: abs(iconOrigin.x), height: abs(iconOrigin.y))
let emptyIconTextAttachment = NSTextAttachment(image: UIGraphicsImageRenderer(size: emptyIconSize).image(actions: {
con in
UIColor.clear.setFill()
con.fill(CGRect(origin: .zero, size: emptyIconSize))
}))
emptyIconTextAttachment.bounds = CGRect(origin: .zero, size: emptyIconSize)
let emptyIconString = NSMutableAttributedString(attachment: emptyIconTextAttachment)
var finalString: NSMutableAttributedString!
if atEnd{
finalString = NSMutableAttributedString(attributedString: toText)
finalString.append(emptyIconString)
finalString.append(iconString)
}else{
finalString = NSMutableAttributedString(attributedString: iconString)
finalString.append(emptyIconString)
finalString.append(toText)
}
return finalString
}

Swift: Can I adjust attributed text with dynamic font size?

I'm working with the collectionView
whose each cell has the content like this:
everything works pretty well until I test it on an iphone 5s:
"Ho Chi Minh City" was out of bounds.
Here is my code:
func configureNameLabel() {
nameLabel.numberOfLines = 2
let attributedText = NSMutableAttributedString(string: post?.name ?? "", attributes: [.font: UIFont.boldSystemFont(ofSize: 15)])
attributedText.append(NSAttributedString(string: "\nSaturday, December 1, 2018 ⦁ Ho Chi Minh City ⦁ ", attributes:
[.foregroundColor: UIColor.lightGray, .font: UIFont.preferredFont(forTextStyle: .caption1)]))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5
attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedText.string.count))
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "user-male")
attachment.bounds = CGRect(x: 0, y: -2, width: 12, height: 12)
attributedText.append(NSAttributedString(attachment: attachment))
nameLabel.attributedText = attributedText
}
I think this problem is due to the font size, can anybody fix this ?
Thanks guys, I solved it. "label.numberOfLines = 0" worked. the problem is at my constraints.

How to set the UIColor for each letter in a string

Is there a simple way to set the colour for each letter in a UILabel in swift? (Or any other text for that matter!)
For example
let label = UILabel(frame CGRectMake(100,100,100,100))
label.text = "spell"
label.textColor = UIColor.whiteColor()
But I want the S to be red, the p to be blue, the e to be yellow etc.
Thanks in advance if anyone knows how to do this easily...
I got this to work with help from those below. And added the attributed string to a UIButton title.
let mainspring = "spell"
let spellAttrString: NSMutableAttributedString = NSMutableAttributedString(string: mainString)
spellAttrString.addAttribute(NSForegroundColorAttributeName, value: UIColor.blueColor(), range: NSRange(location: 0,length: 1))
spellAttrString.addAttribute(NSForegroundColorAttributeName, value: UIColor.greenColor(), range: NSRange(location: 1, length: 1))
spellAttrString.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellowColor(), range: NSRange(location: 2, length: 1))
spellAttrString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSRange(location: 3, length: 1))
spellAttrString.addAttribute(NSForegroundColorAttributeName, value: UIColor.magentaColor(), range: NSRange(location: 4, length: 1))
[spellButton .setAttributedTitle(spellAttrString, forState: .Normal)]
For creating of the colorful string you should use NSMutableAttributedString.
Small example:
var attrString: NSMutableAttributedString = NSMutableAttributedString(string: "Some text")
attrString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, 3))

is it possible to set the UILabel distance between the line?

Is it possible to set a UILabel's distance between the line, as i had a UILabel contain 3 lines, and linebreakmode is wordwrap?
If you're referring to "leading", which refers to the gap between lines of type - you cannot change this on a UILabel. That is inferred from the front of the label itself. Some people have tried to create categories to override the "leading" property of the UIFont for the label but it doesn't actually work when rendering.
If you really need to control the vertical spacing between lines of text then your best bet is to programmatically drop 1 UILabel per line of fixed width and control the vertical gap yourself.
Here is how you can set line spacing using interface builder and programatically as well.
From Interface Builder:
Programmatically:
SWift 4
Using label extension
extension UILabel {
// Pass value for any one of both parameters and see result
func setLineSpacing(lineSpacing: CGFloat = 0.0, lineHeightMultiple: CGFloat = 0.0) {
guard let labelText = self.text else { return }
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
paragraphStyle.lineHeightMultiple = lineHeightMultiple
let attributedString:NSMutableAttributedString
if let labelattributedText = self.attributedText {
attributedString = NSMutableAttributedString(attributedString: labelattributedText)
} else {
attributedString = NSMutableAttributedString(string: labelText)
}
// Line spacing attribute
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
self.attributedText = attributedString
}
}
Now call extension function
let label = UILabel()
let stringValue = "is\nit\npossible\nto\nset\nthe\nUILabel\ndistance\nbetween\nthe\nline?"
// Pass value for any one argument - lineSpacing or lineHeightMultiple
label.setLineSpacing(lineSpacing: 2.0) . // try values 1.0 to 5.0
// or try lineHeightMultiple
//label.setLineSpacing(lineHeightMultiple = 2.0) // try values 0.5 to 2.0
Or using label instance (Just copy & execute this code to see result)
let label = UILabel()
let stringValue = "is\nit\npossible\nto\nset\nthe\nUILabel\ndistance\nbetween\nthe\nline?"
let attrString = NSMutableAttributedString(string: stringValue)
var 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
// Line spacing attribute
attrString.addAttribute(NSAttributedStringKey.paragraphStyle, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
// Character spacing attribute
attrString.addAttribute(NSAttributedStringKey.kern, value: 2, range: NSMakeRange(0, attrString.length))
label.attributedText = attrString