Visually Rich Links Tutorial for iOS: Image Thumbnails

Generate visually rich links from the URL of a webpage. In this tutorial, you’ll transform Open Graph metadata into image thumbnail previews for an iOS app. By Lea Marolt Sonnenschein.

Leave a rating/review
Download materials
Save for later

Visually rich links describe beautiful imagery, typography and even video thumbnails used to preview web page content. They’re a significant upgrade from blue text, which was the only thing available before iOS 13.

Regular vs rich links.

In this tutorial, you’ll use the LinkPresentation framework to make links better looking, more engaging and more user friendly. Check out this example of a video thumbnail preview:

Rich link preview with video.

All a web page has to do is add a couple of special HTML tags to specify the title, icon, image or video and voilà: Your links come alive!

Just imagine how nice it’ll be for your messaging, blog or recipe app to immediately show a preview of the content the user’s about to see. You’d click on the right link much faster than the left, even though they link to the same web page.

Regular vs rich link.

With the addition of the LinkPresentation framework, you can easily and quickly showcase links in your apps. Ready to dive in?

In this tutorial, you’ll learn how to:

  • Create rich links.
  • Handle LinkPresentation errors.
  • Store metadata.
  • Optimize the share sheet.
  • Save favorite links.

Getting Started

In this tutorial, you’ll be working on an app called Raylette. Each time you spin the wheel, Raylette randomly chooses a tutorial and presents it to you using the LinkPresentation framework.

Final app screenshots.

Hopefully, it inspires you to check out a topic you might not have come across otherwise!

Use the Download Materials button at the top or bottom of this tutorial to download the starter project. Open it and build and run. You’ll see you already have a Spin the Wheel button and two tabs: Spin and Saved:

Begin app screenshots

Before you dive into the code, though, there’s a bit of theory to cover. So hold on tight!

Understanding Rich Links

Rich links are link previews you see, for example, when users send messages through the Messages app.

Depending on the information Apple can extract from the web page, a link preview can look one of these four ways:

Various rich link views based on web page tags.

Understanding Rich Links: Web Page Metadata

The web page’s metadata dictates what you’ll see in the preview. Look at the tags in the <head> section. You can do this in Chrome by right-clicking on a web page and choosing View Page Source.

Context menu in browser View Page Source

Here’s an example for the Flutter Hero Animations tutorial from

<title>Flutter Hero Animations |</title>
<meta property="og:title" content="Flutter Hero Animations">
<meta property="og:type" content="video.other">
<meta property="og:image" content="">
<meta property="og:description" content="<p>Learn and understand how to use the Hero widget in Flutter to animate beautiful screen transitions for your apps.</p>
<meta property="og:site_name" content="">
<link rel="icon" type="image/png" href="/favicon.png">

The metadata that powers rich links consists of both Open Graph meta tags and other HTML tags. The LinkPresentation framework extracts all these tags and uses the most appropriate ones.

Understanding Rich Links: Open Graph Protocol

The Open Graph protocol is a standard of web page meta tags for visually rich links in apps like Facebook, Twitter or Messages:

Examples of how rich links are presented on Facebook, Twitter and Messages

Conforming to the protocol is pretty simple. You just need to add some special <meta> tags in the <head> of your web page and you’ll be up and running in no time.

The <meta> tags required by the Open Graph protocol are:

  • og:title: object title
  • og:type: object type, for example music, video, article and many more
  • og:image: the object’s image URL
  • og:url: the canonical URL of the object

You can easily recognize the Open Graph <meta> tags by their og: prefix.

The majority of articles and video courses have code like this. Each web page has the following tags: og:title, og:type, og:image, og:description and og:site_name.

Note: og:site_name specifies that a particular web page is part of a larger website. In our example, it tells us Flutter Hero Animations is part of the larger website.

Check out the full specifications of the Open Graph protocol to learn more about how it works and what types it supports.

Building the Preview

The LinkPresentation framework extracts the metadata from all the web page’s tags and uses the most appropriate ones to display the best preview.

Which tags generate which link previews.

The preview depends on five pieces of information:

Building the Preview: The URL

The URL comes from either:

  • The site’s URL
  • og:site_name like <meta property="og:site_name" content="">

When og:site_name is present, it takes precedence over the URL in the link preview. All Open Graph meta tags take precedence over the other alternatives when they’re present.

Building the Preview: The Title

