【问题标题】:NSTextAttachment images are not dynamic (light/dark mode)NSTextAttachment 图像不是动态的(亮/暗模式)
【发布时间】:2022-02-08 00:22:07
【问题描述】:

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

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

    let size = CGSize(width: 20, height: 20)
    let renderer = UIGraphicsImageRenderer(size: size)
    let image1 = renderer.image {
        UIColor.red.setFill()
        $0.fill(.init(origin: .zero, size: size))
    }
    let image2 = renderer.image {
        UIColor.green.setFill()
        $0.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 兼容。所以也许这可以解决问题,但该解决方案对我不开放。]

【问题讨论】:

  • 我认为,这是正常行为。恐怕您需要在模式更改时使用func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) 重新计算NSAttributedString,而不是NSTextAttachment(image: image1),而是使用let attachment = NSTextAttachment(image: asset.image(with: traitCollection))
  • 由于NSTextAttachment 持有UIImage 并且UIImage 不知道模式,只有asset 知道,不幸的是我发现这是正常行为。
  • @Larme 实际上在重绘属性字符串时不需要参考asset,因为所讨论的特征集合是最新的。我在这里要指出的问题是具有讽刺意味的是,图像 in an image view 处于暗模式,而属性字符串 color 处于暗模式,但是属性字符串中的图像不是实时的。在我看来,这像是一个错误:苹果似乎只是在实施暗模式时错误地忘记了它。
  • 我的意思是,当您执行NSTextAttachment(image: image1) 时,您不会告诉NSTextAttachment image1 是一种模式,而应该是第二种模式?我也认为它是一个被遗忘的 iOS 开发者,我得到的唯一方法就是听 traitCollectionDidChange()

标签: ios nsattributedstring darkmode


【解决方案1】:

不幸的是,我认为这是正常的行为,但我认为这是 Apple 非开发人员遗忘的功能。

我目前唯一的方法是听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)
}

【讨论】:

  • 是的,我认为枚举.attachment 属性的属性字符串并更新其图像的想法是最好的方法。我可能会将此作为 UILabel 的扩展,因为这是我现实生活中所有实例发生的地方。
  • 确实,对 Label 进行扩展可能是个好主意。您还可以收听UIScreen.main.traitCollection 更改(KVO ?),并发布通知以刷新所有需要的标签,并在其上注册您的标签。这只是一个想法,不确定在实践中是否可行......
  • 按照这个想法,如果标签在 Xibs/Storyboard 中,您可以使用 stackoverflow.com/a/45302919/1801544 并将它们注册到通知中,但我不喜欢在扩展中覆盖 awakeFromNib
【解决方案2】:

为了记录,这是我对拉尔姆建议的重写。我在 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 子类。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-07
    • 2023-02-10
    • 1970-01-01
    • 1970-01-01
    • 2020-06-07
    • 2020-11-21
    • 1970-01-01
    • 2021-06-29
    相关资源
    最近更新 更多