Swift 5 字符串插值-AttributedStrings

做者:Olivier Halligon,原文連接,原文日期:2018-12-16 譯者:Nemocdz;校對:numbbbbbWAMaker;定稿: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 中渲染,結果是這個樣子的:工具

image

☝️ 是的,上面的文字和圖片……真的是一個 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))!
  """
複製代碼

imgage

結論

期待你享受這一系列 StringInterpolation 文章,而且能從中瞥到這個新設計威力的冰山一角。

你能夠 在這下載個人 Playground 文件,裏面有 GitHubComment(見 第一部分),AttrString 的所有實現,說不定還能從我簡單實現 RegEX 的嘗試中獲得一些靈感。

這裏還有更多更好的思路去使用 Swift 5 中新的 ExpressibleByStringInterpolation API - 包括 Erica Sadun 博客裏這篇這篇這篇 - 還在猶豫什麼,閱讀更多……從中感覺樂趣吧!


  1. 這篇文章和 Playground 裏的代碼,須要使用 Swift 5。在寫做時,最新的 Xcode 版本是 10.1,Swift 4.2,因此若是你想嘗試這些代碼,須要遵循官方指南去下載開發中的 Swift 5 快照。安裝 Swift 5 工具鏈並在 Xcode 偏好設置裏啓用並不困難(見官方指南)。
  2. 固然,這裏僅做爲 Demo,只實現了一部分樣式。將來能夠延伸思路讓 Style 類型支持更多的樣式,在理想狀況下,能夠覆蓋全部存在 NSAttributedString.Key

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg

相關文章
相關標籤/搜索