0

I'm building a UILabel based SwiftUI view using UIViewRepresentable on an app that has a minimum deployments target set to iOS 14.

The code of the View is:

struct MyLabel: View {
    @State private var size: CGSize? = SizeKey.defaultValue
    @State private var holder = LabelHolder()

    private var attributedText: NSAttributedString

    public init(_ attributedText: NSAttributedString) {
        self.attributedText = attributedText
    }

    public var body: some View {
        Color.clear
            .background(
                GeometryReader { proxy in
                    InternalLabel(attributedText: attributedText, holder: holder)
                        .preference(key: SizeKey.self, value: proxy.size)
                }
            )
            .onPreferenceChange(SizeKey.self) {
                self.size = $0
            }
            .configureConditionalFrame(size, holder: holder)
    }

    private struct SizeKey: PreferenceKey {
        static var defaultValue: CGSize? = nil
        static func reduce(value: inout CGSize?, nextValue: () -> CGSize?)
        {
            value = nextValue()
        }
    }
}

fileprivate extension View {
    func configureConditionalFrame(_ size: CGSize?, holder: LabelHolder) -> some View {
        guard let size else {
            return self.frame(width: nil, height: nil)
        }
        let idealSize = holder.idealSize(in: size)
        return self.frame(width: idealSize.width, height: idealSize.height)
    }
}

struct InternalLabel: UIViewRepresentable {
    private let text: Encore.AttributedString
    private let holder: UILabel

    init(attributedText: Encore.AttributedString,
         holder: LabelHolder) {
        self.attributedText = attributedText
        self.holder = holder
    }

    func makeUIView(context: Context) -> UILabel {
        let label = holder.label
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        label.backgroundColor = UIColor.red.withAlphaComponent(0.5)
        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        label.setContentHuggingPriority(.defaultLow, for: .vertical)
        updateUIView(label, context: context)
        return label
    }

    func updateUIView(_ label: UILabel, context: Context) {
        label.attributedText = attributedText
    }
}

final class LabelHolder {
    let label = LabelType()

    func idealSize(in size: CGSize) -> CGSize {
        let idealSize = label.systemLayoutSizeFitting(
            size,
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        ).snapToPixel(snapType: .ceil)

        print("ideal size: \(idealSize) for size: \(size)")

        return idealSize
    }
}

This code uses the GeometryReader in background() to read the size that the parent proposed to MyLabel. I use a Color.clear because is the only kind of View that I found actually sends the proposed size to the GeometryReader.

I set the size as a preference key. When that updates it fires a new layout pass and the UILabel in the LabelHolder can calculate its fitting size.

I'm using frame(width:height:) to constrain the Color.clear to use the label's fitting size. All of this works.

The problem I have and I can't understand how to solve is that whenever the Parent size changes it doesn't bother to proposed a new size to MyLabel as now there is a defined .frame() so the MyLabel view keeps is first size forever.

Am I approaching this from a wrong angle? Is there anything I'm missing?

Luca Bartoletti
  • 2,435
  • 1
  • 24
  • 46

0 Answers0