Swift 3: установить цвет метки Finder

Я пытаюсь установить цветные метки, отображаемые искателем. Единственная функция, которую я знаю, это setResourceValue. Но для этого нужны локализованные имена!

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

Является ли функция перевода, которая принимает стандартный параметр типа enum или int и выдает локализованное имя цвета?

У меня есть бегущая часть, но только для двух языков (немецкий и английский):

let colorNamesEN = [ "None", "Gray", "Green", "Purple", "Blue", "Yellow", "Red", "Orange" ]
let colorNamesDE = [ "",     "Grau", "Grün",  "Lila",   "Blau", "Gelb",   "Rot", "Orange" ]

public enum TagColors : Int8 {
    case None = -1, Gray, Green, Purple, Blue, Yellow, Red, Orange, Max
}

//let theURL : NSURL = NSURL.fileURLWithPath("/Users/dirk/Documents/MyLOG.txt")

extension NSURL {
    // e.g.  theURL.setColors(0b01010101)
    func tagColorValue(tagcolor : TagColors) -> UInt16 {
        return 1 << UInt16(tagcolor.rawValue)
    }

    func addTagColor(tagcolor : TagColors) -> Bool {
        let bits : UInt16 = tagColorValue(tagcolor) | self.getTagColors()
        return setTagColors(bits)
    }

    func remTagColor(tagcolor : TagColors) -> Bool {
        let bits : UInt16 = ~tagColorValue(tagcolor) & self.getTagColors()
        return setTagColors(bits)
    }

    func setColors(tagcolor : TagColors) -> Bool {
        let bits : UInt16 = tagColorValue(tagcolor)
        return setTagColors(bits)
    }

    func setTagColors(colorMask : UInt16) -> Bool {
        // get string for all available and requested bits
        let arr = colorBitsToStrings(colorMask & (tagColorValue(TagColors.Max)-1))

        do {
            try self.setResourceValue(arr, forKey: NSURLTagNamesKey)
            return true
        }
        catch {
            print("Could not write to file \(self.absoluteURL)")
            return false
        }
    }

    func getTagColors() -> UInt16 {
        return getAllTagColors(self.absoluteURL)
    }
}


// let initialBits: UInt8 = 0b00001111
func colorBitsToStrings(colorMask : UInt16) -> NSArray {
    // translate bits to (localized!) color names
    let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode)!

    // I don't know how to automate it for all languages possible!!!!
    let colorNames = countryCode as! String == "de" ? colorNamesDE : colorNamesEN

    var tagArray = [String]()
    var bitNumber : Int = -1   // ignore first loop
    for colorName in colorNames {
        if bitNumber >= 0 {
            if colorMask & UInt16(1<<bitNumber) > 0 {
                tagArray.append(colorName)
            }
        }
        bitNumber += 1
    }
    return tagArray
}


func getAllTagColors(file : NSURL) -> UInt16 {
    var colorMask : UInt16 = 0

    // translate (localized!) color names to bits
    let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode)!
    // I don't know how to automate it for all languages possible!!!!
    let colorNames = countryCode as! String == "de" ? colorNamesDE : colorNamesEN
    var bitNumber : Int = -1   // ignore first loop

    var tags : AnyObject?

    do {
        try file.getResourceValue(&tags, forKey: NSURLTagNamesKey)
        if tags != nil {
            let tagArray = tags as! [String]

            for colorName in colorNames {
                if bitNumber >= 0 {
                    // color name listed?
                    if tagArray.filter( { $0 == colorName } ).count > 0 {
                        colorMask |= UInt16(1<<bitNumber)
                    }
                }
                bitNumber += 1
            }
        }
    } catch {
        // process the error here
    }

    return colorMask
}

5 ответов

Решение

история

Сначала был мой предыдущий ответ, который работает для установки одной цветовой метки в файл: /questions/12793806/swift-3-ustanovit-tsvet-metki-finder/12793824#12793824.

Затем mz2 опубликовал этот превосходный ответ, который успешно устанавливает несколько цветовых меток для файла и объясняет процесс: /questions/12793806/swift-3-ustanovit-tsvet-metki-finder/12793821#12793821.

А теперь это маленькое дополнение, простое продолжение ответа @mz2.

Решение

Я просто реализовал предложение @mz2: я расширил его пример enum методами для извлечения предпочтений Finder и извлечения правильных локализованных имен цветов меток перед установкой атрибутов в файл.

enum LabelColors: Int {
    case none
    case gray
    case green
    case purple
    case blue
    case yellow
    case red
    case orange

    func label(using list: [String] = []) -> String? {
        if list.isEmpty || list.count < 7 {
            switch self {
            case .none: return nil
            case .gray: return "Gray\n1"
            case .green: return "Green\n2"
            case .purple: return "Purple\n3"
            case .blue: return "Blue\n4"
            case .yellow: return "Yellow\n5"
            case .red: return "Red\n6"
            case .orange: return "Orange\n7"
            }
        } else {
            switch self {
            case .none: return nil
            case .gray: return list[0]
            case .green: return list[1]
            case .purple: return list[2]
            case .blue: return list[3]
            case .yellow: return list[4]
            case .red: return list[5]
            case .orange: return list[6]
            }
        }
    }

