Overview
At time of writing, the Share Extension has yet to be given the SwiftUI overhaul that other Target-types have received, so you’ll be met with a UIKit setup and Storyboard interface upon initial creation.
If you’re like me and want to leverage SwiftUI wherever possible, this article will show you how to do just that for your Share Extensions 👇
Share Extension Setup
The sections below will walk you through setting up a brand new Share Extension in your app. If you’ve already got one setup, feel free to jump down to SwiftUI Conversion.
Creating a Share Extension
Following this guide requires you have a Share Extension to work from. Follow the steps below if you have yet to create one:
- In Xcode, select File → New → Target
- Find the
Share Extension
target and select Next
- Provide a name for your Share Extension target
- If prompted, go ahead and activate the Scheme for the Share Extension
At this point you should now have a Share Extension target with the following files:
ShareViewController.swift
(a subclass ofSLComposeServiceViewController
)MainInterface.storyboard
Info.plist
Running the Share Extension
To see your Share Extension in action:
- Build + Run your Share Extension scheme to a simulator
- When prompted for an app to run in, select Safari
- Open any webpage and tap Safari’s Share button
- Select your app as the target for the shared content
SwiftUI Conversion
Removing the Storyboard Interface
To kick things off, we’ll remove the Storyboard file and update the Info.plist
to point directly to the ShareViewController
class as the entry point for the Share Extension:
- Delete
MainIterface.storyboard
- Make the following updates to
Info.plist
:- Delete the entry for
NSExtensionMainStoryboard
- Add a new entry in its place with the key
NSExtensionPrincipalClass
and the value<TargetName>.ShareViewController
- Replace
<TargetName>
with the actual name of your Share Extension target
- Replace
- Delete the entry for
- Build + Run your Share Extension scheme to a simulator to ensure the behavior has not changed
Present a SwiftUI View
Now we’ll update the existing ShareViewController
to start presenting a SwiftUI view:
- Update
ShareViewController
to remove the inheritance ofSLComposeServiceViewController
and replace it withUIViewController
- Create a SwiftUI view that will be the main interface of your Share Extension
- Load the SwiftUI view in a
UIHostingController
and add it to yourUIViewController
onviewDidLoad
// MARK: - SwiftUI Interface
struct ShareView: View {
var body: some View {
Text("Hello, World!")
}
}
// MARK: - UIKit Interface
class ShareViewController: UIViewController {
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
guard let extensionContext else {
// Exit if we weren't provided a context to work with
return
}
setupShareView(with: extensionContext)
}
private func setupShareView(with context: NSExtensionContext) {
// Wrap the SwiftUI view in a Hosting Controller
let contentView = UIHostingController(
rootView: ShareView()
)
// Add the SwiftUI view as a child of ShareViewController
addChild(contentView)
view.addSubview(contentView.view)
// Pin the child view to its parent
contentView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.view.topAnchor.constraint(equalTo: view.topAnchor),
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
}
When you build and run your Share Extension now, you should see the following when sharing a webpage.
Congrats, you’ve got SwiftUI up and running in a Share Extension! 🎉
Feel free to stop here and customize your view to fit your needs, but if you’d like to continue, I’ll walk through replicating a similar UI to that of SLComposeServiceViewController
where we pull metadata from the URL and display it to the user. ⬇️
Display URL Metadata in your SwiftUI Share Extension
Below I’ll walk you through the steps needed to pull metadata from a URL that was shared with your Share Extension and display it back to the user. The final result will be the following:
App Extension Utilities
Working with NSExtensionContext
and NSItemProvider
can be fairly verbose, so I’ve created a couple helper extensions that we can use to extract the URL and image from the webpage shared with our Share Extension.
import Foundation
import UniformTypeIdentifiers
// MARK: - App Extension Utilities
enum ShareExtensionError: Error {
case noAttachmentsMatchingType(UTType)
case itemTypecastFailed(UTType)
}
extension NSExtensionContext {
/// Attempts extracting the first attachment from the current context
/// that matches the provided Uniform Type.
func firstAttachment(ofType type: UTType) throws -> NSItemProvider {
guard let firstItem = inputItems.first as? NSExtensionItem,
let attachments = firstItem.attachments,
let attachment = attachments.first(where: { $0.hasItemConformingToTypeIdentifier(type.identifier) }) else {
throw ShareExtensionError.noAttachmentsMatchingType(type)
}
return attachment
}
}
extension NSItemProvider {
func loadURL() async throws -> URL {
let uniformType = UTType.url
let loadedItem = try await loadItem(forTypeIdentifier: uniformType.identifier)
guard let url = loadedItem as? URL else {
throw ShareExtensionError.itemTypecastFailed(uniformType)
}
return url
}
func loadImage() async throws -> UIImage {
let uniformType = UTType.image
// Make image loading async by wrapping the loadDataRepresentation(for:)
// API with withCheckedThrowingContinuation
return try await withCheckedThrowingContinuation { continuation in
_ = loadDataRepresentation(for: uniformType) { imageData, error in
if let error {
continuation.resume(throwing: error)
} else if let imageData, let image = UIImage(data: imageData) {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: ShareExtensionError.itemTypecastFailed(uniformType))
}
}
}
}
}
Configuring the SwiftUI View to Display Webpage Metadata
Next we’ll configure the SwiftUI view to fetch metadata from the provided URL, populate the metadata in the UI, and provide Cancel and Save actions for the user, similar to the previous SLComposeServiceViewController
:
import LinkPresentation
import SwiftUI
// MARK: - SwiftUI Interface
struct ShareView: View {
@State private var text: String = ""
@State private var previewImage: Image?
let context: NSExtensionContext
var body: some View {
NavigationStack {
HStack(alignment: .top) {
// We'll try auto-populating the TextEditor with the webpage
// title, but allow the user to change that as needed.
TextEditor(text: $text)
if let previewImage {
previewImage
.resizable()
.frame(width: 80, height: 50)
.aspectRatio(contentMode: .fill)
.cornerRadius(8)
.shadow(radius: 10)
}
}
.padding(.horizontal)
.toolbar {
// Setup a navigation bar that mirrors
// SLComposeServiceViewController
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: cancelAction)
}
ToolbarItem(placement: .confirmationAction) {
Button("Save", action: saveAction)
}
}
.task {
// Start loading webpage metadata before the view appears
do {
try await loadWebpageMetadata(for: context)
} catch {
print(error)
}
}
}
}
// MARK: Button Actions
private func cancelAction() {
enum ShareError: Error {
case userCancelled
}
context.cancelRequest(withError: ShareError.userCancelled)
}
private func saveAction() {
// Return an array of results ([NSExtensionItem]) to the host app
context.completeRequest(returningItems: [])
}
// MARK: Networking
private func loadWebpageMetadata(
for context: NSExtensionContext
) async throws {
// Use previously defined extensions to extract the URL
// from the extension context:
let url = try await context.firstAttachment(ofType: .url).loadURL()
// Use Apple's LPMetadataProvider API to extract metadata from the URL:
let linkMetadataProvider = LPMetadataProvider()
let metadata = try await linkMetadataProvider.startFetchingMetadata(for: url)
// Once we extract the metadata, update the UI with available info
if let webpageTitle = metadata.title {
text = webpageTitle
}
if let imageProvider = metadata.imageProvider,
let linkPreviewImage = try? await imageProvider.loadImage() {
previewImage = Image(uiImage: linkPreviewImage)
}
}
}
The final step (which the compiler should already be complaining about) is to pass the extension context into your SwiftUI view:
class ShareViewController: UIViewController {
private func setupShareView(with context: NSExtensionContext) {
let contentView = UIHostingController(
rootView: ShareView(context: context)
)
...
}
}
Build and Run your Share Extension again to start seeing webpage metadata populated in SwiftUI! 🚀
Additional Resources
As you continue building our your Share Extension, check out these Apple Developer resources: