/ iOS

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`](http://stackoverflow.com/questions/20919616/keep-getting-unbalanced-calls-to-begin-end-appearance-transitions-for-viewcont) 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:

![](https://github.com/martinnormark/ShareByMail/raw/master/ios8-share-extension.gif)

Complete project is also public on GitHub: [**martinnormark/ShareByMail**](https://github.com/martinnormark/ShareByMail)