Greetings, traveler!
Modern applications often enhance user experience by providing rich previews for shared links. These previews typically include an image, title, and domain, giving users context before they decide to open a URL. In this article, we’ll explore how to build a native link preview component in SwiftUI using built-in frameworks like LinkPresentation
, SwiftUI
, and UniformTypeIdentifiers
.
This solution doesn’t rely on third-party dependencies and is built entirely with Apple’s native APIs.

When You Need a Link Preview
A link preview view becomes useful in any context where user-generated content or external links are presented. Use cases include:
- Messaging and chat applications
- Notes or bookmarking apps
- Admin dashboards showing submitted links
- News feeds or aggregators
By generating rich previews, you reduce friction and increase trust for end users.
Required Frameworks
To build this component, the following native frameworks are used:
import LinkPresentation
import UniformTypeIdentifiers
LinkPresentation
: for fetching metadata from a URLUniformTypeIdentifiers
: for resolving image data types inNSItemProvider
Core Structure
The view consists of two main components:
LinkPreviewView
: A SwiftUI view that displays the previewLinkPreviewViewModel
: An@Observable
class that fetches metadata asynchronously
Here’s how the view is initialized:
struct LinkPreviewView: View {
@State private var viewModel: LinkPreviewViewModel
init(_ url: URL) {
self.viewModel = .init(url)
}
var body: some View {
content
.padding()
.task {
await viewModel.onAppear()
}
}
}
Display Logic
The view conditionally renders content based on the state of the viewModel
:
private var content: some View {
VStack(spacing: 20) {
switch viewModel.state {
case let .ready(model):
webContent(model)
case .loading:
ProgressView()
case let .error(message):
Text(message)
}
controls()
}
}
When metadata is ready, a clickable card is shown:
private func webContent(_ model: PreviewModel) -> some View {
Button {
viewModel.openURL()
} label: {
VStack(spacing: 8) {
Image(uiImage: model.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxHeight: 150)
.cornerRadius(16)
VStack(alignment: .leading) {
Text(model.title)
.font(.body)
.lineLimit(2)
Text(model.subtitle)
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
Asynchronous Metadata Fetching
The view model uses LPMetadataProvider
to asynchronously fetch metadata:
@MainActor
final class LinkPreviewViewModel {
enum State {
case loading
case ready(PreviewModel)
case error(String)
}
var state: State = .loading
private let previewURL: URL
init(_ url: URL) {
self.previewURL = url
}
func onAppear() async {
let provider = LPMetadataProvider()
do {
let metadata = try await provider.startFetchingMetadata(for: previewURL)
let title = metadata.title
let subtitle = metadata.url?.host()
let image = try await makeImage(metadata.imageProvider)
let model = PreviewModel(image: image ?? .init(), title: title ?? "", subtitle: subtitle ?? "")
state = .ready(model)
} catch {
state = .error(error.localizedDescription)
}
}
func openURL() {
UIApplication.shared.open(previewURL)
}
}
The makeImage(_:)
helper safely extracts a UIImage
from NSItemProvider
:
private func makeImage(_ itemProvider: NSItemProvider?) async throws -> UIImage? {
guard let itemProvider else { return nil }
let utType = String(describing: UTType.image)
if itemProvider.hasItemConformingToTypeIdentifier(utType) {
let item = try await itemProvider.loadItem(forTypeIdentifier: utType)
if let image = item as? UIImage {
return image
}
if let url = item as? URL {
return UIImage(data: try Data(contentsOf: url))
}
if let data = item as? Data {
return UIImage(data: data)
}
}
return nil
}
Conclusion
With minimal code and no third-party dependencies, you can deliver a native link preview experience in SwiftUI using LinkPresentation
.
If you’re building any app that surfaces links to users, incorporating a rich preview component like this adds both polish and usability.
GitHub: https://github.com/Livsy90/LinkPreview/tree/main
If you enjoyed this article, please feel free to follow me on my social media: