In my side project I am handling various URLs to various websites a user can save to keep track of their analytical data. To enrich my UI with some metadata about the respective URLs I was searching for a convenient way to do so. What I found was LPMetadataProvider
(Apple Documentation). A class I never heard of before but already exists for several years. I searched for some more information and found that most of the information is about how to use it in combination with LPLinkView
or how to create instances of LPMetadataProvider
to enrich the share sheet. All of them were very interesting but not quite what I was looking for. I want to extract all metadata from an URL and use it in my custom UI. Therefore I dug into LPMetadataProvider
and searched for ways to use it how I want.
To make it a little bit more interesting, we can set up a task which is used as an anchor point for this post.
Show a list of URLs and asynchronously fetch their favicons, if available, using
LPMetadataProvider
.
The Basics
LPMetadataProvider
can be used to fetch metadata for a URL
. It consists of title, icon, image and video links. All of these properties are Optionals
. If the website did not provide the data for these properties they are just not available.
Let's have an example: Given the URL of one of my blog posts: https://alexanderweiss.dev/blog/2023-03-05-swiftui-task-modifier, we can take a look at some of the HTML header tags.
<link rel="shortcut icon" href="/favicon.ico">
<meta property="og:title" content="SwiftUI's .task modifier">
<meta property="og:image" content="https://alexanderweiss.dev/api/og?title=SwiftUI%27s%20.task%20modifier">
They contain a favicon
and two OpenGraph tags for an image
and title
. Most of today's websites have implemented OpenGraph tags, if you want to read a little bit more about that concept, please refer to https://ogp.me.
If we compare this with the data from the provider, we can already see similarities:
import LinkPresentation
Task {
let metadataProvider = LPMetadataProvider()
let url = URL(string: "https://alexanderweiss.dev/blog/2023-03-05-swiftui-task-modifier")!
let metadata = try await metadataProvider.startFetchingMetadata(for: url)
print(metadata.title)
// Optional("SwiftUI\'s .task modifier")
print(metadata.url)
// Optional(https://alexanderweiss.dev/blog/2023-03-05-swiftui-task-modifier)
print(metadata.imageProvider)
// Optional(<NSItemProvider: 0x600002802610> {types = (
// "public.png"
// )})
print(metadata.iconProvider)
// Optional(<NSItemProvider: 0x600002810380> {types = (
// "com.microsoft.ico"
// )})
}
After the metadata is fetched, the title
and url
properties are already available for consumption. We can also see they are optional, meaning if a website does not implement its header/meta tags properly LPMetadataProvider
is not capable of properly fetching the corresponding data.
Furthermore, it can be seen that there is an image and icon available for the website. To receive the image data out of the NSItemProvider
we have to work with the item providers API
metadata.iconProvider?.loadDataRepresentation(for: .image, completionHandler: { data, error in
if let data {
let image = UIImage(data: data)
} else if let error {
print(error)
}
})
metadata.imageProvider?.loadDataRepresentation(for: .image, completionHandler: { data, error in
if let data {
let image = UIImage(data: data)
} else if let error {
print(error)
}
})
The above implementation then results in downloading and instantiating UIImage
instances.
Custom usage
After covering the basics of LPMetadataProvider
we can utilise this information to complete the task we set at the beginning of this post.
At first, we introduce an extension on NSItemProvider
. While the metadata provider does offer some async/await conforming methods, the item provider does not. We are going to patch this by wrapping loadDataRepresentation(for:completionHandler:)
into an async/await compatible method.
import LinkPresentation
import UniformTypeIdentifiers
extension NSItemProvider {
func loadDataRepresentation(for type: UTType) async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
_ = self.loadDataRepresentation(for: type) { data, error in
if let data {
continuation.resume(returning: data)
} else if let error {
continuation.resume(throwing: error)
}
}
}
}
}
In the introduction, we saw that loading an icon from an URL is a two-step approach. First loading the metadata, then using the iconProvider
property. To abstract away the two-step approach we can introduce an abstraction on the provider. It should hide the heavy lifting and provide an easy-to-use modern swifty API.
import LinkPresentation
import UniformTypeIdentifiers
class LPLoader { }
The next step now is to model some error cases that can occur while loading a favicon.
enum LPLoaderError: Error {
/// Metadata loading failed
case metadataLoadingFailed(error: Error)
/// Favicon loading failed
case faviconCouldNotBeLoaded
/// Favicon data is invalid
case faviconDataInvalid
}
Having the base class and error model in place, we can come up with a method to load a favicon of a given URL
.
class LPLoader {
func favicon(for url: URL) async throws -> UIImage {
let metadataProvider = LPMetadataProvider()
let metadata: LPLinkMetadata
do {
metadata = try await metadataProvider.startFetchingMetadata(for: url)
} catch {
throw LPLoaderError.metadataLoadingFailed(error: error)
}
guard let iconProvider = metadata.iconProvider else {
throw LPLoaderError.faviconCouldNotBeLoaded
}
let iconData: Data
do {
iconData = try await iconProvider.loadDataRepresentation(for: .image)
} catch {
throw LPLoaderError.metadataLoadingFailed(error: error)
}
guard let icon = UIImage(data: iconData) else {
throw LPLoaderError.faviconDataInvalid
}
return icon
}
}
Let us have a more detailed look at what is happening in the snippet above.
- First, the metadata associated with the given
URL
is loaded - Then, check whether it is possible to load some icon
- If the check is successful, the icon is loaded
- Last step, is to convert the loaded data into an
UIImage
instance and return it to the caller.
Usage
Structuring and coming up with a solution is one part. Using that solution in an application is the other one. To finish up this post, the following lines demonstrate how our solution can be used to fulfil the task we set at the beginning. The following code snippets set up a simple UI with a list of rows that asynchronously load the favicon of a given URL
.
- Create a list of URLs
struct URLList: View {
// MARK: - State Properties
@State private var urls: [URL] =
[
URL(string: "https://apple.com")!,
URL(string: "https://stackoverflow.com")!,
URL(string: "https://alexanderweiss.dev")!
]
// MARK: - Body
var body: some View {
List {
ForEach(urls, id: \.self) { url in
URLRow(url: url)
}
}
}
}
- Implement the list row where, where
LPLoader
comes into play
struct URLRow: View {
// MARK: - State Properties
@State private var favicon: UIImage?
// MARK: - Private Properties
private let url: URL
private var loader: LPLoader
// MARK: - Init
init(url: URL) {
self.url = url
self.loader = .init()
}
// MARK: - Body
var body: some View {
Label {
Text(verbatim: url.absoluteString)
} icon: {
if let favicon {
Image(uiImage: favicon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.background(.white)
.cornerRadius(2)
} else {
ProgressView()
}
}
.task(id: url) {
do {
self.favicon = try await loader.favicon(for: url)
} catch {
self.favicon = UIImage(systemName: "exclamationmark.octagon.fill")
}
}
}
}
- Final look
❗ Caveats ❗
Before coming to an end I want to highlight some caveats I encountered implementing LPMetadataProvider
:
- Internally it seems to spin up a
WKWebView
to receive the metadata, this can be expensive and causes calls to be ´MainActor´ isolated - Caching is highly recommended since the calls to fetch the metadata are expensive. I did not cover caching in this post since I wanted it to be more focused on how to use the
LPMetadataProvider
in a more custom environment. There are multiple ways of tackling this issue, some ideas I have:- Using
NSCache
for in-memory caching - Using a custom on-disk cache solution utilising the fact that the metadata is
NSSecureCoding
conforming
- Using
- The API is not intuitive and needs some digging to understand what's going on
Conclusion
That's a wrap for today. I hope this post was helpful to understand how LPMetadataProvider
works in general and you can use it in your apps.
We did take a look at how the provider can be used in a general way as well in a more independent way in combination with some loading data into a custom UI.
If you find any mistake or would like to reach out to me, please do so 🙂
See you next time! 👋