That is, what is an idiomatic way to do this that is prescribed by Apple? For any suggestion, please explain HOW that should be done and/or provide a link to official guides. This should be a common enough scenario, but I was only able to find workarounds.

Approaching this from the other end, I know that UserDefaults(suiteName:) and Keychain services can be used from the containing app to share information about the authenticated user with the extension, but what if the user installs the app and just jumps right into trying to share content using its extension without ever signing in (or up)?

  1. Ask user to sign in in the containing app? (In a custom view? Extensions are modal by default.)

  2. Re-implement authentication in extension? (Or shared via custom framework? Is this possible?)

  3. Switch to containing app and then back? This doesn't seem to be supported except in Today extension, but the mechanism described in the docs have been used for workarounds (SO threads: 1, 2, 3).


An (ugly) sample implementation of item 2 in this answer using Firebase.

  • I specifically need this for an app and its share extension built atop Firebase, there is even a related email thread, but it seems like an overkill and I assume there is a standard way to do this that I wasn't able to find yet. – toraritte Mar 6 at 15:51  
  • that os primarily opinion-based, but I would store the successful login details in keychain (e.g. token), what your extension can read as well – until the successful login has happened, the extension would show a login button (obscure the actual content for e.g.) which redirect the user into the app to log-in; after the login was a complete success, the extension's content can show the user-related information. – holex Mar 6 at 16:17
  • @holex Thanks for the comment; it made me realize that the phrasing of my question was incorrect. Your suggestion would be item 3 in the question, but how would you implement it? Do you know of any official resources about it? This should be a common enough problem. – toraritte Mar 6 at 16:35  
  • if you want to navigate from extension to the core app, just use URL schemes, quite standard protocol; however there may be no way to open the extension from the app directly, that is supposed to initiated by the user explicitly (by 3D touch on your app's icon) or implicitly (by opening widget page). – holex Mar 6 at 16:43

The steps that worked for me:

  1. Add Firebase to app extension

  2. Set up shared container via App Group (or registered URL scheme etc., not going in details here about those)

  3. Sync login information (or any other data) using UserDefaults


Step 1. Add Firebase to app extension

An app extension is treated as a separate app by Firebase, therefore one needs to it to the main Firebase project.

Steps:

  1. Add new iOS app to your existing Firebase project
  2. Drag the new GoogleService-Info.plist into your extension in Xcode
  3. Add new target to your Podfile
  4. Install dependencies (pod install)
  5. Configure the Firebase application object in your extension

See detailed steps in this SO answer.

Step 2. Set up shared container via App Group

Apps in a Firebase project are isolated from each other. For example, users, who logged in to the project from the app extension, will have to log in the containing app as well. Checking Auth.auth().currentUser will only yield results for the specific context.

The official Apple guide (App Extension Programming Guide: Sharing Data with Your Containing App) shows how to do it, and gives a good explanation with illustrations.

Steps:

  1. Create new App Group and configure iOS app to use it

  2. Enable "App Groups" capability in Xcode for both targets (i.e., extension and containing app)

For example, our main app is "Access News":

enter image description here

The share extension is "Access-News-Uploader":

enter image description here

Step 3. Sync login information (or any other data) using UserDefaults

Make sure to save the user ID in defaults! The users in Firebase are handled in the project level which makes it possible for the users to sign in via any app in the project (e.g., one for the containing app and one for the extension), but these states are only saved for the actual app in use. For example, if the user signs in the share extension and opens the containing app, if the containing app calls Auth.auth().currentUser.uid at any point it will probably yield nil.

To share data using UserDefaults singleton and App Groups follow the steps in any class where you need them:

  1. let defaults = UserDefaults.init(suiteName: "group.your-app-group-id")!

  2. Set default values using one of the UserDefaults.set(...) functions

  3. Query the the values with a specific UserDefaults getter


Example

When our project starts up, the containing app's (Access News) root view controller (NVC) checks the "user-logged-in" bool UserDefaults value (and not Auth.auth().currentUser as that would only show the containing apps Firebase app status).

(Could've just saved the user ID, and check if it is present, instead of using both a Bool and String key.)

//  NVC.swift

import UIKit
import FirebaseAuth

class NVC: UINavigationController {

    /* Step 1 */
    let defaults = UserDefaults.init(suiteName: "group.org.societyfortheblind.access-news-reader-ag")!
    /**********/

    override func viewDidLoad() {
        super.viewDidLoad()

        /* Step 3 */
        if self.defaults.bool(forKey: "user-logged-in") == false {
        /**********/
            let storyboard = UIStoryboard(name: "Main", bundle: .main)
            let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
            loginViewController.navigationItem.hidesBackButton = true
            self.pushViewController(loginViewController, animated: false)
        }
    }

If no user is logged in, LoginViewController is loaded that sets the key to true on successful login. (It is set to false on logout in SessionStartViewController.)

//  ViewController.swift

import UIKit
import FirebaseAuth

class LoginViewController: UIViewController {

    /* Step 1 */
    let defaults = UserDefaults.init(suiteName: "group.org.societyfortheblind.access-news-reader-ag")!
    /**********/

    // ...

    @IBAction func tapSignInButton(_ sender: Any) {

       // ...
       Auth.auth().signIn(withEmail: username.text!, password: password.text!) {
            (user, error) in
            if error != nil {
                // ...
            } else {
                /* Step 2 */
                self.defaults.set(true, forKey: "user-logged-in")

                // !!!
                self.defaults.set(Auth.auth().currentUser.uid, forKey: "user-id")
                /**********/
                self.navigationController?.popViewController(animated: false)
            }
        }
    }
}

In the app extension the key is checked in the main navigation controller, and if no user is logged it, it would load LoginViewController from the containing app.

//  UploaderNavigationViewController.swift

import UIKit
import Firebase

class UploaderNavigationViewController: UINavigationController {

    /* Step 1 */
    let defaults = UserDefaults.init(suiteName: "group.org.societyfortheblind.access-news-reader-ag")!
    /**********/

    override func viewDidLoad() {
        super.viewDidLoad()

        if FirebaseApp.app() == nil {
            FirebaseApp.configure()
        }

        /* Step 3 */
        if self.defaults.bool(forKey: "user-logged-in") == false {
        /**********/
            let storyboard = UIStoryboard(name: "Main", bundle: .main)
            let loginViewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
            loginViewController.navigationItem.hidesBackButton = true
            self.pushViewController(loginViewController, animated: false)
        }
    }

Here is our project at the commit when this has been set up.

I couldn't find any official guidelines, but the solution below did work and also got accepted in App Store. Probably the bottom line is exactly that: (1) it shouldn't crash and (2) should be able to go through the review process.

The solution with [FirebaseUI authentication[(https://github.com/firebase/FirebaseUI-iOS):

animation

The pertinent code parts:

import UIKit
import Social
import Firebase
import FirebaseAuthUI

class ShareViewController: SLComposeServiceViewController {

    var authUI: FUIAuth?

    /* Using shared container to communicate between extension
       and containing app. Keychain would probably work too.
    */
    let defaults = UserDefaults.init(suiteName: "your-app-group")!

    override func presentationAnimationDidFinish() {

        /* https://stackoverflow.com/questions/37910766/
        */
        if FirebaseApp.app() == nil {
            FirebaseApp.configure()
        }

        self.authUI = FUIAuth.defaultAuthUI()
        self.authUI?.delegate = self

        if self.defaults.bool(forKey: "userLoggedIn") == false {
            let fuiSignin     = 
                FUIPasswordSignInViewController(
                    authUI: FUIAuth.defaultAuthUI()!,
                    email: nil)
            let navController = 
                UINavigationController(rootViewController: fuiSignin)

            self.present(navController, animated: true)
        }
    }

/* FirebaseAuthUI delegate to handle sign-in
*/
extension ShareViewController: FUIAuthDelegate {
    func authUI(_ authUI: FUIAuth, didSignInWith user: User?, error: Error?) {
        if error != nil {
            fatalError()
        }
        if user != nil {
            self.defaults.set(true, forKey: "userLoggedIn")
        }
    }
}

Successful sign in also gets remembered via the shared container (i.e., opening the containing app won't ask for login).

The relevant commit in the github project: https://github.com/society-for-the-blind/Access-News-Reader-iOS/commit/e752b1c554f79ef027818db35c11fceb1ae817e0


ISSUES

The first I ran it, the forms appeared, but wouldn't accept any input. Did Product > Clean and Product > Clean Build Folder ..., restarted Xcode and the Simulator, and it worked. It also worked on an old iPad (iOS 10.3.3).

Your Answer