avatar
Published on

Detect Successful Share Sheet Completion in SwiftUI

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

SwiftUI's ShareLink view is the preferred way to present a share sheet, but it has a significant limitation: there's no way to know whether the user actually completed sharing. This can be problematic if you want to reward users for sharing your app, track analytics, unlock features conditionally, or trigger actions based on successful shares.

Some developers attempt to work around this by using simultaneousGesture() to detect taps on ShareLink, but this approach only tells you that the button was tapped, not whether the user followed through with sharing or simply cancelled the sheet. To get true completion feedback and know definitively whether content was shared, you need to bridge to UIKit's UIActivityViewController, which provides a robust completion handler with detailed information about the sharing outcome.

Wrapping UIActivityViewController

We can create a UIViewControllerRepresentable wrapper that exposes the completion handler from UIActivityViewController:

struct ActivityViewControllerView: UIViewControllerRepresentable {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler?

    func makeUIViewController(
        context: UIViewControllerRepresentableContext<ActivityViewControllerView>
    ) -> UIActivityViewController {
        let controller = UIActivityViewController(
            activityItems: activityItems,
            applicationActivities: applicationActivities
        )

        controller.completionWithItemsHandler = { activityType, completed, returnedItems, error in
            self.completionWithItemsHandler?(activityType, completed, returnedItems, error)
        }

        return controller
    }

    func updateUIViewController(
        _ uiViewController: UIActivityViewController,
        context: UIViewControllerRepresentableContext<ActivityViewControllerView>
    ) {}
}

Creating a ShareButton Component

To make this easier to use, we can create a reusable ShareButton view that handles the presentation and provides a cleaner API:

enum ShareButtonResult {
    case shared(UIActivity.ActivityType?)
    case cancelled
    case failed(Error)
}

struct ShareButton<Label>: View where Label: View {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    var completion: @MainActor (ShareButtonResult) -> Void
    @ViewBuilder var label: () -> Label

    @State private var isSharePresented = false

    var body: some View {
        Button {
            isSharePresented = true
        } label: {
            label()
        }
        .sheet(isPresented: $isSharePresented) {
            ActivityViewControllerView(
                activityItems: activityItems,
                applicationActivities: applicationActivities
            ) { activityType, completed, _, error in
                if let error {
                    completion(.failed(error))
                } else if completed {
                    completion(.shared(activityType))
                } else {
                    completion(.cancelled)
                }
            }
        }
    }
}

Note that it's unusual for a SwiftUI view to use a completion handler rather than state binding. This is a pragmatic workaround since we're bridging to UIKit's UIActivityViewController, which itself uses completion handlers.

Understanding the Completion Parameters

The UIActivityViewController.CompletionWithItemsHandler provides four parameters that help you understand what happened:

  • activityType: An optional UIActivity.ActivityType indicating which activity was used (e.g., .message, .mail, .copyToPasteboard, .postToFacebook). This is nil if the user cancelled.
  • completed: A Boolean indicating whether the activity was completed successfully.
  • returnedItems: Optional items returned by the activity. Most activities don't return items, so this is often nil.
  • error: An optional error if the activity failed.

By examining the activityType, you can track which sharing methods users prefer, helping you optimize your app's sharing features or understand user behavior patterns.

Using the ShareButton

Here's how to use the ShareButton in your app:

struct ContentView: View {
    @State private var shareCount = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("Shares: \(shareCount)")
                .font(.headline)

            ShareButton(
                activityItems: ["Check out this awesome app!"],
                completion: { result in
                    switch result {
                    case .shared(let activityType):
                        shareCount += 1
                        print("Shared via: \(activityType?.rawValue ?? "unknown")")
                    case .cancelled:
                        print("User cancelled sharing")
                    case .failed(let error):
                        print("Share failed: \(error.localizedDescription)")
                    }
                },
                label: {
                    Label("Share App", systemImage: "square.and.arrow.up")
                }
            )
        }
        .padding()
    }
}

With this approach, you can track successful shares, reward users, update analytics, or trigger any other action based on whether the user actually completed the share action.