The title comes from either:

  • <title>Flutter Hero Animations |</title>
  • <meta property="og:title" content="Flutter Hero Animations">

<title> specifies the title of the web page you see in the browser. But sometimes, the <title> tag duplicates the site’s URL, as in this example. To avoid this duplication in your preview, use og:title instead. It will take precedence over the <title> tag.

Apple recommends you:
  1. Keep titles unique and informative.
  2. Avoid duplicating the site name in the title.
  3. Don’t generate tags dynamically, because the LinkPresentation framework doesn’t run JavaScript.

Building the Preview: The Icon

The icon comes from this tag: <link rel="icon" type="image/png" href="/favicon.png">

Building the Preview: The Image

The image comes from this tag: <meta property="og:image" content="image.png">

Apple recommends you:
  1. Use images specific to your content.
  2. Avoid adding text. Rich links appear in many sizes across multiple devices; the image text might not scale.
  3. Specify an icon even when you have an image, as a fallback.

Building the Preview: The Video

The video comes from this tag: <meta property="og:video:url" content="video.mp4">

Apple recommends you:
  1. Keep the size of the icon + image + video to 10MB or less.
  2. Reference video files directly, rather than YouTube links, which will not autoplay.
  3. Avoid videos that require HTML or plug-ins; they are not supported.

All of these, except the URL itself, are optional. The LinkPresentation framework will always choose the “richest” information possible to present the link preview in the best way. The order of “richness” goes from Video > Image > Icon.

And with that, you’re finally ready to jump into the code!

Retrieving the Metadata

The first step to presenting rich links is to get the metadata.

In Xcode, open SpinViewController.swift. Here you’ll see a large array of tutorials, some outlets from the storyboard and several methods for you to implement.

To start using the LinkPresentation framework, you first have to import it. Place this at the top of the file, right below import UIKit:

import LinkPresentation

To grab the metadata for a given URL, you’ll use LPMetadataProvider. If the fetch is successful, you’ll get back LPLinkMetadata, which contains the URL, title, image and video links, if they exist. All the properties on LPLinkMetadata are optional because there’s no guarantee the web page has them set.

Add a new provider property, right below the last @IBOutlet definition for errorLabel:

private var provider = LPMetadataProvider()

To fetch the metadata, you’ll call startFetchingMetadata(for:completionHandler:) on the provider.

Locate spin(_:) and add the following implementation:

// Select random tutorial link
let random = Int.random(in: 0..<links.count)
let randomTutorialLink = links[random]

// Re-create the provider
provider = LPMetadataProvider()

guard let url = URL(string: randomTutorialLink) else { return }

