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?