Ok, so given the lack of a easy to follow documentation on how to implement an iOS ShareSheet Extension, I finally manage to do so, thanks to a couple of well written blog posts and a video by some folks over the internet. I'll link all of them at the end of this post.
Now I'm writing this blog post to document my process, in case I need to do this again in the future.
Goals
My goal was simple, or so I thought it was: share a link from Safari to my app.
It seems simple but it's rather weird, for lack of a better word.
Getting Started
This post will assume you have a SwiftUI app up and running, for the sake of brevity.
This post will also assume you are using Xcode 16 or later, and even so, bear in mind that things could have changed, by the time you are reading.
Ok, so, the first thing we wanna do is to add a new target to your project.
To do so: File > New > Target...
Then, type in the search box "share", select the Share Extension option, and then hit Next.

Give it a name, it can be anything but try to use a meaningful name, something like YourProjectNameShare or whatever.
Click Finish and then Activate.
Your project directory would look something like this:

Ok, cool? Cool.
Creating a SwiftUI View
Next, let's just create a Swift UI View placeholder, we will use this later. So go ahead and select your ShareView target folder (in my case it's called ShareExtensionView like the image above), and then right click New File from Template..., select SwiftUI View, give it a name, make sure you select the Share Extension Target (like the image below) and click Create.

We will be back to this view later.
Defining the ShareViewController
Now, let's define our ShareViewController as our NSExtensionPrincipalClass, and remove the automatically created MainInterface storyboard.
You can do that in two ways, both using the Info.plist located inside the Share Extension target.
The first way is to open the Info.plist file as Property List, by clicking on it. You will see something like this:

Or, you can right click on the Info.plist file, then go to Open As > Source Code. It doesn't matter which way you go, as long as you do the following changes:
First, rename NSExtensionMainStoryboard to NSExtensionPrincipalClass, and then rename its value from MainInterface to <NameOfYourShareExtensionTarget>.ShareViewController.
Updating NSExtensionActivationRule
Let's go ahead and change the NSExtensionActivationRule, which for what I understood, defines for what kind of data the Share Extension should be presented. By default it's presented for every kind of data, and it seems like this is forbidden if you try to publish your app.
As you can probably image, this is also done via the Info.plist file.
To do so:
- Change NSExtensionActivationRule type from String to Dictionary;
- Since we want to work with URLs, add a new key inside NSExtensionActivationRule key and name it as NSExtensionActivationSupportsWebURLWithMaxCount;
- Set its type to Number and give it a value of 1
- Follow the same steps as above and create a new key named NSExtensionActivationSupportsText, set its type to Boolean and its value as true;
- It seems we need this key to recognize new URLs that were just typed or pasted to the browser.
After all of this, your Info.plist should look like this:

Or like so, in the Source Code mode:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
</dict>
</dict>
<key>NSExtensionPrincipalClass</key>
<string>ShareExtensionView.ShareViewController</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
Adding the App Groups capability
Ok, so this part is very important since we want to share data between the two Targets of our app. Without this, things won't work, meaning, the link you want to share from Safari via our Share Extension to our main app won't work.
We need to add a new capability to our app's Targets, called App Groups.
To do this, select the root folder on Xcode's Navigator (the one that has your app's name).
Then click on Signing & Capabilities.
On the left side, click on + Capability.
Search for App Groups and double click on it. Make sure to repeat this process for both Targets.

On the App Groups section, add a new container by clicking on the + button. Give it a meaningful name, starting with group. something like group.yourprojectname.

Make sure to select this container name for both Targets. If it's in red, deselect it and select it again. Just to make sure.
Editing ShareViewController
Right now, our ShareViewController is untouched, we need to change quite a few things. Basically we need to prepare it to receive the data from Safari via Share Sheet, and to pass it forward to our SwiftUI View we created before, and from there do whatever you wanna do it with this information.
For that, I did something like this:
import UIKit
import SwiftUI
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard
let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
let itemProvider = extensionItem.attachments?.first else {
self.close()
return
}
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil) { (url, error) in
if error != nil {
self.close()
return
}
if let sharedUrl = url as? URL {
DispatchQueue.main.async {
let contentView = UIHostingController(rootView: ShareView(urlFromShareViewSeet: "\(sharedUrl)"))
self.addChild(contentView)
self.view.addSubview(contentView.view)
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true
}
} else {
self.close()
return
}
}
} else {
close()
return
}
NotificationCenter.default.addObserver(forName: NSNotification.Name("Close"), object: nil, queue: nil) { _ in
DispatchQueue.main.async {
self.close()
}
}
}
func close() {
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
Editing our SwiftUI View
Same thing if our SwiftUI ShareView.
import SwiftUI
import SwiftData
struct ShareView: View {
@State var urlFromShareViewSheet: String
init(urlFromShareViewSeet: String) {
self.urlFromShareViewSheet = urlFromShareViewSeet
}
var body: some View {
NavigationStack {
VStack(spacing: 20) {
Text(urlFromShareViewSheet)
Button {
Task {
await saveLink(sharedLink: urlFromShareViewSheet)
}
self.close()
} label: {
Text("Save Link")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 5))
}
.padding()
.toolbar {
Button("Cancel") {
self.close()
}
}
.navigationTitle("Add new bookmark")
}
}
func saveLink(sharedLink: String) async {
// do something
}
func close() {
NotificationCenter.default.post(name: NSNotification.Name("Close"), object: nil)
}
}
//#Preview {
// ShareView()
//}
Resources
I wouldn't be able to accomplish such feature without the following resources:
- https://tnvmadhav.me/guides/how-to-build-a-simple-share-extension-in-swift/
- https://www.merrell.dev/ios-share-extension-with-swiftui-and-swiftdata/
- https://medium.com/@damisipikuda/how-to-receive-a-shared-content-in-an-ios-application-4d5964229701
- https://kait.dev/post/implementing-swiftui-share-extension
- https://medium.com/@henribredtprivat/create-an-ios-share-extension-with-custom-ui-in-swift-and-swiftui-2023-6cf069dc1209
- https://www.youtube.com/watch?v=_YJPnTQ8R1A
Thank you all! 😄