// Start fetching metadata
provider.startFetchingMetadata(for: url) { metadata, error in
    let metadata = metadata, 
    error == nil 
    else { return }

  // Use the metadata
  print(metadata.title ?? "No Title")

You're probably wondering why you're recreating provider every time the user taps to spin the wheel. Well, LPMetadataProvider is a one-shot object, so you can only use an instance once. If you try to reuse it, you'll get an exception like this:

2020-01-12 19:56:17.003615+0000 Raylette[23147:3330742] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Trying to start fetching on an LPMetadataProvider that has already started. LPMetadataProvider is a one-shot object.'

But, it's a good idea to have a class-wide reference to it in case you need to use it later on in other methods.

Build and run and press the spin button a few times to make sure the URL titles get printed to the console:

Success text in console

Presenting Your Links

It's no fun just printing the title of the web page to the console, though. The real magic of rich links is to render them beautifully in the app!

Presenting a link is quite easy. The LinkPresentation framework includes LPLinkView that does all the heavy lifting for you.

Add a new property, right below provider:

private var linkView = LPLinkView()

Each time you spin the wheel, you'll create a new LPLinkView instance with the given URL and add it to stackView. Once you fetch the metadata for that particular URL, you'll add it to linkView.

Replace the current implementation of spin(_:) with the code below:

let random = Int.random(in: 0..<links.count)
let randomTutorialLink = links[random]

provider = LPMetadataProvider()
// 1

guard let url = URL(string: randomTutorialLink) else { return }
// 2
linkView = LPLinkView(url: url)

provider.startFetchingMetadata(for: url) { metadata, error in
    let metadata = metadata, 
    error == nil 
    else { return }

  // 3
  DispatchQueue.main.async { [weak self] in
    // 4
    guard let self = self else { return }

    self.linkView.metadata = metadata
// 5
stackView.insertArrangedSubview(linkView, at: 0)

In the code above, you:

  1. Remove linkView from stackView, if it's already there. You only want to present one link at a time.
  2. Initialize linkView with just the URL so while you're fetching the metadata, the user will still see something displayed.
  3. Assign the metadata to linkView. Then you use DispatchQueue to process UI changes on the main thread, since the metadata fetching executes on a background thread. If you don't, the app will crash.
  4. Use a reference to the view controller to update the interface in the background. By using [weak self] and guard let self = self, you ensure the update can proceed without causing a retain cycle — no matter what the user does while the background process is running.
  5. Add linkView to the stack view. This code runs immediately and gives the user something to see (the URL). Then, when the background process completes, it updates the view with the rich metadata.

Build and run and spin the wheel to see the link previews in action!

The app with link previews of image, icon and video links.

Some of the previews take quite a while to load, especially ones that include video links. But there's nothing that tells the user the preview is loading, so they have little incentive to stick around. You'll fix that next.

Adding an Activity Indicator

To improve the user experience when waiting for rich links to load, you'll add an activity indicator below the link view.

To do that, you'll use UIActivityIndicatorView. Take a look at SpinViewController.swift and notice it already has a property called activityIndicator. You add this property to stackView at the end of viewDidLoad().

Start animating activityIndicator by adding this line at the beginning of spin(_:):


Next, replace the block of code for fetching the metadata with this:

provider.startFetchingMetadata(for: url) { [weak self] metadata, error in
  guard let self = self else { return }

    let metadata = metadata, 
    error == nil 
    else {

  DispatchQueue.main.async { [weak self] in
    guard let self = self else { return }

    self.linkView.metadata = metadata

After unwrapping a couple optional values, this code tells the main queue to update the user interface by stopping the animation and setting the metadata on the linkView.

Build and run to see how much a simple activity indicator adds to the experience!

Link previews with loading indicator added.

Handling Errors

Thinking further about the user experience, it'd be nice if you let your users know when an error occurs, so they don't keep spinning the wheel in vain.

LPError defines all the errors that can occur if fetching the metadata fails:

  • .cancelled: The client cancels the fetch.
  • .failed: The fetch fails.
  • .timedOut: The fetch takes longer than allowed.
  • .unknown: The fetch fails for an unknown reason.

If the fetch fails, you'll show the user why. To do this, you'll use errorLabel in stackView. It starts hidden but you'll unhide it and assign it some sensible text based on the error you receive.

Handling Errors: Error Messages

Go to LPError+Extension.swift and replace LPError with this:

extension LPError {
  var prettyString: String {
    switch self.code {
    case .metadataFetchCancelled:
      return "Metadata fetch cancelled."
    case .metadataFetchFailed:
      return "Metadata fetch failed."
    case .metadataFetchTimedOut:
      return "Metadata fetch timed out."
    case .unknown:
      return "Metadata fetch unknown."
    @unknown default:
      return "Metadata fetch unknown."

This extension creates a human-readable error string for the different LPErrors.

Now go back to SpinViewController.swift and add this at the top of spin(_:):

errorLabel.isHidden = true

This clears out the error when the user taps spinButton.

Next, update the fetch block to show the error like this:

  let metadata = metadata, 
  error == nil 
  else {
    if let error = error as? LPError {
      DispatchQueue.main.async { [weak self] in
        guard let self = self else { return }

        self.errorLabel.text = error.prettyString
        self.errorLabel.isHidden = false

In the code above, you check for any errors. If one exists, you update the UI on the main thread to stop the activity indicator and then display the error to the user.

Unfortunately, you can't test this with the current setup. So, add this to spin(_:), right after the new provider instance:

provider.timeout = 1

This will trigger an error message when any of the links take longer than one second to load. Build and run to see this:

Error handing label when metadata fetch times out.

You set timeout to 1 to test the error message. Bump it up to 5 now to allow a more reasonable amount of time for these rich previews to load:

provider.timeout = 5
Note: The default timeout is 30 seconds.

Handling Errors: Cancel Fetch

Your users don't know the fetch will time out at five seconds, and they might not want to wait longer than two. If it's taking that long, they'd rather cancel the fetch. You'll give them that option next.

Inside the implementation of spin(_:), add this right under errorLabel.isHidden = true:

guard !activityIndicator.isAnimating else {

spinButton.setTitle("Cancel", for: .normal)

First, you make sure activityIndicator isn't spinning. But if it is, you know:

  1. The user tapped the Spin the Wheel version of the button. This started the fetch and set activityIndicator.isAnimating to true.
  2. The user also tapped the Cancel version of the button because they decided to bail on the fetch.

If so, you call cancel() and return.

Otherwise, if activityIndicator isn't spinning, you know the user only tapped the Spin the Wheel version of the button. So, before you kick off the fetch, you change the button title to Cancel, in case they want to cancel the fetch later.

At this point, cancel() doesn't do anything. You'll fix that next. Replace it with this:

private func cancel() {
  provider = LPMetadataProvider()

Here, you first call cancel() on the provider itself. Then you create a new provider instance and call resetViews.

But resetViews() doesn't do anything yet either. Fix that by replacing it with this:

private func resetViews() {
  spinButton.setTitle("Spin the Wheel", for: .normal)

In the code above, you stop the activity indicator and set the title for spinButton back to "Spin the Wheel":

Also, to get this same functionality in provider.startFetchingMetadata, replace the two instances of self.activityIndicator.stopAnimating() with self.resetViews():


Now if you encounter an error or the preview loads, you'll stop the activity indicator and reset the title of spinButton to "Spin the Wheel".

Build and run. Make sure you can cancel the request and that errorLabel shows the correct issue.

Cancelling the metadata fetch error.

Storing the Metadata

It can get a bit tedious to watch these links load, especially if you get the same result back more than once. To speed up the process, you can cache the metadata. This is a common tactic because web page metadata doesn't change very often.

And guess what? You're in luck. LPLinkMetadata is serializable by default, which makes caching it a breeze. It also conforms to NSSecureCoding, which you'll need to keep in mind when archiving. You can learn about NSSecureCoding in this tutorial.

Storing the Metadata: Cache and Retrieve

Go to MetadataCache.swift and add these methods to the top of MetadataCache:

static func cache(metadata: LPLinkMetadata) {
  // Check if the metadata already exists for this URL
  do {
    guard retrieve(urlString: metadata.url!.absoluteString) == nil else {

    // Transform the metadata to a Data object and 
    // set requiringSecureCoding to true
    let data = try NSKeyedArchiver.archivedData(
      withRootObject: metadata, 
      requiringSecureCoding: true)

    // Save to user defaults
    UserDefaults.standard.setValue(data, forKey: metadata.url!.absoluteString)
  catch let error {
    print("Error when caching: \(error.localizedDescription)")

static func retrieve(urlString: String) -> LPLinkMetadata? {
  do {
    // Check if data exists for a particular url string
      let data = UserDefaults.standard.object(forKey: urlString) as? Data,
      // Ensure that it can be transformed to an LPLinkMetadata object
      let metadata = try NSKeyedUnarchiver.unarchivedObject(
        ofClass: LPLinkMetadata.self, 
        from: data) 
      else { return nil }
    return metadata
  catch let error {
    print("Error when caching: \(error.localizedDescription)")
    return nil

Here, you're using NSKeyedArchiver and NSKeyedUnarchiver to transform LPLinkMetadata into or from Data. You use UserDefaults to store and retrieve it.

Note: UserDefaults is a database included with iOS that you can use with very minimal setup. Data stored in UserDefaults persists on hard drive storage even after the user quits your app.

Storing the Metadata: Refactor

Hop back to SpinViewController.swift.

spin(_:) is getting a little long. Refactor it by extracting the metadata fetching into a new method called fetchMetadata(for:). Add this code after resetViews():

private func fetchMetadata(for url: URL) {
  // 1. Check if the metadata exists
  if let existingMetadata = MetadataCache.retrieve(urlString: url.absoluteString) {
    linkView = LPLinkView(metadata: existingMetadata)
  } else {
    // 2. If it doesn't start the fetch
    provider.startFetchingMetadata(for: url) { [weak self] metadata, error in
      guard let self = self else { return }

        let metadata = metadata, 
        error == nil 
        else {
          if let error = error as? LPError {
            DispatchQueue.main.async { [weak self] in
              guard let self = self else { return }

              self.errorLabel.text = error.prettyString
              self.errorLabel.isHidden = false

      // 3. And cache the new metadata once you have it
      MetadataCache.cache(metadata: metadata)

      // 4. Use the metadata
      DispatchQueue.main.async { [weak self] in
        guard let self = self else { return }

        self.linkView.metadata = metadata

In this new method, you not only extract the metadata fetching, you also add the following functionality:

  1. Render linkView and reset the views to normal if metadata exists.
  2. Start the fetch if metadata doesn't exist.
  3. Cache the results of the fetch.

Next, replace provider.startFetchingMetadata() with a call to your new method. When you're done, you'll have the single line calling fetchMetadata() between linkView and stackView:

linkView = LPLinkView(url: url)

// Replace the prefetching functionality
fetchMetadata(for: url)

stackView.insertArrangedSubview(linkView, at: 0)

Build and run to observe how fast your links load. Keep tapping Spin the Wheel until you get a link that has been cached. Notice that your links will load immediately if you've seen them before!

Showing how fast links load when they're cached.

What's the point of finding all these great tutorials if you can't share them with your friends though? You'll fix that next.

Sharing Links

The LinkPresentation framework has a cool protocol, UIActivityItemSource, which you can use to pass LPLinkMetadata directly to the share sheet.

That means that instead of reaching out to the server and waiting for the link to load the title and icon asynchronously in the share sheet, you can pre-populate it with the metadata you already fetched.

Share sheet link loading gif.

Now your users will see a rich preview immediately. It's the little things that count!

Share sheet preview for loaded and not loaded metadata.

Sharing Links: UIActivityItemSource

First, keep track of the metadata by adding a new property to SpinViewController right below linkView:

private var currentMetadata: LPLinkMetadata?

Next, assign the value to currentMetadata in fetchMetadata(for:).

Add this line inside of the first if let statement in fetchMetadata(for:):

currentMetadata = existingMetadata

And add this line right before you cache the metadata, preceding the line, MetadataCache.cache(metadata: metadata):

self.currentMetadata = metadata

Now, to make use of the new LinkPresentation functionality, you have to make SpinViewController conform to UIActivityItemSource.

Add this extension outside SpinViewController, after the closing curly brace:

extension SpinViewController: UIActivityItemSource {
  // 1. Required function returning a placeholder
  func activityViewControllerPlaceholderItem(
    _ activityViewController: UIActivityViewController
  ) -> Any {
    return ""

  // 2. Required function that returns the actual activity item
  func activityViewController(
    _ activityViewController: UIActivityViewController, 
    itemForActivityType activityType: UIActivity.ActivityType?
  ) -> Any? {
    return currentMetadata?.originalURL

  // 3. The metadata that the share sheet automatically picks up
  func activityViewControllerLinkMetadata(
    _ activityViewController: UIActivityViewController
  ) -> LPLinkMetadata? {
    return currentMetadata

Conforming to UIActivityItemSource requires you to implement three methods:

  1. The placeholder method should return something close to the real data you intend to show in the Subject field of your activity item. However, it does not have to contain real data and it should return as fast as possible. You'll update it when the real data finishes loading. For now, a simple text string is sufficient.
  2. The originalURL of currentMetadata allows the view to figure out what type of information it will display.
    Note: You must return the originalURL property of the metadata because it contains type information that wouldn't exist if you merely return a new URL() with the same string. This is easy to get wrong and can create bugs that are hard to track down.
  3. Finally, activityViewControllerLinkMetadata(_:) is where the real magic happens when you extract all the juicy details from currentMetadata.

Sharing Links: View Update

To display this in the UI, you'll add a share button below the link view once the preview loads. The starter project provides a whole stack view with two activity buttons on SpinViewController; you simply have to show it!

Showing the activity buttons below the link preview.

The view to show is actionsStackView. When the link metadata is loading, you hide the view. Once the preview is loaded, you show it.

Add this line under activityIndicator.startAnimating() inside spin(_:):

actionsStackView.isHidden = true

And unhide it later by adding this to the end of resetViews(), before the closing curly brace:

actionsStackView.isHidden = false

Next, replace share(:) with this:

@IBAction func share(_ sender: Any) {
  guard currentMetadata != nil else { return }

  let activityController = UIActivityViewController(
    activityItems: [self], 
    applicationActivities: nil)
  present(activityController, animated: true, completion: nil)

In the code above, you:

  1. Check if currentMetadata exists. If it does, you create an instance of UIActivityViewController.
  2. Pass it [self] as activityItems. This is the important bit as it tells the activity controller to look at how SpinViewController conforms to UIActivityItemSource.

Build and run and tap the share button to see how smooth it is!

Preloaded share sheet.

Note: If you want to swap the icon that appears in the sheet preview, you can use code like this in fetchMetadata(for:) right before MetadataCache.cache(metadata: metadata):
if let imageProvider = metadata.imageProvider {
  metadata.iconProvider = imageProvider

This swaps out iconProvider for imageProvider.

Saving Favorites

Lastly, you want to save the tutorials you come across, because, hey, you might never see them again!

Next to the Share button you implemented in the previous section, there's also a Save button.

Highlighting the save button.

You'll store the tutorials the user wants to save under a special key in UserDefaults called savedURLs and display all the tutorials as link previews in the Saved tab in a stack view.

Go to MetadataCache.swift and add this underneath your current methods, right before the closing curly brace:

// 1
static var savedURLs: [String] {
  UserDefaults.standard.object(forKey: "SavedURLs") as? [String] ?? []

// 2
static func addToSaved(metadata: LPLinkMetadata) {
  guard var links = UserDefaults.standard
    .object(forKey: "SavedURLs") as? [String] else {
    UserDefaults.standard.set([metadata.url!.absoluteString], forKey: "SavedURLs")

  guard !links.contains(metadata.url!.absoluteString) else { return }

  UserDefaults.standard.set(links, forKey: "SavedURLs")

In the code above, you:

  1. Make a new property, savedURLs, which returns the array stored at the SavedURLs key in UserDefaults.
  2. Create addToSaved(metadata:) that you can call to check if a URL already exists in the SavedURLs array in UserDefaults and add it to the array if it does not.

Next, go back to SpinViewController.swift and replace save(_:) with this:

@IBAction func save(_ sender: Any) {
  guard let metadata = currentMetadata else { return }
  MetadataCache.addToSaved(metadata: metadata)
  errorLabel.text = "Successfully saved!"
  errorLabel.isHidden = false

In the code above, you check for metadata. If any exists, you call addToSaved(metadata:) and notify the user of the success through errorLabel.

Now that you're successfully saving your favorite URLs, it's time to display the links.

Switch over to SavedViewController.swift and replace loadList() with this:

private func loadList() {
  // 1
  stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }

  // 2
  let links = MetadataCache.savedURLs
  let metadata = links.compactMap { MetadataCache.retrieve(urlString: $0) }

  // 3
  metadata.forEach { metadata in
    let linkView = LPLinkView(metadata: metadata)

In the function above, you:

  1. Remove all the link preview subviews from stackView so it's completely empty.
  2. Grab all the links and convert them to LPLinkMetadata objects with retrieve(urlString:).
  3. Add all the subviews to stackView.

Build and run. Save a couple of the tutorials, and see them appear on the Saved tab of the app!

Saved tab with saved tutorials.

Using UIStackView Versus UITableView

You're using UIStackView to show these link previews instead of UITableView. Table views seem like the obvious choice to display data in a list but they're not the way to go with link presentations. There are two big reasons for this:

  1. Sizing: If you follow Apple's advice and call sizeToFit() on the link preview in tableView(_:cellForRowAt:), you'll find the previews don't appear. You could get them to appear with Auto Layout constraints on your custom cell or by giving them a specific CGSize, but read on before you try this.
  2. Memory: Even if you can get your previews to appear, you have another issue: LPLinkViews in table views cause huge memory leaks. They can get as high as 10GB when scrolling! Take a look at the memory load below:

    Memory issues in a table view.

With a memory leak like that, your app will crash and the one-star reviews will start flying. Best to stay away from table views for link presentations!

Time to test your app one more time. Build and run and tap one of the links in the Saved tab. Safari will open and you'll see your chosen tutorial displayed as a web page. Good job!

Where to Go From Here?

Congrats! You've just learned most of what there is to know about link presentations!

Download the final project using the Download Materials button at the top or bottom of this page.

To learn more about what Apple has to say on the topic, watch the WWDC 2019 videos on Embedding and Sharing Visually Rich Links and Ensuring Beautiful Rich Links.

If you want to take a closer look at the framework, read the documentation on the LinkPresentation framework.

And if you're curious how to make links even more powerful and useful, check out our Universal Linking tutorial!

If you have any questions or comments, please join the forum discussion below.