Как отображать значения OptionSet в удобочитаемой форме?

Swift имеет тип OptionSet, который в основном добавляет операции набора к битам бит C-Style. Apple использует их довольно широко в своих рамках. Примеры включают параметр параметров в animate(withDuration:delay:options:animations:completion:).

На стороне плюса он позволяет использовать чистый код, например:

options: [.allowAnimatedContent, .curveEaseIn]

Однако есть и недостатки.

Если я хочу отобразить указанные значения OptionSet, похоже, нет чистого способа сделать это:

let options: UIViewAnimationOptions = [.allowAnimatedContent, .curveEaseIn]
print("options = " + String(describing: options))

Отображает очень бесполезное сообщение:

options = UIViewAnimationOptions (rawValue: 65664)

Документы для некоторых из этих битовых полей выражают константу как значение силы двух:

flag0    = Flags(rawValue: 1 << 0)

Но документы для моего примера OptionSet, UIViewAnimationOptions, ничего вам не говорят о числовом значении этих флагов и вычислении битов из десятичных чисел не просто.

Вопрос:

Есть ли какой-то чистый способ сопоставления OptionSet с выбранными значениями?

Мой желаемый результат будет примерно таким:

options = UIViewAnimationOptions ([. allowAnimatedContent,.curveEaseIn])

Но я не могу придумать способ сделать это без кучи беспорядочного кода, который потребовал бы, чтобы я поддерживал таблицу отображаемых имен для каждого флага.

(Я заинтересован в том, чтобы делать это как для системных фреймворков, так и для специальных наборов параметров, которые я создаю в своем собственном коде.)

Перечисления позволяют вам иметь как имя, так и необработанное значение для перечисления, но они не поддерживают функции набора, которые вы получаете с помощью OptionSets.

Ответ 1

Эта статья в NSHipster дает альтернативу OptionSet, которая предлагает все функции OptionSet, а также простую регистрацию:

https://nshipster.com/optionset/

Если вы просто добавите требование, чтобы тип Option был CustomStringConvertible, вы можете очень аккуратно регистрировать наборы этого типа. Ниже приведен код с сайта NSHipster - единственное изменение заключается в добавлении соответствия CustomStringConvertible к классу Option

protocol Option: RawRepresentable, Hashable, CaseIterable, CustomStringConvertible {}

enum Topping: String, Option {
    case pepperoni, onions, bacon,
    extraCheese, greenPeppers, pineapple

    //I added this computed property to make the class conform to CustomStringConvertible
    var description: String {
        return ".\(self.rawValue)"
    }
}

extension Set where Element == Topping {
    static var meatLovers: Set<Topping> {
        return [.pepperoni, .bacon]
    }

    static var hawaiian: Set<Topping> {
        return [.pineapple, .bacon]
    }

    static var all: Set<Topping> {
        return Set(Element.allCases)
    }
}

typealias Toppings = Set<Topping>

extension Set where Element: Option {
    var rawValue: Int {
        var rawValue = 0
        for (index, element) in Element.allCases.enumerated() {
            if self.contains(element) {
                rawValue |= (1 << index)
            }
        }
        return rawValue
    }
}

Тогда используя это:

let toppings: Set<Topping> = [.onions, .bacon]

print("toppings = \(toppings), rawValue = \(toppings.rawValue)")

Что выводит

toppings = [.onions,.bacon], rawValue = 6

Так же, как вы этого хотите.

Это работает, потому что набор отображает своих членов в виде списка, разделенного запятыми, в квадратных скобках и использует свойство description каждого члена набора для отображения этого члена. Свойство description просто отображает каждый элемент (имя перечисления в виде String) с . префикс

А поскольку rawValue для Set<Option> совпадает с OptionSet с тем же списком значений, вы можете легко конвертировать между ними.

Я хотел бы, чтобы Swift просто сделал эту функцию родным языком для OptionSet s.

Ответ 2

Вот один из подходов, который я выбрал, используя словарь и перебирая ключи. Не отлично, но это работает.

struct MyOptionSet: OptionSet, Hashable, CustomStringConvertible {

    let rawValue: Int
    static let zero = MyOptionSet(rawValue: 1 << 0)
    static let one = MyOptionSet(rawValue: 1 << 1)
    static let two = MyOptionSet(rawValue: 1 << 2)
    static let three = MyOptionSet(rawValue: 1 << 3)

    var hashValue: Int {
        return self.rawValue
    }

    static var debugDescriptions: [MyOptionSet:String] = {
        var descriptions = [MyOptionSet:String]()
        descriptions[.zero] = "zero"
        descriptions[.one] = "one"
        descriptions[.two] = "two"
        descriptions[.three] = "three"
        return descriptions
    }()

    public var description: String {
        var result = [String]()
        for key in MyOptionSet.debugDescriptions.keys {
            guard self.contains(key),
                let description = MyOptionSet.debugDescriptions[key]
                else { continue }
            result.append(description)
        }
        return "MyOptionSet(rawValue: \(self.rawValue)) \(result)"
    }

}

let myOptionSet = MyOptionSet([.zero, .one, .two])

// prints MyOptionSet(rawValue: 7) ["two", "one", "zero"]

Ответ 3

Протокол StrOptionSet:

  • Добавьте свойство набора меток, чтобы проверить каждое значение метки в Self.

Расширение StrOptionSet:

  • Отфильтруйте, который не пересекается.
  • Вернуть текст метки в виде массива.
  • Присоединяется к "," как CustomStringConvertible :: description

Вот фрагмент кода:

protocol StrOptionSet : OptionSet, CustomStringConvertible {
    typealias Label = (Self, String)
    static var labels: [Label] { get }
}
extension StrOptionSet {
    var strs: [String] { return Self.labels
                                .filter{ (label: Label) in self.intersection(label.0).isEmpty == false }
                                .map{    (label: Label) in label.1 }
    }
    public var description: String { return strs.joined(separator: ",") }
}

Добавить набор меток для целевого набора параметров VTDecodeInfoFlags.

extension VTDecodeInfoFlags : StrOptionSet {
    static var labels: [Label] { return [
        (.asynchronous, "asynchronous"),
        (.frameDropped, "frameDropped"),
        (.imageBufferModifiable, "imageBufferModifiable")
    ]}
}

Используйте его

let flags: VTDecodeInfoFlags = [.asynchronous, .frameDropped]
print("flags: ", flags)