# Share Extension

The StreamChat iOS SDK provides possibilities for deeper integration with the operating system, helping your users to engage with your app even when it is not active. One example is the "Share" extension, which allows users to share content via third-party apps from other apps, such as Apple's native Photos app.

### Chat Share Extension

In this guide, we will show you how to create such extension, which will share photos from Apple's share sheet to the Stream chat.

In order to get started, you need to create a share extension for your project in Xcode:

![Screenshot shows an Xcode screen creating share extension](@chat-sdk/ios/v5/_assets/share-extension.png)

This action will generate a new target and a starting point to implement the share extension. In this example, we will not use a Storyboard, so you can delete that file from the generated code. The `Info.plist` file needs to be updated accordingly, by removing the storyboard entry. Its contents should look something like this:

```swift
<?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>IntentsSupported</key>
            <array>
                <string>INSendMessageIntent</string>
            </array>
            <key>NSExtensionActivationRule</key>
            <dict>
                <key>NSExtensionActivationSupportsImageWithMaxCount</key>
                <integer>10</integer>
            </dict>
        </dict>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.share-services</string>
        <key>NSExtensionPrincipalClass</key>
        <string>DemoShare.ShareViewController</string>
    </dict>
</dict>
</plist>
```

Note that the `NSExtensionPrincipalClass`'s name consists of the target name and the name of the class that will be called when the user selects our extension from Apple's native share sheet.

In the `NSExtensionActivationRule`, we specify how our extension can be activated. Since we want to share images, we will use `NSExtensionActivationSupportsImageWithMaxCount`, with the value of 10. That means that our extension would be shown when up to 10 images are selected in the photos app. You can configure this value according to your requirements.

#### Setting up App Groups

To support sharing from an extension, we would need to take the currently logged in user in the main app. To do this, we need to create a shared container between the main app and the service extension. You can do this by adding an app group capability within your projects “Signing & Capabilities” section.

Make sure to use the same group for both targets (app and extension). When you have both configured, you need to adjust your `ChatClient` setup and add the following code to the configuration object:

```swift
var config = ChatClientConfig(apiKeyString: "<# Your API Key Here #>")
config.applicationGroupIdentifier = "group.x.y.z"

let client = ChatClient(config: config)
```

In order for this to work correctly, you need to do it in the service extension and in the application.

#### ShareViewController

Next, let's implement the `ShareViewController`, which is called when the user selects our extension for sharing. In the generated code, the `ShareViewController` is subclassing the `SLComposeServiceViewController`, which provides a default UI with a text field and the images to share.

However, in our case we want to have the possibility to select a channel where we want to share the message, so we will change the subclass to `UIViewController` instead, and provide our custom UI.

Here's how the end result will look like:

![Screenshot showing the share sheet UI](@chat-sdk/ios/v5/_assets/share-sheet.jpeg)

In this example, we will use SwiftUI views, wrapped in a `UIHostingController`. However, you can build your UI entirely in UIKit as well.

Here's how the `ShareViewController`'s implementation looks like.

```swift
import UIKit
import Social
import StreamChat
import SwiftUI
import CoreServices

class ShareViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let userCredentials = UserDefaults.shared.currentUser else {
            self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
            return
        }

        self.view.backgroundColor = .systemBackground
        let demoShareView = UIHostingController(
            rootView: DemoShareView(
                userCredentials: userCredentials,
                extensionContext: self.extensionContext
            )
        ).view!

        demoShareView.frame = view.frame
        self.view.addSubview(demoShareView)
    }
}
```

First, we try to fetch the current user. Here `UserDefaults` are used for simplicity. However, in your app, you should store the currently logged in user in a more secure storage, such as the keychain.

Next, we create a `UIHostingController` with a root view called `DemoShareView`, and we add this as a subview to the view controller's view.

#### DemoShareView

Let's check the `DemoShareView` implementation next.

