前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WWDC 2021新Formatter API:新老比较及如何自定义

WWDC 2021新Formatter API:新老比较及如何自定义

作者头像
东坡肘子
发布2022-07-28 12:54:59
1.4K0
发布2022-07-28 12:54:59
举报
文章被收录于专栏:肘子的Swift记事本

WWDC 2021新Formatter API:新老比较及如何自定义

在WWDC 2021的What's in Foundation专题中,苹果隆重介绍了适用于Swift的新Formatter API。网上已经有不少文章对新API的用法进行了说明。本文将通过介绍如何创建符合新API的Formatter,让读者从另一个角度了解新Formatter API的设计机制;并对新旧两款API进行比较。

请访问我的博客www.fatbobman.com获得更好的阅读体验

本文的演示代码可以在Github上下载

新旧交替或风格转换

新Formatter API可以做什么

新Formatter提供了一个便捷的接口,让Swift程序员以更熟悉方式在应用程序中呈现本地化的格式字符串。

新API比旧API好吗

好和坏都是相对的,对于以Swift开发为主或者只会Swift的程序员(比如我本人),新Formatter不仅学习和使用起来更容易,同时也更适合日益流行的声明式编程风格。不过从整体功能和效率上讲,新Formatter并不具备优势。

新旧API比较

调用方便度

如果说新API相较旧API的最大优势,便是在调用上更符合直觉、更方便了。

旧API:

代码语言:javascript
复制
      let number = 3.147      let numberFormat = NumberFormatter()      numberFormat.numberStyle = .decimal      numberFormat.maximumFractionDigits = 2      numberFormat.roundingMode = .halfUp      let numString = numberFormat.string(from: NSNumber(3.147))!      // 3.15

新API:

代码语言:javascript
复制
      let number = 3.147      let numString = number.formatted(.number.precision(.fractionLength(2)).rounded(rule: .up))      // 3.15

旧API:

代码语言:javascript
复制
      let numberlist = [3.345,534.3412,4546.4254]      let numberFormat = NumberFormatter()              numberFormat.numberStyle = .decimal              numberFormat.maximumFractionDigits = 2              numberFormat.roundingMode = .halfUp      let listFormat = ListFormatter()      let listString = listFormat                  .string(from:                              numberlist                              .compactMap{numberFormat.string(from:NSNumber(value: $0))}                  ) ?? ""      // 3.35, 534.35, and 4,546.43

新API:

代码语言:javascript
复制
        let numString1 = numberlist.formatted(            .list(                memberStyle: .number.precision(.fractionLength(2)).rounded(rule: .up),                type: .and            )        )    // 3.35, 534.35, and 4,546.43

即使你对新API并不很了解,仅凭代码的自动提示你就可以快速组合出想要的格式化结果。

运行效率

在WWDC视频中,苹果几次提及新API对性能的提升。不过苹果并没有告诉你全部的真相。

从我个人的测试数据来看,新API的效率相较于仅使用一次的Formatter实例来说,提升还是比较明显的(30% —— 300%),不过同可复用的Formatter实例比较,仍有数量级上的差距。

旧API,每次都重新创建实例

代码语言:javascript
复制
    func testDateFormatterLong() throws {        measure {            for _ in 0..<count {                let date = Date()                let formatter = DateFormatter()                formatter.dateStyle = .full                formatter.timeStyle = .full                _ = formatter.string(from: date)            }        }    }// 0.121

旧API,只创建一次实例

代码语言:javascript
复制
    func testDateFormatterLongCreateOnce() throws {        let formatter = DateFormatter()        measure {            for _ in 0..<count {                let date = Date()                formatter.dateStyle = .full                formatter.timeStyle = .full                _ = formatter.string(from: date)            }        }    }// 0.005

新API

代码语言:javascript
复制
    func testDateFormatStyleLong() throws {        measure {            for _ in 0..<count {                _ = Date().formatted(.dateTime.year().month(.wide).day().weekday(.wide).hour(.conversationalTwoDigits(amPM: .wide)).minute(.defaultDigits).second(.twoDigits).timeZone(.genericName(.long)))            }        }    }// 0.085

