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?
Let’s see how can you achieve something like that using the UIActivityItemProvider
class.
UIActivityItemProvider
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)
You can even add some text by appending the activities items: let activityViewController = UIActivityViewController(activityItems: [PhotoActivityItemProvider(photoURL), "Whitesmith!"], applicationActivities: nil)
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.
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!
The Non-Technical Founders survival guide
How to spot, avoid & recover from 7 start-up scuppering traps.
The Non-Technical Founders survival guide: How to spot, avoid & recover from 7 start-up scuppering traps.