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

相关文章
相关标签/搜索