使用新API,配置的内容越多,执行所需时间也会相应增长。不过除非是对性能有非常高要求的场景,否则新API的执行效率还是有可以令人满意的。

本文的Demo中,附带了部分Unit Test代码,大家可以自行测试。

统一性

旧API中,针对不同的格式化类型,我们需要创建不同的Formatter实例。比如使用NumberFormatter格式化数字、DateFormatter格式化日期。

新API针对每个支持的类型都提供了统一的调用接口,尽量减少代码层面的复杂度

代码语言:javascript
复制
Date.now.formatted()// 9/30/2021, 2:12 PM345.formatted(.number.precision(.integerLength(5)))// 00,345Date.now.addingTimeInterval(100000).formatted(.relative(presentation: .named))// tomorrow
自定义难度

新API的调用便利性是建立在大量繁杂工作的基础之上的。相较于旧API通过属性直接设置,新API采用函数式编程方式,针对每个属性单独编写设置方法。虽然并不复杂,但工作量明显提高。

AttributedString

新API为每个可转换类型都提供AttributedString格式支持。通过AttribtedString中的Field,可以方便的生成想要的显示样式。

比如:

代码语言:javascript
复制
    var dateString: AttributedString {        var attributedString = Date.now.formatted(.dateTime            .hour()            .minute()            .weekday()            .attributed        )        let weekContainer = AttributeContainer()            .dateField(.weekday)        let colorContainer = AttributeContainer()            .foregroundColor(.red)        attributedString.replaceAttributes(weekContainer, with: colorContainer)        return attributedString    }Text(dateString)

image-20210930142453213

代码出错率

在新API中,一切都是类型安全的,开发者无需反复的查阅文档,你的代码可以享受编译时检查的好处。

比如下面的代码

旧API

