My SwiftUI Pain: Creating Tappable Links in Text
My solution and how you can do it too
Update as of WWDC 21: You can now create Text from a new AttributedString struct that is available on all platforms. Check out Zheng’s great article on using them.

So you think SwiftUI is cool, and you want to write your new app in it? And you know what? SwiftUI is cool. It can do all kinds of fancy things that’d be a pain in UIKit or AppKit. It’s all fun and good until you hit one of the many sharp corners laying around everywhere. Here’s the sharp corner I ran into recently:
Links in text. You know, like the ones I gratuitously added in the paragraph above. Links are ubiquitous on the web and in lots of apps too. They’re certainly critical to my current project. So let’s say you want to add some text to your app, and that text has some links within it. How do you go about doing that in SwiftUI?
Well, luckily Text
and Link
exist — though Link
was introduced in 2020 with iOS 14/mac OS 11, etc., so it might not have the compatibility you’re looking for. Anyway, let’s take some text and break it down into these views.
Simple enough. Let’s write a View
to display this text using Text
and Link
.
Cool, let’s see how that looks.
![Text reading “Hello world, my name is” [new line] “Emma” in blue [new line] “. Sometimes I write Swift code”](https://miro.medium.com/v2/resize:fit:637/1*Ox3RnbBFGUBHO4THBa9bzQ.png)
Not quite what we want. I’d love for this text to flow naturally, as if it were one text block. Let’s try the naive thing and place it in an HStack.
And the result:

Terrible. My text is too wide to layout on one line in an HStack, so SwiftUI tries to split the space into thirds, vertically, to try and fit my text. It doesn’t really work out.
Emma, you say, “Text views support using +
to concatenate multiple Text
views together!” They sure do — let’s try that.

Unfortunately, you can’t use +
to concatenate a Link
view to a Text
view. But maybe we’re onto something. Link
, in the way we’re using it, is really just a fancy Text
view, right? Let’s try converting it into a Text
view and adding an onTapGesture
to open the URL ourselves.

Once again, we’re thwarted. onTapGesture
is only defined on View
, so it outputs some View
as its return type, not Text
. So we can’t concatenate Text
views when that View
modifier is applied.
These are really the solutions I’d expect to work in SwiftUI, and from here I truly believe all of the reasonable options are exhausted. You could experiment around with other container types, like grids or whatever, but all of them have the same basic flaw: They aren’t designed for displaying text.
So what do we do now? Well, I think any sane person would look at UITextView
or NSTextView
and think, “Hey, these are robust, tested solutions to this problem” and just use a ViewRepresentable
to drop down into UIKit and AppKit. That’s the solution of the sane and rational. I’m not that.
I want a pure SwiftUI solution to this problem, and, amazingly, there is one. But you’re not going to like it. It’s ugly, error-prone, and incredibly slow. And that’s pretty much my experience whenever I drop into unknown SwiftUI waters. So let’s take a look.
My first step to tackling this problem is breaking down the content a bit. Instead of having long Text
views containing sentences or paragraphs, I want to have each Text
view be just a word. This will allow us to have a lot of control over how much text we lay out at a time. So I created a View
to break the Text
down into single words:
In ContentText
, a given string is broken into words by spaces (you may want to use a whitespace separator here, but for my content, spaces work just fine). Then the ForEach
view is used to generate a Text view for each word.
Importantly, ContentText
has a count property that allows other View
s to know how many Text
views are going to be output by the ForEach
. This is important because when applying View
modifiers to a ForEach
, the View
modifier is called for each View
within the ForEach
. That’ll be important later.
Next, I want this solution to be fairly general, so I defined an enum
that can either be text
or link
, with the appropriate View
as associated data. Then I grabbed some new text to work with from Apple’s developer site:
Alrighty, now we can jump into laying out our Text
and Link
views. We’ll need a View
that can calculate its own height, a GeometryReader
, and a ZStack
.
Within the body of ContentView
, the height of the ZStack
(which will contain all of our content) is calculated and output to the VStack
, which will properly set its frame to the height of the ZStack
.
Since ContentView
can lay out any text, the height is variable, so we need to calculate it to keep things around our content laying out correctly. If you’re curious about how this height calculation works Swift with Majid has a great explainer.
Now we need to actually lay out the views in our ZStack
. That code looks like this:
Whoa! Let’s break it down. First, we add the zStackViews(_:)
method call to our ZStack
and define zStackViews(_:)
. Within zStackViews
, we keep track of the current horizontal and vertical positions on line 19. Then, we return a ForEach
view, with the content generated by the forEachView(_:)
function. forEachView
generates the layout for a single ContentText
or Link
view.
Here within forEachView
, we determine how many View
s need to be rendered by us.
This is where the count
property on ContentText
comes in handy. Since ContentText
’s body
is actually a ForEach
, we’ll need to render multiple views for every ContentText
— that’s line 7.
Links are just one view, so we set numberOfViewsInContent
to 1
on line 10. We also extract the actual View
from our Content
in lines 8 and 11, and set our currently rendered number of View
s to 0
.
Finally, forEachView
returns our View
with some alignmentGuide
view modifiers. Alignment guides are used within the stack family of View
s to custom align items within the stack. Here we’re using a ZStack
with topLeading
alignment so we have the freedom to adjust both the top and leading alignments manually.
The computeValue
function of the alignment guide is returning an offset in points from the top or leading edge.
On line 29, we start setting the leading (horizontal) alignment. If the View
we’re currently iterating over will fit on the current line, line 43 decreases the current horizontal
offset.
If the View
doesn’t fit on the current line (meaning the current offset + the width of the view is larger than the width of our ZStack
, as in line 32), we return 0
as our horizontal offset (the start of a new line). Set the horizontal offset to the width of the current View
, and decrease the vertical offset by the height of the View
, as in lines 35-37.
That’s actually all of the offset modification we need for regular use. The alignment guide on line 47 only modifies the horizontal or vertical offset if we’ve reached the last of our View
s. Since View
s can be drawn multiple times, we need to clean up our offsets and prepare for another draw.
To put it all together, here’s the final code:
This solution is pure SwiftUI and supports dynamic type, as you’d expect.

Yes, there’s lots of hacking to get to this solution, and manually modifying text tends to be error-prone. You’ll also have some trouble applying view modifiers to this text. It’s possible, but not in the usual manner.
I might do another write-up that includes view modifiers, but for now, this is the only way I know of to combine Text
and Link
s together into a single paragraph. Using this method, you can actually add any View
you want into the paragraph. I use this technique to include images, buttons, and all kinds of other types of text you might want to include in a paragraph.
Once again, I really think UIKit or AppKit would better suit just about any project when encountering this problem, at least for now. But if you’re like me and live that SwiftUI life, here you go!