Building a Native Link Preview in SwiftUI


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 URL
  • UniformTypeIdentifiers: for resolving image data types in NSItemProvider

Core Structure

The view consists of two main components:

  • LinkPreviewView: A SwiftUI view that displays the preview
  • LinkPreviewViewModel: 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