SwiftUI: Missing intrinsic content size — how to get it?
Finding a way to read intrinsic content size in SwiftUI using
If you’re developing for iOS, even if started recently, you have to know UIKit well. But SwiftUI is there, and you probably know it is the future. So it is normal, that when you’re trying to learn SwiftUI, you intuitively search for concepts you know. But some of them are missing or solved differently. One of them is intrinsic content size.
What is intrinsic content size?
It’s a view’s preferred size. It’s how big the view would like to be. Not every view has its intrinsic size, though.
UILabel would like to be as big as the text it contains. But
UITableView? It’s not having its size, and in most cases, those are just as big as free space on a screen.
When developing for UIKit,
intrinsicContentSize is just a property on every view, ready to be accessed if needed. When not determined, it’s returning a value of
UIView.noIntrinsicMetric (separately for each dimension).
SwiftUI system layout
Before explaining more about the goal, let’s briefly check, how SwiftUI calculates its layout. Generally, you can divide SwiftUI views into two categories: those whose size is known, like
Stacks, and those, that are designed to fill space, like
If you’re using
Text, its size will be just enough to display its content. You can always change it, by for example adding
frame modifier. That sounds pretty much like intrinsic content size, right? Well, that’s true. So what is the difference? Only that you can’t read this preferred size value when needed, even though you know it’s there!
If you played a bit with SwiftUI, you now may think: hello, there is
GeometryReader, which is designed to do exactly that! Well, not really. To understand the problem better, let’s jump into an example (finally!).
ScrollView with GeometryReader inside
So now, let’s create some
ScrollView, and let’s put
Text inside, just like that:
Now we would like to add second
Text that would display the size of the first
Text. How to approach that? No cheating: no UIKit, no
String extensions allowed, pure SwiftUI solution, please. I thought it’s easy: let’s just put the first
GeometryReader, store read value to property and display it in the second
Text. There is nothing that could go wrong, right?
Ok, wait. What just happened? This is for sure not looking as you would expect (at least not what I expected when trying it for the first time). The thing is, what
GeometryReader does: it’s reading and then providing size proposed by a parent to its child. You have a full-screen view and you are passing value inside
Stack, to make items equal size? That perfectly what
GeometryReader is designed for. But here, we’re inside
ScrollView. While horizontal space is constrained by the device screen width, the height is theoretically unlimited. What’s more,
GeometryReader return its height as
9.899414. Where this value came from? To be honest, I have no idea, and I’m not sure if I want to know.
Intrinsic content size, where are you?
So we can remove
GeometryReader, it won’t help us — at least not in a place it is right now. What we need is the intrinsic content size of the first
Text. We would like to have something like in code below:
Let’s take a look at a
PreferenceKey for the rescue
Not going into detail, Preference System is SwiftUI mechanism that allows reporting values upwards view hierarchy. To be able to use it, we first need to create
PreferenceKey for value, we will be determining.
Having above we can now create
View modifier that we need.
So what does the above code do? First, let’s focus on a new modifier’s function body. We’re using
background modifier, with
GeometryReaderer inside. Why? The
background will be the same size as view, so that size is not ambiguous at this point — and therefore
GeometryReader can provide both width and height.
And since we do not want to make any visual changes,
GeometryReader content is just clear
Color. We also use
Color to send preference, with the size provided by
GeometryReader. That’s, where size is pushed upwards hierarchy. It’s caught immediately, in
onPeferenceChanged modifier. Here we can read a passed value, and store it to binder passed as an argument.
Now, the only thing left is to use our new modifier to see, if it works. Whole code is already in place, but let’s see on our ScrollView again, and — of course — final result.
Looks, like we have it 💪
Getting intrinsic content size in SwiftUI wasn’t that easy as you may expect, but at the end of a journey, the goal is achieved, and the solution seems to be quite easy to use. To be honest, I’m still a bit surprised that modifier we just created, is not there in SwiftUI library.