Subclassing Formatter to accept Swift Structs in Swift 3.2 and later

180 Views Asked by At

I have a need to format in several places the same piece of data to a human readable format. However, I have several frameworks that represent this information in different ways. To avoid duplication of this code, I have a Formatter for formatting a common representation of this data. Any frameworks that want to use this human readable representation can have their data declare conformance to a Protocol (usually in an extension) that converts their own representation to the common Struct used by the formatter.

This worked as I'd expect in Swift 3. However, in Swift 3.2 and higher, the cast to the Protocol inside string(for obj:) fails. I don't know if this is a deliberate change, or a bug. In this example:

import Foundation

struct Widget {
    let frobberCount: Int
    let tweakerCount: Int
}

protocol WidgetRepresentable {
    var widgetRepresentation: Widget { get }
}

extension Widget: WidgetRepresentable {
    var widgetRepresentation: Widget {
        return self
    }
}

class WidgetFormatter: Formatter {
    override func string(for obj: Any?) -> String? {
        guard let widget = obj as? WidgetRepresentable else { // This cast fails
            return nil
        }

        return string(from: widget)
    }

    func string(from widgetRepresentable: WidgetRepresentable) -> String {
        let widget = widgetRepresentable.widgetRepresentation
        return "\(widget.frobberCount) frobber(s), \(widget.tweakerCount) tweaker(s)"
    }
}

let formatter = WidgetFormatter()
let widget = Widget(frobberCount: 2, tweakerCount: 4)

formatter.string(for: widget) // Returns nil. Expected 2 frobber(s), 4 tweaker(s)
formatter.string(from: widget) // Correctly returns 2 frobber(s), 4 tweaker(s)

As a workaround, the issue goes away if I do any of the following:

  • Use the convenience string(from:) function, instead of string(for:) overridden from Formatter.
  • Have WidgetFormatter no longer subclass Formatter (removing the override annotation from the string(for:) function)
  • Have Widget be a class, along with whatever is conforming to WidgetRepresentable.

Those last 2 points are making me wonder whether this is an issue with Objective-C bridging between NSFoundation and Foundation in Swift.

Is there an issue with my methodology here, or is this a bug in Swift? If it's the latter, I'll file a radar.

0

There are 0 best solutions below