SwiftUI: Missing intrinsic content size — how to get it?

Finding a way to read intrinsic content size in SwiftUI using PreferenceKey

Tomasz Załoga

--

Photo by Eran Menashri on Unsplash

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. Text/UILabel would like to be as big as the text it contains. But List/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 Text, Button or Stacks, and those, that are designed to fill space, like Spacer.

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!

And GeometryReader?

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:

Starting easy — ScrollView with Text

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 Text inside GeometryReader, store read value to property and display it in the second Text. There is nothing that could go wrong, right?

ScrollView with two Texts, and with backgrounds to see frames clearly
Read text size with GeometryReader? Nope.

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:

Intrinsic content size to get textSize

Let’s take a look at a PreferenceKey.

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.

Simple PreferenceKey

Having above we can now create View modifier that we need.

Read intrisicContentSize using PreferenceKey

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 thereforeGeometryReader 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.

Final solution

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.

Same code again, will it work after adding missing modifier implementation?
IntrinsicContentSize in action

Looks, like we have it 💪

Summary

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.

--

--

Tomasz Załoga

I'm an iOS developer with 7 years of experience, recently happily learning SwiftUI and Vapor. Enjoyed being part of the team developing mobile apps for Toyota.