    static func set(colors: [LabelColors],
                    to url: URL,
                    using list: [String] = []) throws
    {
        // 'setExtendedAttributeData' is part of https://github.com/billgarrison/SOExtendedAttributes
        try (url as NSURL).setExtendedAttributeData(propertyListData(labels: colors, using: list),
                                                    name: "com.apple.metadata:_kMDItemUserTags")
    }

    static func propertyListData(labels: [LabelColors],
                                 using list: [String] = []) throws -> Data
    {
        let labelStrings = labels.flatMap { $0.label(using: list) }
        return try PropertyListSerialization.data(fromPropertyList: labelStrings,
                                                  format: .binary,
                                                  options: 0)
    }

    static func localizedLabelNames() -> [String] {
        // this doesn't work if the app is Sandboxed:
        // the users would have to point to the file themselves with NSOpenPanel
        let url = URL(fileURLWithPath: "\(NSHomeDirectory())/Library/SyncedPreferences/com.apple.finder.plist")

        let keyPath = "values.FinderTagDict.value.FinderTags"
        if let d = try? Data(contentsOf: url) {
            if let plist = try? PropertyListSerialization.propertyList(from: d,
                                                                       options: [],
                                                                       format: nil),
                let pdict = plist as? NSDictionary,
                let ftags = pdict.value(forKeyPath: keyPath) as? [[AnyHashable: Any]]
            {
                var list = [(Int, String)]()
                // with '.count == 2' we ignore non-system labels
                for item in ftags where item.values.count == 2 {
                    if let name = item["n"] as? String,
                        let number = item["l"] as? Int {
                        list.append((number, name))
                    }
                }
                return list.sorted { $0.0 < $1.0 }.map { "\($0.1)\n\($0.0)" }
            }
        }
        return []
    }
}

Использование:

do {
    // default English label names
    try LabelColors.set(colors: [.yellow, .red],
                        to: fileURL)

    // localized label names
    let list = LabelColors.localizedLabelNames()
    try LabelColors.set(colors: [.green, .blue],
                        to: fileURL,
                        using: list)
} catch {
    print("Error when setting label color(s): \(error)")
}

Чтобы установить один цвет, setResourceValue API-вызов действительно то, что вы должны использовать. Однако ключ ресурса, который вы должны использовать, NSURLLabelNumberKey, или же URLResourceKey.labelNumberKey в Свифте 3 (не NSURLTagNamesKey):

enum LabelNumber: Int {
    case none
    case grey
    case green
    case purple
    case blue
    case yellow
    case red
    case orange
}

do {
    // casting to NSURL here as the equivalent API in the URL value type appears borked:
    // setResourceValue(_, forKey:) is not available there, 
    // and setResourceValues(URLResourceValues) appears broken at least as of Xcode 8.1…
    // fix-it for setResourceValues(URLResourceValues) is saying to use [URLResourceKey: AnyObject], 
    // and the dictionary equivalent also gives an opposite compiler error. Looks like an SDK / compiler bug. 
    try (fileURL as NSURL).setResourceValue(LabelNumber.purple.rawValue, forKey: .labelNumberKey)
}
catch {
    print("Error when setting the label number: \(error)")
}

(Это порт Swift 3 для ответа на связанный вопрос Objective-C. Протестировано с Xcode 8.1, macOS Sierra 10.12.1)

Чтобы установить несколько цветов, вы можете использовать API, который вы использовали для установки значений ресурсов с помощью ключа метки. Различие между этими двумя кодировками описано здесь: http://arstechnica.com/apple/2013/10/os-x-10-9/9/ - в основном ключ метки внутренне устанавливает расширенный атрибут "com.apple.metadata:_kMDItemUserTags", которая хранит массив этих строк меток в виде двоичного списка, в то время как показанная выше опция для одного цвета устанавливает 10-й байт значения расширенного атрибута длиной 32 байта"com.apple.FinderInfo".

"Локализованный" в этом имени ключа немного сбивает с толку в том смысле, что на самом деле он задает набор меток, выбранных пользователем, среди имен меток, установленных пользователем. Эти значения меток действительно локализованы, но только в той степени, в которой они установлены в соответствии с настройками локализации при первоначальном создании учетной записи. Чтобы продемонстрировать, это значения меток, используемые Finder в моей системе, для которых я выбрал финскую локализацию в качестве тестового и перезапущенного Finder, перезагруженного компьютера и т. Д.

➜  defaults read com.apple.Finder FavoriteTagNames
(
    "",
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Purple,
    Gray
)

