Home Using LPMetadataProvider to Extract Metadata from a URL
Post
Cancel

Using LPMetadataProvider to Extract Metadata from a URL

TLDR:

LPMetadataProvider allows us to extract metadata from a URL.

Introduction

Nowadays, majority of websites include OpenGraph <meta> tags, that summarizes their content for search engines. For example, If you inspect my home page source by right clicking View Page Source you will be presented with something like this:

1
2
3
4
5
6
7
8
9
10
<meta property="og:title" content="Osaretin’s Blog" />
<meta property="og:locale" content="en" />
<meta name="description" content="An iOS blog" />
<meta property="og:description" content="An iOS blog" />
<meta property="og:url" content="/" />
<meta property="og:site_name" content="Osaretin’s Blog" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="Osaretin’s Blog" />
<meta name="twitter:site" content="@swiftlogic" />

consuming these metadata in our app is where apple’s LinkPresentation framework comes in. We can leverage her LPMetadataProvider class to fetch these metadata and then construct a rich preview for our users.

Rich link previews are common in many mobile apps. They make it easier for users to see what a link is about before clicking on it, which is definitely a better user experience than just a plain ole ugly url.

Apple’s Link Presentation framework provides a way for us to extract and display metadata from URLs in a visually appealing way. We can either fetch and display the metadata ourselves, or we can use Apple’s provided LPLinkView.

How it Works

The LPMetadataProvider is the primary API that allows us to extract the metadata from a web page’s tags, and then gives us back things like the title, image, video, icon, URL, and even the description (although the description is a private key called _summary; more on that later).

Under the hood, when you tell LPMetadataProvider to extract metadata from a URL, it essentially fires up a WKWebView to accomplish the task. This is obviously not ideal, and it definitely raises performance issues, especially when dealing with lists or collection views. However, thankfully, Apple made LPLinkMetadata, the object in which we store the metadata, conform to NSSecureCoding, which makes it easy to serialize and cache locally.

Let’s See it In Action

All we need to fetch metadata for a given URL is the LPMetadataProvider's startFetchingMetadata method. Here is what that looks like in action. Below, I have extracted the title, description, hostName, image, icon, and even remoteVideoURL info when available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func extractMetadata(from url: URL = URL(string: "https://swiftlogic.io/posts/how-to-learn-technical-things/")!) async -> LPLinkMetadata? {
    let metadataProvider = LPMetadataProvider()
    
    do {
        let metadata = try await metadataProvider.startFetchingMetadata(for: url)
        
        let title = metadata.title
        let description = metadata.value(forKey: "_summary") as? String
        let hostName = url.host
        
        // Get URL Image
        _ = metadata.imageProvider?.loadDataRepresentation(for: .image) { imageData, error in
            if let imageData = imageData {
                // We now have access to the URL's image by using NSItemProvider to load the image object
                let uiImage = UIImage(data: imageData)
                print("📀 Metadata image: \(uiImage)")
            }
        }
        
        // Get URL Logo
        _ = metadata.iconProvider?.loadDataRepresentation(for: .image) { imageData, error in
            if let imageData = imageData {
                // We now have access to the URL's icon by using NSItemProvider to load the image object
                let uiImage = UIImage(data: imageData)
                print("📀 Metadata icon: \(uiImage)")
            }
        }
        
        let videoURL = metadata.remoteVideoURL
        print("📀 Metadata Title: \(title)")
        print("📀 Metadata Desc: \(description)")
        print("📀 Metadata hostName: \(hostName)")
        print("📀 Metadata videoURL: \(String(describing: videoURL))")
        
        return metadata
    } catch {
        print("Failed to get metadata for URL: \(error.localizedDescription)")
    }   
    return nil
}

We can lean into Apple-provided LPLinkView to show link previews and here’s how to do that in SwiftUI. First we need to wrap our LPLinkView which is a UIKit view in a UIViewRepresentable so we can interop with UIKit, then we need to fetch the metadata and inject it into our LPLinkView. Here’s the code on how to accomplish that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import SwiftUI
import LinkPresentation

class CustomLinkView: LPLinkView {
    override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
}

struct LinkViewRepresentable: UIViewRepresentable {
 
    typealias UIViewType = CustomLinkView
    
    var metadata: LPLinkMetadata?
 
