做者:Olivier Halligon,原文連接,原文日期:2018-12-16 譯者:Nemocdz;校對:numbbbbb,WAMaker;定稿:Pancfgit
咱們已經在 前文 裏介紹了 Swift 5 全新的 StringInterpolation 設計。在這第二部分中,我會着眼於 ExpressibleByStringInterpolation
其中一種應用,讓 NSAttributedString
變得更優雅。github
在看到 Swift 5 這個全新的 StringInterpolation 設計 時,我立刻想到的應用之一就是簡化 NSAttributedString
的生成。express
個人目標是作到用相似下面的語法建立一個 attributed 字符串:swift
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)
""", .alignment(.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))!
"""
複製代碼
這一大串字符串不只使用了多行字符串的字面量語法(順帶一提,這個特性是在 Swift4 中新增的,以避免你錯過了) ——並且在其中一個多行字符串字面量中包含了另外一個(見 \(wrap: ...)
段落)!- 甚至還包含了給一部分字符添加一些樣式的插值……因此由大量 Swift 新特性組合而成!app
這個 NSAttributedString
若是在一個 UILabel
或者 NSTextView
中渲染,結果是這個樣子的:工具
☝️ 是的,上面的文字和圖片……真的只是一個 NSAttributedString
(而不是一個複雜的視圖佈局或者其餘)! 🤯佈局
因此,從哪裏開始實現?固然和第一部分中如何實現 GitHubComment
差很少!ui
好的,在實際解決字符串插值以前,咱們先從聲明特有類型開始。this
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)
}
}
複製代碼
挺簡單的吧?僅僅給 NSAttributedString
封裝了一下。如今,讓咱們添加 ExpressibleByStringInterpolation
的支持,來同時支持字面量和帶 NSAttributedString
屬性註釋的字符串。spa
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)
}
}
}
複製代碼
這時,已經能夠用下面這種方式簡單地構建一個 NSAttributedString
了:
let user = "AliSoftware"
let str: AttrString = """
Hello \(user, attributes: [.foregroundColor: NSColor.blue])!
"""
複製代碼
這看起來已經優雅多了吧?
但用字典 [NAttributedString.Key: Any]
的方式處理屬性不夠優雅。特別是因爲 Any
沒有明確類型,要求瞭解每個鍵值的明確類型……
因此能夠經過建立特有的 Style
類型讓它變得更優雅,並幫助咱們構建屬性的字典:
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])
}
}
}
複製代碼
這容許使用 Style.color(.blue)
來簡單地建立一個封裝了 [.foregroundColor: NSColor.blue]
的 Style
。
可別止步於此,如今讓咱們的 StringInterpolation
能夠處理下面這樣的 Style
屬性!
這個想法是能夠作到像這樣寫:
let str: AttrString = """
Hello \(user, .color(.blue)), how do you like this?
"""
複製代碼
是否是更優雅?而咱們僅僅須要爲它正確實現 appendInterpolation
而已!
extension AttrString.StringInterpolation {
func appendInterpolation(_ string: String, _ style: AttrString.Style) {
let astr = NSAttributedString(string: string, attributes: style.attributes)
self.attributedString.append(astr)
}
複製代碼
而後就完成了!但……這樣一次只支持一個 Style
。爲何不容許它傳入多個 Style
做爲形參呢?這能夠用一個 [Style]
形參來實現,但這要求調用側將樣式列表用括號括起來……不如讓它使用可變形參?
讓咱們用這種方式來代替以前的實現:
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)
}
}
複製代碼
如今能夠將多種樣式混合起來了!
let str: AttrString = """
Hello \(user, .color(.blue), .underline(.red, .single)), how do you like this?
"""
複製代碼
NSAttributedString
的另外一種能力是使用 NSAttributedString(attachment: NSTextAttachment)
添加圖像,讓它成爲字符串的一部分。要實現它,僅須要實現 appendInterpolation(image: NSImage)
並調用它。
我但願爲這個特性順便加上縮放圖像的能力。因爲我是在 macOS 的 playground 上嘗試的,它的圖形上下文是翻轉的,因此也得將圖像翻轉回來(注意這個細節可能會和 iOS 上實現對 UIImage 的支持時不同)。這裏是個人作法:
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))
}
}
複製代碼
最後,有時候你會但願應用一個樣式在一大段文字上,但裏面可能也包含了子段落的樣式。就像 HTML 裏的 "<b>Hello <i>world</i></b>"
,整段是粗體但包含了一部分斜體的。
如今咱們的 API 還不支持這樣,因此讓咱們來加上它。思路是容許將一串 Style…
不止應用在 String
上,還能應用在已經存在屬性的 AttrString
上。
這個實現和 appendInterpolation(_ string: String, _ style: Style…)
類似,但會修改 AttrString.attributedString
來添加屬性到上面,而不是單純用 String
建立一個全新的 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)
}
}
複製代碼
上面這些所有完成以後,目標就達成了,終於能夠用單純的字符串加上插值建立一個 AttributedString:
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)
""", .alignment(.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))!
"""
複製代碼
期待你享受這一系列 StringInterpolation
文章,而且能從中瞥到這個新設計威力的冰山一角。
你能夠 在這下載個人 Playground 文件,裏面有 GitHubComment
(見 第一部分),AttrString
的所有實現,說不定還能從我簡單實現 RegEX
的嘗試中獲得一些靈感。
這裏還有更多更好的思路去使用 Swift 5 中新的 ExpressibleByStringInterpolation
API - 包括 Erica Sadun 博客裏這篇、這篇 和 這篇 - 還在猶豫什麼,閱讀更多……從中感覺樂趣吧!
Style
類型支持更多的樣式,在理想狀況下,能夠覆蓋全部存在 NSAttributedString.Key
。本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg。