Olivier Halligon, Translator: Nemocdz; Proofreading: Numbbbbb, WAMaker; Finalized: Pancf
We have already introduced the new StringInterpolation design for the Swift 5 in previous articles. In the second part, I will focus on ExpressibleByStringInterpolation one of the application, make NSAttributedString more elegant.
The target
One of the applications that immediately came to mind when I saw the new StringInterpolation design for Swift 5 was to simplify the generation of NSAttributedString.
My goal is to create a view string using syntax like the following:
let username = "AliGator" let str: AttrString = """ Hello \(username, .color(.red)), isn't this \("cool", .color(.blue), .oblique, .underline(.purple, .single))? \(wrap: """ \(" Merry Xmas! ", .font(.systemFont(ofSize: 36)), .color(.red), .bgColor(.yellow)) \(image: #imageLiteral(resourceName: "santa.jpg"), scale: 0.2) """,.center) Go there to \(" Learn more about String Interpolation", .link("https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md"), .underline(.blue, .single))! "" "Copy the code
This large string not only uses the multi-line string literal syntax (incidentally, this feature was added to Swift4, in case you missed it) — but also contains one multi-line string literal within another (see \(wrap:…). Paragraph)! – It even includes interpolation to add some styles to some characters… So it’s a combination of lots of new Swift features!
This NSAttributedString would look something like this if rendered in a UILabel or NSTextView:
☝️ Yes, the words and pictures above… Really just an NSAttributedString(not a complex view layout or anything)! 🤯
The preliminary implementation
So, where to start? Of course, it’s similar to how to implement GitHubComment in Part 1!
Well, before we actually tackle string interpolation, let’s start by declaring unique types.
struct AttrString {
let attributedString: NSAttributedString
}
extension AttrString: ExpressibleByStringLiteral {
init(stringLiteral: String) {
self.attributedString = NSAttributedString(string: stringLiteral)
}
}
extension AttrString: CustomStringConvertible {
var description: String {
return String(describing: self.attributedString)
}
}
Copy the code
Pretty simple, right? It’s just encapsulating NSAttributedString. Now, let’s add ExpressibleByStringInterpolation support, to support both literal and take NSAttributedString attributes of a string.
extension AttrString: ExpressibleByStringInterpolation {
init(stringInterpolation: StringInterpolation) {
self.attributedString = NSAttributedString(attributedString: stringInterpolation.attributedString)
}
struct StringInterpolation: StringInterpolationProtocol {
var attributedString: NSMutableAttributedString
init(literalCapacity: Int, interpolationCount: Int) {
self.attributedString = NSMutableAttributedString()
}
func appendLiteral(_ literal: String) {
let astr = NSAttributedString(string: literal)
self.attributedString.append(astr)
}
func appendInterpolation(_ string: String, attributes: [NSAttributedString.Key: Any]) {
let astr = NSAttributedString(string: string, attributes: attributes)
self.attributedString.append(astr)
}
}
}
Copy the code
At this point, you can simply build an NSAttributedString as follows:
let user = "AliSoftware" let str: AttrString = """ Hello \(user, attributes: [.foregroundColor: NSColor.blue])! "" "Copy the code
Doesn’t that look elegant already?
Easy style addition
But treating attributes like a dictionary [nAttributedString.key: Any] is not elegant. Especially since Any has no explicit type, knowing the explicit type of each key value is required…
So we can make it more elegant by creating a unique Style type that helps us build a dictionary of attributes:
extension AttrString { struct Style { let attributes: [NSAttributedString.Key: Any] static func font(_ font: NSFont) -> Style { return Style(attributes: [.font: font]) } static func color(_ color: NSColor) -> Style { return Style(attributes: [.foregroundColor: color]) } static func bgColor(_ color: NSColor) -> Style { return Style(attributes: [.backgroundColor: color]) } static func link(_ link: String) -> Style { return .link(URL(string: link)!) } static func link(_ link: URL) -> Style { return Style(attributes: [.link: Link])} static let oblique = Style(attributes: [.obliqueness: 0.1]) static func underline(_ color: NSColor, _ Style: NSUnderlineStyle) -> Style { return Style(attributes: [ .underlineColor: color, .underlineStyle: style.rawValue ]) } static func alignment(_ alignment: NSTextAlignment) -> Style { let ps = NSMutableParagraphStyle() ps.alignment = alignment return Style(attributes: [.paragraphStyle: ps]) } } }Copy the code
This allows you to use style.color (.blue) to simply create a Style that encapsulates [.foregroundColor: nscolor.blue].
Don’t stop there, now let our StringInterpolation handle the following Style property!
The idea is to write something like this:
let str: AttrString = """ Hello \(user, .color(.blue)), how do you like this? "" "Copy the code
Is it more elegant? We just need to implement appendInterpolation properly for it!
extension AttrString.StringInterpolation {
func appendInterpolation(_ string: String, _ style: AttrString.Style) {
let astr = NSAttributedString(string: string, attributes: style.attributes)
self.attributedString.append(astr)
}
Copy the code
And then you’re done! But… Only one Style is supported at a time. Why not allow it to pass in multiple styles as parameters? This can be done with a [Style] parameter, but this requires the calling side to enclose the Style list in parentheses… Why don’t we make it use morphable parameters?
Let’s replace the previous implementation with this:
extension AttrString.StringInterpolation {
func appendInterpolation(_ string: String, _ style: AttrString.Style...) {
var attrs: [NSAttributedString.Key: Any] = [:]
style.forEach { attrs.merge($0.attributes, uniquingKeysWith: {$1}) }
let astr = NSAttributedString(string: string, attributes: attrs)
self.attributedString.append(astr)
}
}
Copy the code
Now you can mix the styles together!
let str: AttrString = """ Hello \(user, .color(.blue), .underline(.red, .single)), how do you like this? "" "Copy the code
Support image
Another capability of NSAttributedString is to add an image using NSAttributedString(Attachment: NSTextAttachment), making it part of the string. To implement it, just implement appendInterpolation(image: NSImage) and call it.
I’d like to add the ability to zoom in and out of images to this feature. Since I tried it on the Playground of macOS, whose graphics context was flipped, I had to flip the image back as well (note that this detail may not be the same as when UIImage support is implemented on iOS). Here’s how I did it:
extension AttrString.StringInterpolation { func appendInterpolation(image: NSImage, scale: CGFloat = 1.0) {let attachment = NSTextAttachment() let size = NSSize(width: image.size. Width * scale, height: image.size.height * scale ) attachment.image = NSImage(size: size, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in NSGraphicsContext.current? .cgContext.translateBy(x: 0, y: size.height) NSGraphicsContext.current? .cgContext.scaleBy(x: 1, y: -1) image.draw(in: rect) return true }) self.attributedString.append(NSAttributedString(attachment: attachment)) } }Copy the code
Style nested
Finally, sometimes you will want to apply a style to a large paragraph of text, but it may also include sub-paragraph styles. Like “Hello < I >world
” in HTML, the whole paragraph is bold but some italics are included.
Right now our API doesn’t support this, so let’s add it. The idea is to allow a string of styles… Apply not only to strings, but also to attrStrings that already have attributes.
AppendInterpolation (_ string: string, _ style: style…) Similar, but can modify AttrString attributedString to add attributes to the above, rather than just use a String to create an entirely new NSAttributedString.
extension AttrString.StringInterpolation { func appendInterpolation(wrap string: AttrString, _ style: AttrString.Style...) { var attrs: [NSAttributedString.Key: Any] = [:] style.forEach { attrs.merge($0.attributes, uniquingKeysWith: {$1}) } let mas = NSMutableAttributedString(attributedString: string.attributedString) let fullRange = NSRange(mas.string.startIndex.. <mas.string.endIndex, in: mas.string) mas.addAttributes(attrs, range: fullRange) self.attributedString.append(mas) } }Copy the code
Now that we’re done, we can create an AttributedString from a simple string with interpolation:
let username = "AliGator" let str: AttrString = """ Hello \(username, .color(.red)), isn't this \("cool", .color(.blue), .oblique, .underline(.purple, .single))? \(wrap: """ \(" Merry Xmas! ", .font(.systemFont(ofSize: 36)), .color(.red), .bgColor(.yellow)) \(image: #imageLiteral(resourceName: "santa.jpg"), scale: 0.2) """,.center) Go there to \(" Learn more about String Interpolation", .link("https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md"), .underline(.blue, .single))! "" "Copy the code
conclusion
We hope you enjoy this series of StringInterpolation articles and get a glimpse of the power of this new design.
You can download my Playground file here, with GitHubComment(see part 1), the full implementation of AttrString, and perhaps some inspiration from my attempt at a simple RegEX.
There are more and better ideas to use Swift 5 new ExpressibleByStringInterpolation API – including Erica Sadun blog this article, this article and this article – what, not hesitate to read more… Have fun!
- Swift 5 is required for this article and the code in Playground. At the time of writing, the latest Xcode version is 10.1, Swift 4.2, so if you want to try this code out, follow the official guidelines to download Swift 5 snapshots in development. It is not difficult to install the Swift 5 toolchain and enable it in Xcode preferences (see the official guide).
- Of course, this is only a Demo, and only part of the style is implemented. The future can be extended thinking
Style
Types support more styles and, ideally, override all that existsNSAttributedString.Key
.
This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.