Is it possible to use ASWebAuthenticationSession from SwiftUI?

Viewed 3181

I would like to authenticate against an OAuth API usingASWebAuthenticationSession however it doesn't seem to be usable from SwiftUI. This is what I would like to have:

struct ContentView: View: ASWebAuthenticationPresentationContextProviding {
    var body: some View {
        NavigationView {
            VStack {
                Button("Hello World", {
                    // Run oauth flow
                }
            }
        }
        .navigationBarTitle(Text("Greed of Savin"))
        .navigationViewStyle(StackNavigationViewStyle())
    }

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return BungieApi.sharedInstance.presentationAnchor ?? ASPresentationAnchor()
    }
}

}

It requires adopting the protocol ASWebAuthenticationPresentationContextProviding which is not compatible with SwiftUI's Views.

I can get past this by redirecting to a ViewController that can then provides the ASWebAuthenticationPresentationContextProviding, but that adds an additional view/navigation step.

Is there any way to use ASWebAuthenticationSession from SwiftUI without dropping into a ViewController?

2 Answers

I solved this in three parts:

First, capture the window in a global object during setup in SceneDelegate.swift:

var globalPresentationAnchor: ASPresentationAnchor? = nil
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // ...            
        globalPresentationAnchor = window
    }
}

Then, create a small ViewController to provide that window object to the using ASWebAuthenticationSession:

class ShimViewController: UIViewController, ASWebAuthenticationPresentationContextProviding
{
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        // Perhaps I don't need the window object at all, and can just use:
        // return ASPresentationAnchor()
        return globalPresentationAnchor ?? ASPresentationAnchor()
    }
}

Finally, call the authentication API, providing the ShimViewController as the presenter.

    let session = ASWebAuthenticationSession(/**/)
    session.presentationContextProvider = ShimViewController()
    session.start()

Using BetterSafariView, it is possible to use ASWebAuthenticationSession without hooking SceneDelegate in SwiftUI.

import SwiftUI
import BetterSafariView

struct ContentView: View {
    
    @State private var startingWebAuthenticationSession = false
    
    var body: some View {
        Button("Start WebAuthenticationSession") {
            self.startingWebAuthenticationSession = true
        }
        .webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
            WebAuthenticationSession(
                url: URL(string: "https://github.com/login/oauth/authorize?client_id=\(clientID)")!,
                callbackURLScheme: "myapp"
            ) { callbackURL, error in
                print(callbackURL, error)
            }
        }
    }
}
Related