```swift
struct DemoShareView: View {

    @StateObject var viewModel: DemoShareViewModel

    init(
        userCredentials: UserCredentials,
        extensionContext: NSExtensionContext?
    ) {
        let vm = DemoShareViewModel(
            userCredentials: userCredentials,
            extensionContext: extensionContext
        )
        _viewModel = StateObject(wrappedValue: vm)
    }

    var body: some View {
        NavigationStack {
            VStack(alignment: .center) {
                if viewModel.images.count == 1 {
                    ImageToShareView(image: viewModel.images[0])
                } else {
                    ScrollView(.horizontal) {
                        HStack {
                            ForEach(viewModel.images, id: \.self) { image in
                                ImageToShareView(image: image, contentMode: .fill)
                            }
                        }
                    }
                }

                TextField("Write a message...", text: $viewModel.text)
                    .padding(.vertical)

                HStack {
                    if viewModel.channels.isEmpty {
                        ProgressView()
                    } else {
                        Text("Select a channel")
                            .font(.subheadline)
                        Spacer()
                    }
                }

                ShareChannelsView(viewModel: viewModel)
            }
            .padding()
            .navigationTitle("Send to")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel", action: viewModel.dismissShareSheet)
                }
                ToolbarItem(placement: .confirmationAction) {
                    if viewModel.loading {
                        ProgressView()
                    } else {
                        Button("Send") {
                            Task {
                                do {
                                    try await viewModel.sendMessage()
                                } catch {
                                    viewModel.dismissShareSheet()
                                }
                            }
                        }
                        .disabled(viewModel.selectedChannel == nil)
                    }
                }
            }
        }
        .allowsHitTesting(!viewModel.loading)
    }
}
```

It's a standard SwiftUI implementation for the UI shown above. The `toolbar` modifier provides `Cancel` and `Send` buttons using semantic placements (`.cancellationAction` and `.confirmationAction`), with `.navigationTitle` centering the title automatically.

We are also using few helper views here:

```swift
struct ImageToShareView: View {

    private let imageHeight: CGFloat = 180

    var image: UIImage
    var contentMode: ContentMode = .fit

    var body: some View {
        Image(uiImage: image)
            .resizable()
            .aspectRatio(contentMode: contentMode)
            .frame(height: imageHeight)
            .clipShape(.rect(cornerRadius: 8))
    }

}

struct ShareChannelsView: View {

    @ObservedObject var viewModel: DemoShareViewModel

    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading, spacing: 8) {
                ForEach(viewModel.channels) { channel in
                    Button {
                        viewModel.channelTapped(channel)
                    } label: {
                        HStack {
                            ChatChannelAvatarViewRepresentable(
                                channel: channel,
                                currentUserId: viewModel.currentUserId
                            )
                            .frame(width: 64, height: 64)

                            Text(channel.name ?? channel.id)
                                .bold()
                                .lineLimit(1)
                                .foregroundColor(.primary)
                            Spacer()
                            if viewModel.selectedChannel == channel {
                                Image(systemName: "checkmark")
                            }
                        }
                    }
                }
            }
        }
    }

}

struct ChatChannelAvatarViewRepresentable: UIViewRepresentable {
    var channel: ChatChannel
    var currentUserId: UserId?

    func makeUIView(context: Context) -> ChatChannelAvatarView {
        ChatChannelAvatarView()
    }

    func updateUIView(_ uiView: ChatChannelAvatarView, context: Context) {
        uiView.content = (channel: channel, currentUserId: currentUserId)
    }
}
```

Next, let's see the implementation of the `DemoShareViewModel`, that handles the loading of the selected image and sends it to Stream's chat.

The view model uses the state layer (`ChannelList` and `Chat`) which provides native async/await APIs. This means we don't need any manual async wrappers.

