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
}
Present Link Previews with LPLinkView
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 theLPLinkView
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
Present Link Previews with Custom UI
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 🫶.