(2018-08-23 EDIT) Updated the post and re-created the code from almost scratch using XCode 10.0 beta 6 and iOS 12. Code written using Swift 4. (/EDIT).
Introduction
Hi everyone, I have been working, for a few days now, on a project that requires to make an app fully customisable from a configuration file. I was coding and coding and coding, extracting color definitions, applying tints on images, when I ran into an issue. I could not apply tint over a mutable attributed string, nor simple attributed string for that matter. So I was there, looking at my NSAttributedString and my NSTextAttachment without any property allowing me to change only the image color.
It turns out that you actually can apply a color on your attributed string, I don’t know about the underlying magic when using attributed but it seems that you can apply your text color on an image embedded via your text attachment.
Let’s get started
Setting up the UI
First you start by creating a new single view app project.
Next, you need to untick all checkboxes as we do not need UI nor unit tests for this tutorial.
Then you go on your main storyboard (Main.storyboard) and add a label, that you will link to your view controller code (here as labelWithColoredImage).
Here is what your code should look like:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var labelWithColoredImage: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
}
And that’s it terms of UI setup for now. Now you can run your app to be sure it’s behaving properly and that the label appears.
Now the basic NSAttributedString implementation
Now let’s get our hands dirty with the coding part. First let’s update the text and change its color only using the textColor property only. Let’s add the following lines in the viewDidLoad function and run the app again.
labelWithColoredImage.text = "That text written at 4:20AM for reasons.";
labelWithColoredImage.textColor = .redColor();
Then we can observe that the text is red. One minor issue is that it doesn’t fit the screen so we can add contraints to the label to center it and make it responsive.
(2018-08-23 EDIT) I am realising now that I had been a complete tool by not helping with a screenshot showing how to set constraints. Luckily for you it is much more intuitive to have the constraints you want in 2018 than back in 2016. (/EDIT)
Let’s add the text attachment magic now. It’s time to add an image to the project, I just added the image “swift_logo.png” in the project, drag-n-drop works as well as right clicking in the project and selecting “Add files to “.
(2018-08-23 EDIT) That bit is the same, what I did not know then is that I could just drag the image file straight into the assets of the app. I don’t think I bothered but it was probably already a thing in 2016. (/EDIT)
Let’s create our attributed string now with the text attachment, I will have the text over two lines to show how the image is now part of it and due to the image color I change the text color to blue so you can see the difference.
//set the label text color as red
labelWithColoredImage.textColor=.blueColor()
//load the image
let image:UIImage=UIImage(named:"swift_logo")!
//create the text attachment
let textAttachment:NSTextAttachment=NSTextAttachment()
//assign the loaded image to the text attachment
textAttachment.image=image
//create an attributed string with our text attachment
let attributedStringWithImage:NSAttributedString=NSAttributedString(attachment:textAttachment);
//now create the full string to display
let stringToDisplay="That text written at 4:20AM\nfor reasons."
let fullAttributedString:NSMutableAttributedString=
NSMutableAttributedString(string:"\(stringToDisplay)")
//append space the string to display with our string containing the image
fullAttributedString.appendAttributedString(attributedStringWithImage)
//color the image part of the string
//assign it to the label
labelWithColoredImage.attributedText=fullAttributedString;
(2018-08-23 EDIT) Hahaha wow that is so bad. The comments, the spacing, the noise etc. Please do not use any of the previous bit of code. Especially if you want it to compile. I did compile and work back in 2016 but boy I must have been really tired not to notice the first comment being an outright lie. Since then I read Clean Code as mentioned in a more recent post and do not care for comments as much. The comments part was true for some time but even more since that book read. Well, long as the code is clear and self-explanatory. Which it is. Let me refresh that below. (/EDIT)
labelWithColoredImage.textColor = .blue
labelWithColoredImage.textAlignment = .center
if let image = UIImage(named: "swift_logo", in: Bundle.main, compatibleWith: nil) {
let textAttachment = NSTextAttachment()
textAttachment.image = image
let attributedStringWithImage = NSAttributedString(attachment: textAttachment);
let stringToDisplay = "That text written at 4:20AM\nfor reasons."
let fullAttributedString = NSMutableAttributedString(string:"\(stringToDisplay) ")
fullAttributedString.append(attributedStringWithImage)
labelWithColoredImage.attributedText = fullAttributedString
}
(2018-08-23 EDIT) Ok, I had a little dinner break but then I got stuck about an hour for the dumbest reason. I missed that the default for labels is 1 line, so the second line “for reasons” was missing. Missing as in I thought the image was not loading. Googled around and stuff until realising that a part of the string was missing. A whooooole hour later. To be fair I have been travelling a lot over the past few weeks for work, the flights definitely take their toll on me. And yes I am writing this post from a hotel in central Glasgow. (/EDIT).
Another run!
There you have it! Your image integrated within text. Now let’s check what happens if you render your image as a template.
(2018-08-23 EDIT) You need to replace this line `let image: =UIImage(named:”swift_logo”)! with let image: UIImage = UIImage(named: “swift_logo”)!.imageWithRenderingMode(.AlwaysTemplate) then run the app again. Since the structure changed a bit since last time, you need to add a new variable templateImage like below. Next, you will use it as the text attachment image and run your app again. (/EDIT)
let templateImage = image.withRenderingMode(.alwaysTemplate)
let textAttachment = NSTextAttachment()
textAttachment.image = templateImage
Giving the image a color of its own different from the source one
Now the image is the same color as the text, for some of you it could be enough but we can go a little bit further by giving specifically to the image a color of its own. You can add the following lines before the assignment of the NSAttributedString to labelWithColoredImage.
fullAttributedString.addAttribute( NSAttributedString.Key.foregroundColor, value: UIColor.green, range: NSMakeRange(stringToDisplay.count, attributedStringWithImage.length))
Basically these lines say to color in green the character from the index matching the character after stringToDisplay for as long as the text attachment length. Now let’s run the app again.
And there you are, you know know how to apply a different color to an image through a NSAttributedString.
Conclusion
The source code is available on Github
There is one thing to keep in mind though, there must always be a space before the text attachment containing the image in you NSAttributedString otherwise it will not work. I am not quite sure wether the need to add a space is a bug or if it is actually given access to a hidden feature. Either way, happy coding !
(2018-08-23 EDIT) Here we are, this is the first post I update a while after the first publication. It was pretty fun an exercise to take onon and I hope you enjoy it and find it as relevant as a couple years back. (/EDIT).