Present iOS 8 Share Extension as Modal View

I was having a little fun creating a share extension for iOS 8 in Swift. One problem I quickly stumpled upon was the need to get rid of the storyboard so that I could just setup the view inside the view controller.

Share extensions, like the other type of extensions, describes its point of entry inside the extension target's Info.plist. By default it looks something like this:

<key>NSExtension</key>  
    <dict>
        <key>NSExtensionMainStoryboard</key>
        <string>MainInterface</string>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.share-services</string>
    </dict>
</key>  

And the extension target comes default with a Main.storyboard that is also defined in the Info tab of the extension target. When I say extension target, it is the part of the project that defines the context of the extension:

It turns out there's a key called NSExtensionPrincipalClass that you can use instead of the default NSExtensionMainStoryboard and have the extension run the view controller which class name you should add to the <string> element to set the value for the key. You end up with the above looking like this now:

<key>NSExtension</key>  
    <dict>
        <key>NSExtensionPrincipalClass</key>
        <string>EntryViewController</string>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.share-services</string>
    </dict>
</key>  

But in Swift this is not going to cut it. It turns out thereĆøs some weird module naming going on so you'll get an error when running the code.

To fix it you need to add @objc(EntryViewController) at the top of your EntryViewController file:

import UIKit

@objc(EntryViewController)

class EntryViewController : UIViewController {  
}

Running the extension now will work, but you won't see anything because nothing is actually contained in the view and the background is transparent.

One of the other problems I initially stumpled upon was a way to present the view controller modally and animate up from the bottom. I tried to use this EntryViewController as merely the entry point and then in viewDidLoad present a main view controller as a modal view. And it actually worked, but I was getting some nasty unbalanced call to begin/end appearance warnings in the log so I found a better approach.

Fake the modal presentation

I wanted my share extension to be contained in a UINavigationController, so I made my EntryViewController inherit UINavigationController and then in the init methods I hard-coded the rootViewController so that it would automatically be contained when run:

import UIKit

@objc(EntryViewController)

class EntryViewController : UINavigationController {

    override init() {
        super.init(rootViewController: ShareViewController())
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
}

But this won't animate the presentation of the view. It will just appear instantly which feels a little clunky and aggressive. To take the presentation, you need to do that in viewWillAppear and start the animation:

override func viewWillAppear(animated: Bool) {  
    super.viewWillAppear(animated)

    self.view.transform = CGAffineTransformMakeTranslation(0, self.view.frame.size.height)

    UIView.animateWithDuration(0.25, animations: { () -> Void in
        self.view.transform = CGAffineTransformIdentity
    })
}

This makes the view appear in full screen and it will appear just as when you call self.presentViewController(vc, animated: true, completion: nil) from any other view controller.

Dismiss view controller animated

When the user wants to cancel or submits the content for sharing inside your extension, you need to tell the extension that you're done but by default your view will just get hidden without any easy transition like you'd expect.

Just as you needed to manually animate the presentation of the view, you also need to animate the dismissal of the view.

Below is the full example of the view controller being contained within the navigation controller that acts as the entry point of the extension:

import UIKit  
import Social

class ShareViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor.whiteColor()
        self.navigationItem.title = "Share this"

        self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Cancel, target: self, action: "cancelButtonTapped:")
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Save, target: self, action: "saveButtonTapped:")
    }

    func saveButtonTapped(sender: UIBarButtonItem) {
        self.hideExtensionWithCompletionHandler({ (Bool) -> Void in
            self.extensionContext!.completeRequestReturningItems(nil, completionHandler: nil)
        })
    }

    func cancelButtonTapped(sender: UIBarButtonItem) {
        self.hideExtensionWithCompletionHandler({ (Bool) -> Void in
            self.extensionContext!.cancelRequestWithError(NSError())
        })
    }

    func hideExtensionWithCompletionHandler(completion:(Bool) -> Void) {
        UIView.animateWithDuration(0.20, animations: { () -> Void in
            self.navigationController!.view.transform = CGAffineTransformMakeTranslation(0, self.navigationController!.view.frame.size.height)
        },
        completion)
    }
}

Notice how the completion handler of the animation calls different methods on the extensionContext depending on the action taken by the user.

The end result looks like this:

Complete project is also public on GitHub: martinnormark/ShareByMail

Martin H. Normark

Product and UX Hacker. Web and iOS developer.

Subscribe to Martin Normark's Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!