    func makeUIView(context: Context) -> CustomLinkView {
        guard let metadata = metadata else { return CustomLinkView() }
        let linkView = CustomLinkView(metadata: metadata)
        return linkView
    }
 
    func updateUIView(_ uiView: CustomLinkView, context: Context) {
    }
}

By subclassing the LPLinkView and overriding its intrinsic content size, you can modify the size of the LPLinkView in SwiftUI. We’ll see this in action shortly.

Here’s how we would use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import LinkPresentation
struct LinkPreviewDemoView: View {
    @State private var metadata: LPLinkMetadata?
    
    var body: some View {
        VStack(spacing: 20) {
            if let metadata = metadata {
                LinkViewRepresentable(metadata: metadata)
                // subclassing the `LPLinkView` and overriding its intrinsic content size allows us to set desired dimension.
                    .frame(width: 200, height: 250)
                    .padding(.bottom)
                
                
                LinkViewRepresentable(metadata: metadata)
                // subclassing the `LPLinkView` and overriding its intrinsic content size allows us to set desired dimension.
                    .frame(height: 200)
                    .padding()
                
            } else {
                ProgressView()
            }
        }
        .task {
            await fetchMetadata()
        }
    }
    
    private func fetchMetadata() async {
        let url = URL(string: "https://swiftlogic.io/posts/how-to-learn-technical-things/")!
        self.metadata = await extractMetadata(from: url)
    }
}

Project Demo

Okay, now that we have explored how to display rich link previews using the default LPLinkView, let’s delve into how to craft a custom link preview. We’ll leverage LPLinkMetadata to power this custom view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
struct LinkPreviewView: View {
    let linkPreview: LPLinkMetadata
    @State private var image: UIImage?
    @State private var icon: UIImage?
    
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            
            HStack {
                iconImageView()
                
                if let title = linkPreview.title {
                    Text(title)
                        .font(.headline)
                }
            }
            
            websiteImageView()
            
            if let description = linkPreview.value(forKey: "_summary") as? String {
                Text(description)
                    .foregroundColor(.gray)
            }
            
            if let url = linkPreview.url {
                Link(destination: url) {
                    HStack {
                        Text("Read more")
                        Image(systemName: "arrow.right")
                    }
                    .bold()
                }
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(12)
        .padding()
        .task {
            fetchImageForURL()
            fetchIconForURL()
        }
    }
    
    @ViewBuilder
    private func iconImageView() -> some View {
        if let iconImage = icon {
            Image(uiImage: iconImage)
                .resizable()
                .scaledToFill()
                .frame(width: 50, height: 50)
                .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
        }
    }
    
    @ViewBuilder
    private func websiteImageView() -> some View {
        if let uiImage = image {
            Image(uiImage: uiImage)
                .resizable()
                .scaledToFill()
                .frame(maxHeight: 200)
                .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
        }
    }
    
    private func fetchImageForURL() {
        _ = linkPreview.imageProvider?.loadDataRepresentation(for: .image) { imageData, error in
            if let imageData = imageData {
                self.image = UIImage(data: imageData)
            }
        }
    }
    
    private func fetchIconForURL() {
        _ = linkPreview.iconProvider?.loadDataRepresentation(for: .image) { imageData, error in
            if let imageData = imageData {
                self.icon = UIImage(data: imageData)
            }
        }
    }
}

Usage:

1
2
3
4
5
6
7
8
9
10
11
12
    @State private var linkPreview: LPLinkMetadata?

    ZStack {
      if let linkPreview = linkPreview {
          LinkPreviewView(linkPreview: linkPreview)
      } else {
          ProgressView()
      }
  }
  .task {
      self.linkPreview = await extractMetadata()
  }

And here’s what our custom LinkPreviewView looks like:

Custom LinkPreviewView

Closing Thoughts

Thanks to the link presentation framework, fetching and consuming a URL’s metadata is quite trivial. However, what’s not trivial is the memory and performance issues you will definitely run into if you don’t cache the LPLinkMetadata that you get back after firing the LPMetadataProvider's startFetchingMetadata method. Make sure you implement an on-disk or in-memory cache to prevent your app from hitting up the same resource over and over again.

Hopefully you found this helpful, thanks for reading 🫶.

This post is licensed under CC BY 4.0 by the author.