代码语言:javascript
复制
let dateFormatter:DateFormatter = {    let formatter = DateFormatter()    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"    return formatter}()let dateString = dateFormatter.string(from: Date.now)

新API

代码语言:javascript
复制
let dateString = Date.now.formatted(.iso8601.year().month().day().dateSeparator(.dash).dateTimeSeparator(.space).time(includingFractionalSeconds: false) .timeSeparator(.colon))

如果单从代码量上来看,在本例中,新API不占据任何优势。不过你无需在yyyy和YYYY或者MM还是mm中犹豫,也不用反复查看令人头痛的文档,减少了在代码中犯错的可能性。

风格转换?

旧API是Objc的产物,它十分高效且好用,但在Swift中使用难免有不协调感。

新API是完全为Swift开发的,它采用了当前流行的声明式的风格。开发者只需要声明需要显示的字段,系统将以合适的格式进行呈现。

两种风格将在苹果的开发生态中长期共存,开发者可以选择适合自己的方式来实现同一个目标。

因此不存在风格转换的问题,苹果只是补交了Swift开发环境上缺失的一部分而已。

结论

新旧API将长期共存。

新API并非用来替换旧的Formatter API,应该算是旧Formatter的Swift实现版本。新API基本涵盖了旧API绝大多数的功能,着重改善了开发者的使用体验。

类似的情况在最近几年中将不断上演,苹果在Swift语言层面基本完善的情况下,将逐步提供其核心框架的Swift版本。本届WWDC上推出的AttributedString也可以佐证这一点。

如何自定义新的Formatter

新老API在自定义方面的不同

旧API是用类实现的,在创建自定义格式化器时,我们需要创建一个Formatter的子类,并至少实现以下两个方法:

代码语言:javascript
复制
class MyFormatter:Formatter {   // 将被格式化类型转换成格式类型(字符串)    override func string(for obj: Any?) -> String?{        guard let value = obj as? Double else {return nil}        return String(value)    }   // 将格式化类型(字符串)转换回被格式化类型    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool{        guard let value = Double(string) else {return false}        obj?.pointee = value as AnyObject        return true    }}

需要的情况下,我们也可以提供NSAttributedString的格式化实现

代码语言:javascript
复制
    override func attributedString(for obj: Any, withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString? {        nil    }

数据的格式转换都是在一个类定义中完成的。

新API充分体现了Swift作为面向协议语言的特点,使用两个协议(FormatStyleParseStrategy),分别定义了格式化数据和从格式化转换两个方向的实现。

新协议

FormatStyle

将被格式化类型转换成格式化类型。

代码语言:javascript
复制
public protocol FormatStyle : Decodable, Encodable, Hashable {    /// The type of data to format.    associatedtype FormatInput    /// The type of the formatted data.    associatedtype FormatOutput    /// Creates a `FormatOutput` instance from `value`.    func format(_ value: Self.FormatInput) -> Self.FormatOutput    /// If the format allows selecting a locale, returns a copy of this format with the new locale set. Default implementation returns an unmodified self.    func locale(_ locale: Locale) -> Self}

尽管在导出类型上使用了泛型,不过由于新API着重于格式化(而不是类型转换),因此通常FormatOutpu为String或者AttributedString

func format(_ value: Self.FormatInput) -> Self.FormatOutput是必须实现的方法,locale用来为Formatter设置区域信息,其返回值中的format方法的输出类型同原结构一致。因此,尽管Formatter会针对不同区域提供不同语言的返回结果,但为了兼容性,返回结果仍为String

FormatStyle协议同时约定了必须满足Codable和Hashable。

ParseStrategy

将格式化后的数据转换成被格式化类型

代码语言:javascript
复制
public protocol ParseStrategy : Decodable, Encodable, Hashable {    /// The type of the representation describing the data.    associatedtype ParseInput    /// The type of the data type.    associatedtype ParseOutput    /// Creates an instance of the `ParseOutput` type from `value`.    func parse(_ value: Self.ParseInput) throws -> Self.ParseOutput}

parse的定义可比旧API的getObjectValue容易理解多了。

ParseableFromatStyle

由于FormatStyleParseStrategy是两个独立的协议,因此苹果又提供了ParseableFromatStyle协议,方便我们在一个结构体中实现两个协议的方法。

代码语言:javascript
复制
public protocol ParseableFormatStyle : FormatStyle {    associatedtype Strategy : ParseStrategy where Self.FormatInput == Self.Strategy.ParseOutput, Self.FormatOutput == Self.Strategy.ParseInput    /// A `ParseStrategy` that can be used to parse this `FormatStyle`'s output    var parseStrategy: Self.Strategy { get }}

尽管理论上也可以通过FormatStyle&ParseStrategy在一个结构体中实现双向转换,不过官方框架只支持通过ParseableFromatStyle协议实现的Formatter。

其他

尽管ParseableFromatStyle协议并没有要求一定要输出AttributedString,不过在官方的新Formatter API中还是为每个类型都提供了AttributedString的输出。

为了方便Formatter的调用,所有的官方Formatter都使用了Swift 5.5的新功能——在泛型上下文中扩展静态成员查找

例如

代码语言:javascript
复制
extension FormatStyle where Self == IntegerFormatStyle<Int> {    public static var number: IntegerFormatStyle<Int> { get }}

我们最好也为自定义的Formatter提供类似的定义

实战

目标

本节中,我们将用新的协议来实现针对UIColor的Formatter,它将实现如下功能:

•转换成String

代码语言:javascript
复制
UIColor.red.formatted()// #FFFFFF

•转换成AttributedString

代码语言:javascript
复制
UIColor.red.formatted(.uiColor.attributed)

image-20210930171252694

•从String转换成UIColor

代码语言:javascript
复制
let color = try! UIColor("#FFFFFFCC")// UIExtendedSRGBColorSpace 1 1 1 0.8

•支持链式配置(前缀、标记符号、是否显示透明度)

代码语言:javascript
复制
Text(color, format: .uiColor.alpah().mark().prefix)

image-20210930171608519

•localized

image-20210930171654956

实现ParseStrategy

将字符串转换成UIColor。

代码语言:javascript
复制
struct UIColorParseStrategy: ParseStrategy {    func parse(_ value: String) throws -> UIColor {        var hexColor = value        if value.hasPrefix("#") {            let start = value.index(value.startIndex, offsetBy: 1)            hexColor = String(value[start...])        }        if hexColor.count == 6 {            hexColor += "FF"        }        if hexColor.count == 8 {            let scanner = Scanner(string: hexColor)            var hexNumber: UInt64 = 0            if scanner.scanHexInt64(&hexNumber) {                return UIColor(red: CGFloat((hexNumber & 0xff000000) >> 24) / 255,                               green: CGFloat((hexNumber & 0x00ff0000) >> 16) / 255,                               blue: CGFloat((hexNumber & 0x0000ff00) >> 8) / 255,                               alpha: CGFloat(hexNumber & 0x000000ff) / 255)            }        }        throw Err.wrongColor    }    enum Err: Error {        case wrongColor    }}

在Demo中,我们并没有实现一个要求非常严格的ParseStrategy。任何长度为6或8的十六进制字符串都将被转换成UIColor。

实现ParseableFromatStyle

代码语言:javascript
复制
struct UIColorFormatStyle: ParseableFormatStyle {    var parseStrategy: UIColorParseStrategy {        UIColorParseStrategy()    }    private var alpha: Alpha = .none    private var prefix: Prefix = .hashtag    private var mark: Mark = .none    private var locale: Locale = .current    enum Prefix: Codable {        case hashtag        case none    }    enum Alpha: Codable {        case show        case none    }    enum Mark: Codable {        case show        case none    }    init(prefix: Prefix = .hashtag, alpha: Alpha = .none, mark: Mark = .none, locale: Locale = .current) {        self.prefix = prefix        self.alpha = alpha        self.mark = mark        self.locale = locale    }    func format(_ value: UIColor) -> String {        let (prefix, red, green, blue, alpha, redMark, greenMark, blueMark, alphaMark) = Self.getField(value, prefix: prefix, alpha: alpha, mark: mark, locale: locale)        return prefix + redMark + red + greenMark + green + blueMark + blue + alphaMark + alpha    }}extension UIColorFormatStyle {    static func getField(_ color: UIColor, prefix: Prefix, alpha: Alpha, mark: Mark, locale: Locale) -> (prefix: String, red: String, green: String, blue: String, alpha: String, redMask: String, greenMark: String, blueMark: String, alphaMark: String) {        var r: CGFloat = 0        var g: CGFloat = 0        var b: CGFloat = 0        var a: CGFloat = 0        color.getRed(&r, green: &g, blue: &b, alpha: &a)        let formatString = "%02X"        let prefix = prefix == .hashtag ? "#" : ""        let red = String(format: formatString, Int(r * 0xff))        let green = String(format: formatString, Int(g * 0xff))        let blue = String(format: formatString, Int(b * 0xff))        let alphaString = alpha == .show ? String(format: formatString, Int(a * 0xff)) : ""        var redMark = ""        var greenMark = ""        var blueMark = ""        var alphaMark = ""        if mark == .show {            redMark = "Red: "            greenMark = "Green: "            blueMark = "Blue: "            alphaMark = alpha == .show ? "Alpha: " : ""        }        return (prefix, red, green, blue, alphaString, redMark, greenMark, blueMark, alphaMark)    }}

在ParseableFromatStyle中,除了实现format方法外,我们为不同的配置声明了属性。

getField方法声明为结构方法,便于之后的Attributed调用

在完成了上述代码后,我们已经可以使用代码在UIColor和String之间进行转换:

代码语言:javascript
复制
let colorString = UIColorFormatStyle().format(UIColor.blue)// #0000FFlet colorString = UIColorFormatStyle(prefix: .none, alpha: .show, mark: .show).format(UIColor.blue)// Red:00 Green:00 Blue:FF Alpha:FFlet color = try! UIColorFormatStyle().parseStrategy.parse("#FF3322")// UIExtendedSRGBColorSpace 1 0.2 0.133333 1

链式配置

代码语言:javascript
复制
extension UIColorFormatStyle {    func prefix(_ value: Prefix = .hashtag) -> Self {        guard prefix != value else { return self }        var result = self        result.prefix = value        return result    }    func alpah(_ value: Alpha = .show) -> Self {        guard alpha != value else { return self }        var result = self        result.alpha = value        return result    }    func mark(_ value: Mark = .show) -> Self {        guard mark != value else { return self }        var result = self        result.mark = value        return result    }    func locale(_ locale: Locale) -> UIColorFormatStyle {        guard self.locale != locale else { return self }        var result = self        result.locale = locale        return result    }}

现在我们获得了链式配置的能力。

代码语言:javascript
复制
let colorString = UIColorFormatStyle().alpah(.show).prefix(.none).format(UIColor.blue)// 0000FFFF

localized支持

由于format的输出类型为String,因此,我们需要在getField中将Mark转换成对应区域的文字。在getField中做如下修改:

代码语言:javascript
复制
        if mark == .show {            redMark = String(localized: "UIColorRedMark", locale: locale)            greenMark = String(localized: "UIColorGreenMark", locale: locale)            blueMark = String(localized: "UIColorBlueMark", locale: locale)            alphaMark = alpha == .show ? String(localized: "UIColorAlphaMark", locale: locale) : ""        }

并在项目中创建Localizable.strings文件,添加对应的文字内容:

代码语言:javascript
复制
// English"UIColorRedMark" = " Red:";"UIColorGreenMark" = " Green:";"UIColorBlueMark" = " Blue:";"UIColorAlphaMark" = " Alpha:";

至此,当系统切换到拥有对应语言包的地区时,Mark将显示对应的内容

代码语言:javascript
复制
# Red:00 Green:00 Blue:FF Alpha:FF# 红:00 绿:00 蓝:FF 透明度:FF

截至本文完成时,String(localized:String,locale:Locale)仍有Bug,无法获取到对应的Locale文字。系统的Formatter也有这个问题。正常的情况下,我们可以使用如下代码,在非中文区域获得中文的mark显示

代码语言:javascript
复制
let colorString = UIColorFormatStyle().mark().locale(Locale(identifier: "zh-cn")).format(UIColor.blue)

AttributedString支持

创建自定义Field,便于使用者修改AttributedString不同区域的Style

代码语言:javascript
复制
enum UIColorAttirbute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {    enum Value: String, Codable {        case red        case green        case blue        case alpha        case prefix        case mark    }    static var name: String = "colorPart"}extension AttributeScopes {    public struct UIColorAttributes: AttributeScope {        let colorPart: UIColorAttirbute    }    var myApp: UIColorAttributes.Type { UIColorAttributes.self }}extension AttributeDynamicLookup {    subscript<T>(dynamicMember keyPath: KeyPath<AttributeScopes.UIColorAttributes, T>) -> T where T: AttributedStringKey { self[T.self] }}

过些日子我会写篇博文介绍AttributedString的用法,以及如何自定义AttributedKey

由于将UIColor格式化成AttributedString是单向的(无需从AttribuedString转换回UIColor),因此Attributed只需遵循FormatStyle协议

代码语言:javascript
复制
extension UIColorFormatStyle {    var attributed: Attributed {        Attributed(prefix: prefix, alpha: alpha,mark: mark,locale: locale)    }      struct Attributed: Codable, Hashable, FormatStyle {        private var alpha: Alpha = .none        private var prefix: Prefix = .hashtag        private var mark: Mark = .none        private var locale: Locale = .current        init(prefix: Prefix = .hashtag, alpha: Alpha = .none, mark: Mark = .none, locale: Locale = .current) {            self.prefix = prefix            self.alpha = alpha            self.mark = mark            self.locale = locale        }        func format(_ value: UIColor) -> AttributedString {            let (prefix, red, green, blue, alpha, redMark, greenMark, blueMark, alphaMark) = UIColorFormatStyle.getField(value, prefix: prefix, alpha: alpha, mark: mark, locale: locale)            let prefixString = AttributedString(localized: "^[\(prefix)](colorPart:'prefix')", including: \.myApp)            let redString = AttributedString(localized: "^[\(red)](colorPart:'red')", including: \.myApp)            let greenString = AttributedString(localized: "^[\(green)](colorPart:'green')", including: \.myApp)            let blueString = AttributedString(localized: "^[\(blue)](colorPart:'blue')", including: \.myApp)            let alphaString = AttributedString(localized: "^[\(alpha)](colorPart:'alpha')", including: \.myApp)            let redMarkString = AttributedString(localized: "^[\(redMark)](colorPart:'mark')", locale: locale, including: \.myApp)            let greenMarkString = AttributedString(localized: "^[\(greenMark)](colorPart:'mark')", locale: locale, including: \.myApp)            let blueMarkString = AttributedString(localized: "^[\(blueMark)](colorPart:'mark')", locale: locale, including: \.myApp)            let alphaMarkString = AttributedString(localized: "^[\(alphaMark)](colorPart:'mark')", locale: locale, including: \.myApp)            let result = prefixString + redMarkString + redString + greenMarkString + greenString + blueMarkString + blueString + alphaMarkString + alphaString            return result        }        func prefix(_ value: Prefix = .hashtag) -> Self {            guard prefix != value else { return self }            var result = self            result.prefix = value            return result        }        func alpah(_ value: Alpha = .show) -> Self {            guard alpha != value else { return self }            var result = self            result.alpha = value            return result        }        func mark(_ value: Mark = .show) -> Self {            guard mark != value else { return self }            var result = self            result.mark = value            return result        }        func locale<T:FormatStyle>(_ locale: Locale) -> T {            guard self.locale != locale else { return self as! T }            var result = self            result.locale = locale            return result as! T        }    }}

统一性支持

为UIColorFormatStyle添加FormatStyle扩展,方便在Xcode中使用

代码语言:javascript
复制
extension FormatStyle where Self == UIColorFormatStyle.Attributed {    static var uiColor: UIColorFormatStyle.Attributed {        UIColorFormatStyle().attributed    }}extension FormatStyle where Self == UIColorFormatStyle {    static var uiColor: UIColorFormatStyle {        UIColorFormatStyle()    }}

为UIColor添加便捷构造方法和formatted方法,保持同官方Formatter一致的使用体验。

代码语言:javascript
复制
extension UIColor {    func formatted<F>(_ format: F) -> F.FormatOutput where F: FormatStyle, F.FormatInput == UIColor, F.FormatOutput == String {        format.format(self)    }    func formatted<F>(_ format: F) -> F.FormatOutput where F: FormatStyle, F.FormatInput == UIColor, F.FormatOutput == AttributedString {        format.format(self)    }    func formatted() -> String {        UIColorFormatStyle().format(self)    }    convenience init<T:ParseStrategy>(_ value: String, strategy: T = UIColorParseStrategy() as! T  ) throws where T.ParseOutput == UIColor {        try self.init(cgColor: strategy.parse(value as! T.ParseInput).cgColor)    }    convenience init(_ value: String) throws  {        try self.init(cgColor: UIColorParseStrategy().parse(value).cgColor)    }}

完成品

uicolorFormatter

可以在Github上下载全部代码。

总结

鉴于官方已经提供了大量种类齐全、功能丰富的Formatter,大多数的开发者可能都不会碰到需要自定义Formatter的场景。不过通过对自定义Formatter协议的了解,可以加强我们对原生Formatter的认识,在代码中更好地使用它们。

希望本文能对你有所帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 肘子的Swift记事本 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • WWDC 2021新Formatter API:新老比较及如何自定义
    • 新旧交替或风格转换
      • 新Formatter API可以做什么
      • 新API比旧API好吗
      • 新旧API比较
      • 风格转换?
      • 结论
    • 如何自定义新的Formatter
      • 新老API在自定义方面的不同
      • 新协议
      • 其他
    • 实战
      • 目标
      • 实现ParseStrategy
      • 实现ParseableFromatStyle
      • 链式配置
      • localized支持
      • AttributedString支持
      • 统一性支持
      • 完成品
    • 总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档