1

Starting in iOS 15, you can style text in UIKit with an AttributedString. You can treat the attributes as properties of the AttributedString, or you can make an AttributeContainer and treat the attributes as properties of the AttributeContainer and then apply the AttributeContainer to some or all of the AttributedString. Here's a complete example (from the viewDidLoad of a view controller):

// first syntax:
var container = AttributeContainer()
container.uiKit.font = .boldSystemFont(ofSize: 30)
container.uiKit.foregroundColor = .blue
let attributedString = AttributedString("Howdy", attributes: container)
let label = UILabel()
label.attributedText = NSAttributedString(attributedString)
label.sizeToFit()
label.frame.origin = .init(x: 100, y: 100)
self.view.addSubview(label)

So far, so good. But there's also an ability to chain the attributes of an AttributeContainer by calling them as functions, like this:

// second syntax
let container = AttributeContainer()
    .font(.boldSystemFont(ofSize: 30))
    .foregroundColor(.blue)
let attributedString = AttributedString("Howdy", attributes: container)
let label = UILabel()
label.attributedText = NSAttributedString(attributedString)
label.sizeToFit()
label.frame.origin = .init(x: 100, y: 100)
self.view.addSubview(label)

But here's the problem. You notice how in the first example I said .uiKit to disambiguate the properties? I can't do that in the second example. And that means that in some situations, my styled text is not styled properly. This should be a sufficient example to reproduce; this is the entire view controller:

import UIKit
import SwiftUI

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let container = AttributeContainer()
            .font(.boldSystemFont(ofSize: 30))
            .foregroundColor(.blue)
        let attributedString = AttributedString("Howdy", attributes: container)
        let label = UILabel()
        label.attributedText = NSAttributedString(attributedString)
        label.sizeToFit()
        label.frame.origin = .init(x: 100, y: 100)
        self.view.addSubview(label)
    }
}

The label text is not blue. It seems that the mere act of importing SwiftUI causes the attributes to break. And you can readily see why this is, by printing out label.attributedText; you'll see this:

"SwiftUI.ForegroundColor" = blue;

You see what's happened? We've defaulted to a SwiftUI color, which doesn't work in a UIKit context.

So at last I can enunciate my question! Is there a way to use the second syntax, with chaining, while also disambiguating the attributes to be UIKit attributes as I did in the first syntax?

matt
  • 515,959
  • 87
  • 875
  • 1,141

1 Answers1

0

The issue you're encountering is related to the interaction between UIKit and SwiftUI's color systems. In SwiftUI, the Color.blue corresponds to the default system blue color, while in UIKit, the .blue color refers to a specific shade of blue.

To ensure consistent behavior across UIKit and SwiftUI, you can manually specify the exact color using the RGB values in your UIKit code. See below:

import UIKit
import SwiftUI

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let container = AttributeContainer()
            .font(.boldSystemFont(ofSize: 30))
            .foregroundColor(UIColor(red: 0, green: 0, blue: 1, alpha: 1)) // <--- here!
        let attributedString = AttributedString("Howdy", attributes: container)
        let label = UILabel()
        label.attributedText = NSAttributedString(attributedString)
        label.sizeToFit()
        label.frame.origin = .init(x: 100, y: 100)
        self.view.addSubview(label)
    }
}
nickreps
  • 903
  • 8
  • 20
  • Wouldn't the simpler answer be to change `.blue` to `UIColor.blue`? – HangarRash Jun 30 '23 at 23:34
  • @HangarRash Yes, the solution is to disambiguate from the inside, as it were. But this answer was sufficient to make me realize that. It was the conceptual jog that I needed. – matt Jul 01 '23 at 01:38
  • Still I regard this issue as kind of a bug. This is a UILabel, not a Text. You'd think the compiler would grasp the implications. – matt Jul 01 '23 at 01:41
  • @matt Tricky since at the time the AttributeContainer is created, there's no knowledge that it will be used with a UIKit view versus a SwiftUI view. It's just a generic attribute container at creation. Asking the compiler to look ahead and see that it will first be wrapped in an NSAttributedString which is then used with a UIKit label is asking a lot I think. And what of the cases where all of that code isn't in the same method? – HangarRash Jul 01 '23 at 01:56
  • Well that's exactly why I was hoping for a way to specify the "scope". – matt Jul 01 '23 at 03:17