```swift
import Combine
import CoreServices
import UIKit
import StreamChat
import Social

@MainActor
class DemoShareViewModel: ObservableObject {

    private let chatClient: ChatClient
    private let userCredentials: UserCredentials
    private var channelList: ChannelList?
    private var extensionContext: NSExtensionContext?
    private var imageURLs = [URL]() {
        didSet {
            images = imageURLs.compactMap { url in
                if let data = try? Data(contentsOf: url),
                   let image = UIImage(data: data) {
                    return image
                }
                return nil
            }
        }
    }

    var currentUserId: UserId? {
        chatClient.currentUserId
    }

    @Published var channels: [ChatChannel] = []
    @Published var text = ""
    @Published var images = [UIImage]()
    @Published var selectedChannel: ChatChannel?
    @Published var loading = false

    init(
        userCredentials: UserCredentials,
        extensionContext: NSExtensionContext?
    ) {
        var config = ChatClientConfig(apiKeyString: "<# Your API Key Here #>")
        config.isClientInActiveMode = true
        config.applicationGroupIdentifier = "<# Your App Group Identifier #>"

        chatClient = ChatClient(config: config)
        self.userCredentials = userCredentials
        self.extensionContext = extensionContext
        loadChannels()
        loadImages()
    }

    func sendMessage() async throws {
        guard let cid = selectedChannel?.cid else {
            throw ClientError.Unexpected("No channel selected")
        }
        loading = true
        let chat = chatClient.makeChat(for: cid)
        try await chat.get(watch: false)

        let attachmentPayloads = try await withThrowingTaskGroup(of: AnyAttachmentPayload.self) { group in
            for url in imageURLs {
                group.addTask {
                    let uploaded = try await chat.uploadAttachment(with: url, type: .image)
                    let file = try AttachmentFile(url: url)
                    return AnyAttachmentPayload(payload: ImageAttachmentPayload(
                        title: nil,
                        imageRemoteURL: uploaded.remoteURL,
                        file: file
                    ))
                }
            }
            var results = [AnyAttachmentPayload]()
            for try await payload in group {
                results.append(payload)
            }
            return results
        }

        try await chat.sendMessage(with: text, attachments: attachmentPayloads)
        dismissShareSheet()
    }

    func channelTapped(_ channel: ChatChannel) {
        if selectedChannel == channel {
            selectedChannel = nil
        } else {
            selectedChannel = channel
        }
    }

    func dismissShareSheet() {
        loading = false
        extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
    }

    // MARK: - private

    private func loadItem(from itemProvider: NSItemProvider, type: String) async throws -> NSSecureCoding {
        try await withCheckedThrowingContinuation { continuation in
            itemProvider.loadItem(forTypeIdentifier: type) { item, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else if let item = item {
                    continuation.resume(returning: item)
                } else {
                    continuation.resume(throwing: ClientError.Unknown())
                }
            }
        }
    }

    private func loadImages() {
        Task {
            let inputItems = extensionContext?.inputItems
            var urls = [URL]()
            for inputItem in (inputItems ?? []) {
                if let extensionItem = inputItem as? NSExtensionItem {
                    for itemProvider in (extensionItem.attachments ?? []) {
                        if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
                            let item = try await loadItem(from: itemProvider, type: kUTTypeImage as String)
                            if let item = item as? URL {
                                urls.append(item)
                            }
                        }
                    }
                }
            }
            self.imageURLs = urls
        }
    }

    private func loadChannels() {
        Task {
            try await chatClient.connectUser(
                userInfo: userCredentials.userInfo,
                token: userCredentials.token
            )
            let query = ChannelListQuery(
                filter: .containMembers(userIds: [chatClient.currentUserId ?? ""])
            )
            let list = chatClient.makeChannelList(with: query)
            self.channelList = list
            try await list.get()
            channels = list.state.channels
        }
    }
}
```

When we create the view model, we connect the user using the native async `connectUser` method and use the `ChannelList` state layer to fetch channels. For sending messages, we use the `Chat` state layer which provides native async methods for uploading attachments and sending messages.

Note that in this example, for simplicity, we are not paginating through the channels. You can do that by using `channelList.loadMoreChannels()`.

The `sendMessage` method uses `chat.sendMessage(with:attachments:)` which is async — when it returns, the message has been sent, so we can dismiss the share sheet directly.

With that, image attachments can successfully be sent to Stream chat from a Share extension.

#### Donating intents

If you want to have your extension appear in the suggestions of the share sheet, you should donate a `INSendMessage` intent to `SiriKit`, when you send a message.

You can find more details about this [here](https://developer.apple.com/documentation/foundation/app_extension_support/supporting_suggestions_in_your_app_s_share_extension).


---

This page was last updated at 2026-06-09T08:45:31.175Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/ios/guides/share-extension/](https://getstream.io/chat/docs/sdk/ios/guides/share-extension/).