Swift 3:设置Finder标签颜色

8

我正在尝试设置Finder显示的彩色标签。我知道的唯一函数是setResourceValue。但这需要本地化的名称!

我能想象到我的母语和英语,但其他所有语言我都不知道。我无法相信这应该是正确的方法。

是否有翻译函数,它采用标准参数(如枚举或整数),并提供本地化的颜色名称?

我有一个运行部分,但只适用于两种语言(德语和英语):

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个回答

9
要设置单一颜色,应该使用setResourceValue API调用。但是,你应该使用的资源键是NSURLLabelNumberKey,或者在Swift 3中使用URLResourceKey.labelNumberKey(而不是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)")
}

(这是一个相关的Objective-C问题的答案的Swift 3版本。在Xcode 8.1和macOS Sierra 10.12.1中测试过)

要设置多个颜色,您可以使用与设置标签键的资源值相同的API。这两种编码之间的区别在这里描述: http://arstechnica.com/apple/2013/10/os-x-10-9/9/ - 基本上,标签键在内部设置扩展属性“com.apple.metadata:_kMDItemUserTags”,该属性以二进制plist形式存储这些标签字符串的数组,而上面显示的单个颜色选项则设置了32字节长的扩展属性值“com.apple.FinderInfo”的第十个字节。

在该键名中,“localized”这个词有点令人困惑,因为实际上设置的是用户选择的标签集合,其中包括用户设置的标签名称。这些标签值确实是本地化的,但只限于在创建帐户时根据本地化设置进行设置的程度。为了说明这一点,以下是我系统上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首选项中读取该键(域“com.apple.Finder”中的键“FavoriteTagNames”,它将编码为这些喜欢的标签名称的数组,如上所示)。
忽略上述复杂性,如果您想要正确获取标签名称和颜色,需要从Finder首选项域中读取(根据您的应用程序是否被沙盒化而定,您可能能够执行此操作),如果您希望使用多种颜色,这里是一个示例解决方案,直接使用扩展属性值设置颜色(我使用SOExtendedAttributes避免触摸笨重的xattr C API)。
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)")
}

1
看起来我确实找到了一个基于扩展属性API的解决方案。这个特定的值不是一个二进制plist,正如在回应你的回答中建议的那样。我会再仔细研究一下,相当确定com.apple.FinderInfo属性值中有两个字节包含了这个选项掩码。 - mz2
我已经得出了与此相同的结论:http://superuser.com/a/295155/157584 - 在这32个字节长的扩展属性值中,第10个字节包含颜色信息。我还不确定我是否正确地读取它。 - mz2
1
我不能说我对我找到的解决方案非常满意,但我确信我的解决方案足够详尽,至少可以描述为什么不可能存在更好的解决方案(因为标签不是针对某个固定集合进行本地化的,这些集合会根据系统本地化而改变,而是在账户创建后根据所使用的本地化进行固定,并且当然也可以被用户操纵)。 - mz2
谢谢!是的,由于向后兼容性和对标签名称更改的鲁棒性,这一切似乎有点混乱。 - mz2
我在第二个代码示例中添加了一条注释,再次指出Finder首选项的问题。 - mz2
显示剩余7条评论

5

由于新的URLResourceValues()结构和标签数字,我成功地让它工作,而无需知道颜色名称。

了解到每个标签数字代表一个标签颜色:

0 无
1 灰色
2 绿色
3 紫色
4 蓝色
5 黄色
6 红色
7 橙色

将您的文件制作成URL:

var url = URL(fileURLWithPath: pathToYourFile)

它必须是一个变量,因为我们将对其进行更改。

创建一个新的URLResourceValues实例(也需要是一个变量):

var rv = URLResourceValues()

按照以下方式设置标签号:

rv.labelNumber = 2 // green

最后,将标签写入文件:

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

在我们的例子中,我们将数字标签设置为2,所以现在这个文件被标记为绿色。

不错!(另外说明:在Swift 3中不再需要as NSError转换。) - Martin R
太好了。但是多种颜色怎么办?我可以将0-7的值分配给.labelNumber,但我不能将它们相加,也不能将它们组合成位。 - Peter Silie
@PeterSilie 在我找到的每个提到URLResourceValues的文档中,都只使用了一个标签颜色/标签编号。我猜测Finder有能够设置多个标签的遗留代码,但现在已经没有公共API可以做同样的事情了。希望有一天会有人看到这个并证明我是错的... - Eric Aya
@EricAya:标签名称和颜色存储在文件的“扩展属性”中,例如http://arstechnica.com/apple/2013/10/os-x-10-9/9/,其中分析了格式,它是一个二进制属性列表。以下是一些Swift方法,用于读取/写入扩展属性:https://dev59.com/cloT5IYBdhLWcg3wpQuD#38343753。颜色名称实际上是以文字形式存储在该bplist中的。 - Martin R
@MartinR 很棒的文章,不幸的是我已经阅读过它,但无法使用这些信息来实现我的目标,即不使用本地化名称 - 可能是因为不可能,也可能是因为我没有理解好,所以我想确保一下,因此设置了悬赏。我会在有时间的时候尝试使用您的扩展,它看起来非常有前途。谢谢! - Eric Aya

2

历史

首先,有我的先前答案,它适用于为文件设置一个颜色标签:https://dev59.com/rpvga4cB1Zd3GeqPxB09#39751001

然后,@mz2发布了这个出色的答案,成功地将多个颜色标签设置到文件中,并解释了该过程:https://dev59.com/rpvga4cB1Zd3GeqPxB09#40314367

现在是这个小附加功能,一个简单的跟进@mz2的答案。

解决方案

我只是实现了@mz2的建议:我扩展了他的枚举示例,添加了获取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)")
}

当我尝试实现这个时,我还需要安装 'setExtendedAttributeData' 作为 https://github.com/billgarrison/SOExtendedAttributes 的一部分。但这不是一个 Swift 模块,而我刚开始学习这门语言。我该怎么做才能使用这个 C 模块?是否有仅限于 Swift 的解决方案? - Peter Silie
在Xcode项目中集成SOExtendedAttributes非常容易(而且您不必担心它不是Swift):下载zip文件,然后将“NSURL+SOExtendedAttributes.h”和“NSURL+SOExtendedAttributes.m”拖放到项目导航器中,与您自己的文件并列。在出现的面板中,应该勾选“复制”框。然后,Xcode会询问您是否要创建桥接头文件。选择YES。然后在此头文件中添加以下行:#import "NSURL+SOExtendedAttributes.h":完成了,现在您可以使用setExtendedAttributeData。 :) - Eric Aya


1
截至2023年11月,检索多个彩色标签的最佳方法是这样的(使用swift-xattr package):
    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 []
        }
    }

所以我卡在这个函数调用extendedAttributeValue上。这是一个第三方扩展吗?我在其他评论中提到的Obj-C库(NSURL+SOExtendedAttributes)中找到了一个类似的方法valueOfExtendedAttribute,我尝试将其返回值(id / Any)强制转换为Data(我认为你的方法返回的是这个类型),但是没有成功。 - undefined
更新:经过一番探索,发现valueOfExtendedAttribute返回的是一个NSArray。现在我在我的URL扩展中有了这样的东西: guard let tagsBinaryPropertyList = (try (self as NSURL).valueOfExtendedAttribute(withName: tagsXattrName)) as? NSArray else { return nil } let tags: [String] = tagsBinaryPropertyList.compactMap { $0 as? String } - undefined
我正在使用XAttr Swift包,我会编辑我的答案来添加这个,谢谢。extendeAttributesValue返回的是二进制格式的plist。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接