4

Having a bit of trouble getting authentication to work from within a SwiftUI view. I’m using ASWebAuthentication and whenever I run I get an error:

Cannot start ASWebAuthenticationSession without providing presentation context. Set presentationContextProvider before calling -start.

I’m creating a ViewController and passing in a reference to the Scene Delegate window based on this stack overflow post but that answer doesn’t seem to be working for me. I’ve also found this reddit post, but I’m a little unclear as to how they were able to initialize the view with the window before the scene delegate’s window is set up.

This is the code I’m using for the SwiftUI view:

import SwiftUI
import AuthenticationServices

struct Spotify: View {
  var body: some View {
    Button(action: {
        self.authWithSpotify()
    }) {
        Text("Authorize Spotify")
    }
  }

  func authWithSpotify() {

    let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
    guard let url = URL(string: authUrlString) else { return }

    let session = ASWebAuthenticationSession(
        url: url,
        callbackURLScheme: "http://redirectexample.com/callback",
        completionHandler: { callback, error in

            guard error == nil, let success = callback else { return }

            let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first

            self.getSpotifyAuthToken(code)
    })

    session.presentationContextProvider = ShimViewController()
    session.start()
  }

  func getSpotifyAuthToken(_ code: URLQueryItem?) {
    // Get Token
  }

}

struct Spotify_Previews: PreviewProvider {
  static var previews: some View {
    Spotify()
  }
}

class ShimViewController: UIViewController, ASWebAuthenticationPresentationContextProviding {
  func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
    return globalPresentationAnchor ?? ASPresentationAnchor()
  }
}

And in the SceneDelegate:

var globalPresentationAnchor: ASPresentationAnchor? = nil

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

    // Use a UIHostingController as window root view controller
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: Spotify())
        self.window = window
        window.makeKeyAndVisible()
    }

    globalPresentationAnchor =  window
}

Any idea how I can make this work?

Ronni
  • 55
  • 1
  • 7
  • Is `globalPresentationAnchor` nil when `presentationAnchor(for session:)` is called? – johnny Feb 12 '20 at 00:34
  • Just doubled-checked and `globalPresentationAnchor` isn’t nil when `presentationAnchor(for session:)` gets called – Ronni Feb 13 '20 at 01:38
  • Just a quick question, Is it possible to customize the navigation bar which appears with the button items, Cancel, reload icon, etc. to some controls of our own? Thanks in advance :) – user2580 May 21 '21 at 07:24

4 Answers4

3

I've run into something similar before when implementing ASWebAuthenticationSession. One thing I didn't realize, is you have to have a strong reference to the session variable. So I would make you session variable a property of your class and see if that fixes the issue. A short snippet of what I mean:

// initialize as a property of the class
var session: ASWebAuthenticationSession?

