×
×
whitesmith.co

Control what you're sharing

Ricardo Pereira

Have you ever needed to share an image on iOS? In general, if you want to share a document you might want to use a UIDocumentInteractionController but if you're sharing an image or URL, then you might want to use a UIActivityViewController which is a great timesaver but it's not totally pain-free. For example, if you have a link to a photo and you want to share that photo to some service, you can try the UIActivityViewController to do that which is quite straightforward and you have a lot of examples on the internet like:

let activityViewController = UIActivityViewController(activityItems: [photoURL], applicationActivities: nil)  
present(activityViewController, animated: true)  

but that code will not share the photo, it only shares the URL. For example, if you want to share the actual photo to Instagram, then you need to download it first, instantiate a UIImage object with the downloaded data and add it as an activity item to the UIActivityViewController object. This will work flawlessly if you start the download asynchronously, showing the download progress and presenting the activity view controller after the image is loaded.

Now, what happens if you want to share the downloaded photo only after the user selects an activity?

Activity View Controller
Activity View Controller

Let's see how can you achieve something like that using the UIActivityProvidterItem class.

UIActivityProvidterItem

From the documentation, an UIActivityItemProvider object is a proxy for data passed to an activity view controller. It's an NSOperation that conforms to the UIActivityItemSource protocol. You can subclass it by creating a remote photo provider object and use it to download the necessary data after the user selects any activity. I will call it PhotoActivityItemProvider.

A single PhotoActivityItemProvider object can only return a single photo, so the initialiser should receive the photo URL and retain it as a property of the provider. The URL will be used when the activity view controller requests the data. Usually, you would override the main() method because the provider is an operation but from the documentation, the item() method should be the one to be implemented because that's the method called to generate the item data.

Important notice: the item() method is called on a secondary thread of your app, that means you can block the runtime and wait for any result you need. The system doesn't provide any built-in progress indicator, so if generating the item may take a long time you should plan on providing feedback in your app yourself.

class PhotoActivityItemProvider: UIActivityItemProvider {

    let photoURL: URL
    private var semaphore: DispatchSemaphore?

    init(_ photoURL: URL) {
        self.photoURL = photoURL
        super.init(placeholderItem: UIImage())
    }

    override var item: Any {
        var image: UIImage?
        semaphore = DispatchSemaphore(value: 0)
        let task = URLSession.shared.dataTask(with: photoURL) { data, response, error in
            defer {
                self.semaphore?.signal()
            }
            guard let data = data else {
                return
            }
            image = UIImage(data: data)
        }
        task.resume()
        semaphore?.wait()
        semaphore = nil

        if let image = image {
            return image
        }
        task.cancel()
        return super.item
    }

    override func cancel() {
        semaphore?.signal()
        super.cancel()
    }

}

The initialiser of PhotoActivityItemProvider needs to call the designated initialiser passing a placeholder. The placeholder is called to determine the data type (only the class of the return type is consulted) and that's why you should pass a UIImage because you want to show all the activities that can accept images.

You need to flat the completion block from the URLSession.dataTask:URL because it runs asynchronously and you need to return the UIImage object, so that's why I added a DispatchSemaphore. Here you can add more code to display the download progress to the user. You can even access the view property from the UIActivityViewController object and add a progress bar or a spinner but don't forget to remove it when the operation finishes or the user aborts it.

You can use it like: let activityViewController = UIActivityViewController(activityItems: [PhotoActivityItemProvider(photoURL)], applicationActivities: nil)

Twitter activity example
Twitter activity example

You can even add some text by appending the activities items: let activityViewController = UIActivityViewController(activityItems: [PhotoActivityItemProvider(photoURL), "Whitesmith!"], applicationActivities: nil)

Twitter with text activity example
Twitter with text activity example

ActivityType's

What about returning different types in the item() method for cases like sharing the URL of the photo isn't enough. For example, share the URL if the user selects the Message activity or the Mail activity but it should share the actual photo for other activities.

This can be easily done by using the optional activityType property from the UIActivityItemProvider object. That property returns a UIActivityType, so you can do something like:

class PhotoActivityItemProvider: UIActivityItemProvider {  
    ...

    override var item: Any {
        guard let activityType = self.activityType else {
            return photoURL.absoluteString
        }
        if activityType == .mail || activityType == .message {
            return "The photo link is \(photoURL.absoluteString)."
        }

        ...
    }

The user by selecting a UIActivityType.message it should share the returned text.

Message activity example
Message activity example

If you want to customise the return type by any other known activity type, then you can use the raw value because the UIActivityType conforms to the RawRepresentable protocol of type String. So, for example, if the user selects the Instagram activity, then the activityType.rawValue will return something like "com.instagram.shareextension". You can compare that but be cautious because the Instagram.app can change it anytime.

And that's it, I hope you enjoyed it. It's definitely a nice way use the activity view controller and still charge it with better user experience.
If you have another solution, then don't forget to share it with us!


Subscribe to our newsletter

Would you like to receive more posts of this kind in your Inbox?