То, как данные кодируются в этом двоичном значении plist, - это просто имя любимого тега, за которым следует его индекс в массиве (который имеет фиксированную длину 8, а фактические значения начинаются с 1, то есть соответствуют семи цветам в порядке красного цвета)., Оранжевый, желтый, зеленый, синий, фиолетовый, серый). Например:

xattr -p com.apple.metadata:_kMDItemUserTags foobar.png | xxd -r -p | plutil -convert xml1 - -o -
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>Gray
1</string>
    <string>Purple
3</string>
    <string>Green
2</string>
    <string>Red
6</string>
</array>
</plist>

Таким образом, локализация системы не учитывается, и на самом деле установка тега с любой строкой, за которой следует перевод строки, за которым следует число от 1 до 7, будет отображаться в Finder с цветом, указанным индексом тега. Однако, чтобы узнать правильные текущие значения, применяемые для получения тегов, которые будут применены из набора избранных тегов (например, совпадают и цвет, и метка), вам необходимо прочитать этот ключ в настройках Finder (ключ FavoriteTagNames из домен 'com.apple.Finder', который кодирует массив этих имен любимых тегов, как показано выше).

Игнорирование вышеуказанного усложнения в случае, если вы хотите получить правильное имя и цвет метки, требуя чтения из домена настроек Finder (что вы можете или не можете делать, в зависимости от того, находится ли ваше приложение в изолированной программной среде или нет), если вы захотите использовать несколько цветов, вот пример решения, которое устанавливает цвет с использованием расширенных значений атрибутов напрямую (я использовал SOExtendedAttributes, чтобы не касаться громоздких API-интерфейсов C xattr):

enum LabelNumber: Int {
    case none
    case gray
    case green
    case purple
    case blue
    case yellow
    case red
    case orange

    // using an enum here is really for illustrative purposes:
    // to know the correct values to apply you would need to read Finder preferences (see body of my response for more detail).
    var label:String? {
        switch self {
        case .none: return nil
        case .gray: return "Gray\n1"
        case .green: return "Green\n2"
        case .purple: return "Purple\n3"
        case .blue: return "Blue\n4"
        case .yellow: return "Yellow\n5"
        case .red: return "Red\n6"
        case .orange: return "Orange\n7"
        }
    }

    static func propertyListData(labels: [LabelNumber]) throws -> Data {
        let labelStrings = labels.flatMap { $0.label }
        let propData = try! PropertyListSerialization.data(fromPropertyList: labelStrings,
                                                           format: PropertyListSerialization.PropertyListFormat.binary,
                                                           options: 0)
        return propData
    }
}

do {
    try (fileURL as NSURL).setExtendedAttributeData(LabelNumber.propertyListData(labels: [.gray, .green]),
                                                     name: "com.apple.metadata:_kMDItemUserTags")
}
catch {
    print("Error when setting the label number: \(error)")
}

Я получил его работать без необходимости знать название цвета, благодаря новому URLResourceValues() структура и номера тегов.

Зная, что каждый из этих номеров тегов представляет цвет тега:

0 нет
1 серый
2 зеленых
3 Фиолетовый
4 синий
5 желтый
6 красный
7 Апельсин

Сделайте URL вашего файла:

var url = URL(fileURLWithPath: pathToYourFile)

Это должен быть var, потому что мы собираемся изменить его.

Создать новый URLResourceValues экземпляр (также должен быть переменной):

var rv = URLResourceValues()

Установите номер этикетки следующим образом:

rv.labelNumber = 2 // green

Наконец, напишите тег в файл:

do {
    try url.setResourceValues(rv)
} catch {
    print(error.localizedDescription)
}

В нашем примере мы установили номер тега на 2, так что теперь этот файл помечен зеленым цветом.

NSWorkspace имеет функции, которые возвращают все имена и цвета меток:

https://developer.apple.com/documentation/appkit/nsworkspace/1527553-filelabelcolors

https://developer.apple.com/documentation/appkit/nsworkspace/1533953-filelabels

По состоянию на ноябрь 2023 года лучший способ получить теги нескольких цветов выглядит следующим образом (с использованием пакета Swift-xattr ):

          import XAttr

    func getTags(for url: URL) -> [Tag] {
        let colors = NSWorkspace.shared.fileLabelColors
        do {
            let tagsXattrName = "com.apple.metadata:_kMDItemUserTags"
            let tagsBinaryPropertyList = try url.extendedAttributeValue(forName: tagsXattrName)
            let tags:[String] = try PropertyListDecoder().decode([String].self, from: tagsBinaryPropertyList)
            return tags.map {
                // if the tag has a color, the color index is stored as a number after a newline character
                let tagAndMaybeColor = $0.split(separator: "\n")
                if tagAndMaybeColor.count == 1 {
                    return Tag(name: $0, color: nil)
                }
                let colorIndex = Int(tagAndMaybeColor[1])!
                return Tag(name: String(tagAndMaybeColor[0]), color: colors[colorIndex])
            }
        } catch {
            //print("error gettings tags for \(url): \(error)")
            return []
        }
    }
Другие вопросы по тегам