UIActivityViewController (Part 2): Building a custom UIActivity ("Open In...") in Swift


Building a Custom UIActivity

You'll notice that there was a parameter that was ignored in the last part of this series when creating and presenting a UIActivityViewController and that was applicationActivities, which was always being set to nil. It's important to note the parameter doesn't specify the built-in activity types to display, which are all included at instantiation (and must be subsequently excluded where required), but instead enables custom UIActivity instances to be included.

Custom activities are created by subclassing UIActivity and overriding the following methods:
  • activityType -> String?
  • activityTitle  -> String?
  • activityImage  -> UIImage?
  • canPerformWithActivityItems: -> Bool 
  • prepareWithActivityItems:
  • activityCategory -> UIActivityCategory
  • activityViewController -> UIViewController?
  • performActivity
Half of the methods return optionals and the final two are an either/or choice. The type, title and image methods set the text and the appearance of the icon, along with the string (activityType) that can be used to exclude the activity.

The method that starts the ball rolling is canPerformWithActivityItems(), which returns a Bool to confirm whether or not the activity can be used, and in turn be displayed in the activity popover. Once confirmed, we must be able to respond to the prepareWithActivityItems() method and finally performActivity() or provide a UIViewController in response to the activityViewController() call.

The activityCategory meanwhile establishes whether the icon we provide will be display on the top (Share) row or the second (Action) row of the UIActivityViewController popover.

Open In ...

Here is a subclass created using the knowledge gained from presenting a UIDocumentInteractionController to add "Open In..." functionality. It does this very simply by replacing the current popover with a UIDocumentInteractionController popover when selected.
let UIActivityTypeOpenIn = "OpenIn"

class OpenIn: UIActivity {
    var docController: UIDocumentInteractionController!
    weak var barButton: UIBarButtonItem!
    
    // it's necessary to know which button the UIActivityViewController originated from
    init(barButton barB: UIBarButtonItem) {
        self.barButton = barB
    }
    // this provides a way of excluding the activity if we wish
    override func activityType() -> String? {
        return UIActivityTypeOpenIn
    }
    // provides title within popover
    override func activityTitle() -> String? {
        // your activity title here
        return "Open In..."
    }
    // in real use you would provide image here at required size (see class reference)
    override func activityImage() -> UIImage? {
        // return your icon image here, for simplicity I've used nil
        return nil
    }
    // specify whether Action or Share (default is Action)
    override class func activityCategory() -> UIActivityCategory {
        // ,Share places the option on the top row
        return .Share
    }
    // confirm the action can be performed
    override func canPerformWithActivityItems(activityItems: [AnyObject]) -> Bool {
        for a in activityItems {
            if a is NSURL  {
                return true
            }
        }
        return false
    }
    // prepare for the activity
    override func prepareWithActivityItems(activityItems: [AnyObject]) {
        for a in activityItems {
            if let url = a as? NSURL  {
                docController = UIDocumentInteractionController(URL: url)
            }
        }
    }
    // perform the activity
    override func performActivity() {
        docController.presentOpenInMenuFromBarButtonItem(barButton, animated: true)
    }
}
Note: I've found that in an iPhone simulator the UIDocumentInteractionController is not displayed, but on a device (iPod touch, running iOS 8.4) all works as expected. It defaults to a sheet on an iPod/iPhone, where on an iPad the current popover is replaced by the UIDocumentInteractionController popover.

View Controller Code

Now for the call to make this happen, which you'll need to wire up to a Bar Button Item in this instance to have work:
class ViewController: UIViewController {
    // link to bar button item
    @IBAction func shareStuff(sender: AnyObject) {
        let open = OpenIn(barButton: sender as! UIBarButtonItem)
        let fileURL = NSBundle.mainBundle().URLForResource("MyFile", withExtension: "txt")!
        // present UIDocumentInteractionController
        let activityController = UIActivityViewController(activityItems: [fileURL], applicationActivities: [open])
        self.presentViewController(activityController, animated: true, completion: nil)
        let presCon = activityController.popoverPresentationController
        presCon?.barButtonItem = sender as! UIBarButtonItem
        presCon?.delegate
    }
    
}
Notice how we first instantiate an OpenIn instance:
let open = OpenIn(barButton: sender as! UIBarButtonItem)
And then pass this to the UIActivityViewController within the applicationActivities parameter.
let activityController = UIActivityViewController(activityItems: [fileURL], applicationActivities: [open])

Conclusion

I should note here that I haven't had the opportunity to test this code in iOS 9 (outside of the simulator), and as we'll see, in the next post of this series, popovers and how they are presented changes in iOS 9. Even if you use the same methods, for example, to display a print dialog then what will be displayed and how it will be display changes. You might request a popover originating from a bar button item and actually see an alert style view in the centre of the page.

How much of this is fixed and final I'm not sure and I'm also aware that as with iOS 8.4 there are differences between what we see on an iPod and what we see in the simulator. So I'm not going to alarm myself if I don't see the expected behaviour just yet. Instead what I'll do in this post and in the one that follows as much as possible is to observe variations in behaviour and suggest workarounds where appropriate.

At the moment the way in which the various controllers are presented varies, some being subclasses of UIViewController and so being presented by the current view controller and others being self reliant when it comes to presentation. It's likely that this will change and things will become increasingly standardised but at the moment we need to cope with inconsistencies and the favoured ways of presentation being in a state of transition. And most of all we need to test on real devices whenever possible and to share the information we gather.


Comments