NSTextAttachment 图片不是动态的(light/dark 模式)

NSTextAttachment images are not dynamic (light/dark mode)

我有一个 light/dark 模式的动态图像。如果我将此图像放在 UIImageView 中,动态会起作用:当用户从浅色模式切换到深色模式并返回时,图像会更改显示的自身版本。但是,如果我将与 NSTextAttachment 相同的图像放在 NSAttributedString 中,并在标签中显示该字符串,则动态不起作用:当用户从亮模式切换到暗模式时,图像不会改变。

要查看实际问题,请将此代码粘贴到您的 viewDidLoad:

    let size = CGSize(width: 20, height: 20)
    let renderer = UIGraphicsImageRenderer(size: size)
    let image1 = renderer.image {
        UIColor.red.setFill()
        [=11=].fill(.init(origin: .zero, size: size))
    }
    let image2 = renderer.image {
        UIColor.green.setFill()
        [=11=].fill(.init(origin: .zero, size: size))
    }
    let asset = UIImageAsset()
    asset.register(image1, with: .init(userInterfaceStyle: .light))
    asset.register(image2, with: .init(userInterfaceStyle: .dark))

    let iv = UIImageView(image: image1)
    iv.frame.origin = .init(x: 100, y: 100)
    self.view.addSubview(iv)

    let text = NSMutableAttributedString(string: "Howdy ", attributes: [
        .foregroundColor: UIColor(dynamicProvider: { traits in
            switch traits.userInterfaceStyle {
            case .light: return .red
            case .dark: return .green
            default: return .red
            }
        })
    ])
    let attachment = NSTextAttachment(image: image1)
    let attachmentCharacter = NSAttributedString(attachment: attachment)
    text.append(attachmentCharacter)

    let label = UILabel()
    label.attributedText = text
    label.sizeToFit()
    label.frame.origin = .init(x: 100, y: 150)
    self.view.addSubview(label)

我特意使文本字体颜色动态化,以便您可以看到通常颜色动态 在属性字符串中起作用。但不在属性字符串的文本附件中!

所以:这真的是 iOS 的行为方式,还是我在配置文本附件的方式上犯了一些错误?如果这就是 iOS 的行为方式,您是如何解决这个问题的?

[请注意,我不能使用 iOS 15 附件视图提供程序,因为我必须与 iOS 13 和 14 兼容。所以也许这可以解决问题,但解决方案是不对我开放。]

不幸的是,我认为这是正常行为,但我认为它是 Apple 遗忘的功能 non-dev。

我目前得到的唯一方法是监听 func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) 以检测模式变化。

然后,您可以重建 NSAttributedString,或者枚举它并在需要时更新它。

使用枚举,即仅更新需要的内容,而不是重新生成整个 NSAttributedString:

在您最初创建的附件中:

let attachment = NSTextAttachment(image: asset.image(with: traitCollection))
let attachmentCharacter = NSAttributedString(attachment: attachment)

旁注: 我使用 asset.image(with: traitCollection) 而不是 image1,否则当从暗模式开始时,您的图像将改为亮模式。所以这应该设置正确的图像。

然后,我会更新它:

func switchAttachment(for attr: NSAttributedString?) -> NSAttributedString? {
    guard let attr = attr else { return nil }
    let mutable = NSMutableAttributedString(attributedString: attr)
    mutable.enumerateAttribute(.attachment, in: NSRange(location: 0, length: mutable.length), options: []) { attachment, range, stop in
        guard let attachment = attachment as? NSTextAttachment else { return }
        guard let asset = attachment.image?.imageAsset else { return }
        attachment.image = asset.image(with: .current)
        mutable.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment))
    }
    return mutable
}

更新时间:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)        
    label.attributedText = switchAttachment(for: label.attributedText)
}

仅作记录,这是我对 Larme 的建议的重写。我将其作为 UILabel 的扩展:

extension UILabel {
    func updateAttachments() {
        guard let attributedString = attributedText else { return }
        let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
        attributedString.enumerateAttribute(.attachment, in: .init(location: 0, length: attributedString.string.utf16.count), options: []) { value, range, stop in
            guard let attachment = value as? NSTextAttachment else { return }
            guard let image = attachment.image else { return }
            guard let asset = image.imageAsset else { return }
            attachment.image = asset.image(with: .current)
            mutableAttributedString.setAttributes([.attachment: attachment], range: range)
        }
        attributedText = mutableAttributedString
    }
}

现在,假设我有对麻烦的 UILabel 的引用:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    guard let previousTraitCollection = previousTraitCollection else { return }
    if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
        label.updateAttachments()
    }
}

将覆盖注入所有 UILabel 非常好,这样它们就可以自我更新,但不幸的是,扩展不能那样工作,而且我无法将我应用程序中的所有 UILabel 转换为 UILabel 子类.