func authWithSpotify() {
    let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
    guard let url = URL(string: authUrlString) else { return }

    // assign session here
    session = ASWebAuthenticationSession(url: url, callbackURLScheme: "http://redirectexample.com/callback", completionHandler: { callback, error in

            guard error == nil, let success = callback else { return }

            let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first

            self.getSpotifyAuthToken(code)
    })

    session.presentationContextProvider = ShimViewController()
    session.start()
}
johnny
  • 1,434
  • 1
  • 15
  • 26
  • Thanks! There does seem to be an `Instance will be immediately deallocated because property 'presentationContextProvider' is 'weak’` warning but I’ve added `@State var session: ASWebAuthenticationSession?` as a property of the struct and it’s still there (and I’m still getting the error). When I add the property to the struct without `@State` I get `Cannot assign to property: 'self' is immutable` errors that I can seem to resolve. – Ronni Feb 13 '20 at 02:32
  • That error is because you are using a `Struct` not a `Class`. I would try switching it to class to see if that helps at all (won't need `@State` in this case) unless there is a specific reason you're using `Struct`. – johnny Feb 13 '20 at 17:43
  • I guess the specific reason is that thought SwiftUI views had to be structs that conformed to the View protocol? I’m not really sure now to switch this out for a class and haven’t been able to track down any examples other than [this reddit answer](https://www.reddit.com/r/iOSProgramming/comments/cay92l/update_aswebauthenticationsession_and_swiftui/eu8z2w4/) from my OP that uses `final class`, but I wasn’t able to get it working (and am wondering if Previews still work in that scenario?). – Ronni Feb 20 '20 at 05:22
  • Yeah, I saw that reddit post too. It seems like if that doesn't work, your only option would be to mix UIKit with SwiftUI. I don't have a _ton_ of SwiftUI experience but that seems like the only option if the reddit answer didn't solve it for you. I do know that for UIKit, you need to hold onto the session as a property of the class otherwise it won't work. – johnny Feb 20 '20 at 20:44
3

Regarding the reddit post, I got it to work as is. My misunderstanding was that the AuthView isn't being used as an 'interface' View. I created a normal SwiftUI View for my authentication view, and I have a Button with the action creating an instance of the AuthView, and calling the function that handles the session. I'm storing the globalPositionAnchor in an @EnvironmentObject, but you should be able to use it from the global variable as well. Hope this helps!

struct SignedOutView: View {
    @EnvironmentObject var contentManager: ContentManager

    var body: some View {
        VStack {
            Text("Title")
            .font(.largeTitle)

            Spacer()

            Button(action: {AuthProviderView(window: self.contentManager.globalPresentationAnchor!).signIn()}) {
                Text("Sign In")
                    .padding()
                    .foregroundColor(.white)
                    .background(Color.orange)
                    .cornerRadius(CGFloat(5))
                    .font(.headline)
            }.padding()
        }
    }
}
enordlund
  • 71
  • 4
2

Ronni - I ran into the same problem but finally got the ShimController() to work and avoid the warning. I got sucked into the solution but forgot to instantiate the class. Look for my "<<" comments below. Now the auth is working and the callback is firing like clockwork. The only caveat here is I'm authorizing something else - not Spotify.

var session: ASWebAuthenticationSession?
var shimController = ShimViewController() // << instantiate your object here

func authWithSpotify() {
    let authUrlString = "https://accounts.spotify.com/authorize?client_id=\(spotifyID)&response_type=code&redirect_uri=http://redirectexample.com/callback&scope=user-read-private%20user-read-email"
    guard let url = URL(string: authUrlString) else { return }

    // assign session here
    session = ASWebAuthenticationSession(url: url, callbackURLScheme: "http://redirectexample.com/callback", completionHandler: { callback, error in

            guard error == nil, let success = callback else { return }

            let code = NSURLComponents(string: (success.absoluteString))?.queryItems?.filter({ $0.name == "code" }).first

            self.getSpotifyAuthToken(code)
    })

    session.presentationContextProvider = shimController // << then reference it here
    session.start()
}
briangraph
  • 21
  • 1
1

Using .webAuthenticationSession(isPresented:content) modifier in BetterSafariView, you can easily start a web authentication session in SwiftUI. It doesn't need to hook SceneDelegate.

import SwiftUI
import BetterSafariView

struct SpotifyLoginView: View {
    
    @State private var showingSession = false
    
    var body: some View {
        Button("Authorize Spotify") {
            self.showingSession = true
        }
        .webAuthenticationSession(isPresented: $showingSession) {
            WebAuthenticationSession(
                url: URL(string: "https://accounts.spotify.com/authorize")!,
                callbackURLScheme: "myapp"
            ) { callbackURL, error in
                // Handle callbackURL
            }
        }
    }
}
Stleamist
  • 193
  • 2
  • 12
  • Just a quick question, Is it possible to customize the navigation bar which appears with the button items, Cancel, reload icon, etc. to some controls of our own? Thanks in advance :) – user2580 May 21 '21 at 07:23