SwiftUI - Ignoring async call-back and show next view issue - swift

I made 30 seconds video of issue I am describing below:
I have UserService class which is a part of SceneDelegate and injected it to my first displayed LoginView:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var userService = UserService()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = LoginView().environmentObject(userService)
...
In the LoginView I have navigation link which I trigger via async callback programatically:
NavigationLink(destination: WelcomeView(), isActive: $showWelcomeView) { EmptyView() }
When I get user from the userService I show WelcomeView
struct WelcomeView: View {
#EnvironmentObject var userService: UserService
var body: some View {
VStack {
CircleImage(image: userService.user.image!)
.offset(y: -220)
.frame(height: 140)
.frame(width: 140)
...
as you see my userService is an #EnvironmentObject.
First time I show WelcomeView from navigation link all works good, but when I pop back to LoginView and login again to push WelcomeView I see this error:
Fatal error: Unexpectedly found nil while unwrapping an Optional value:
By some reason my userService.user.image became nil after I pop back to previous Login view and push Welcome view again.
Then I tried to debug it and found out that somehow my async call back here is ignored. You can see I make two async calls I've added a comment below where I never get callback for the second run:
func getUser(with email: String, completion: #escaping (UserFetchResult) -> Void) {
let findUsers:PFQuery = PFUser.query()!
findUsers.whereKey("email", equalTo: email)
findUsers.findObjectsInBackground { (objects, error) in
if error == nil {
guard let firstFoundUser = objects?.first as? PFUser else {
print("Get users error")
completion(.failure)
return
}
self.user = User(pfUser: firstFoundUser)
// IGNORED THIS CALL AND DISPLAY WelcomeView without waiting completion handler.
self.user.avatar?.getDataInBackground(block: { (data, error) in
if error == nil {
if let unwrappedData = data {
if let unwrappedUIImage = UIImage(data: unwrappedData) {
self.user.image = Image(uiImage: unwrappedUIImage)
}
}
}
if self.user.image == nil {
self.user.image = Image("ManPlaceholderAvatar")
}
completion(.success(data: self.user))
})
} else {
print("Get users error")
completion(.failure)
}
}
}
You can check debug details in this video.

Related

SwiftUI published variable not updating view

Context: I am trying to use firebase authentication and firestore to get the user's data. The problem I am running into is that the views are presented before the data is completely fetched and that obviously causes the app to crash. That being said, I am utilizing the firebase authentication listener in my app delegate to ensure the user is authenticated before fetching the users' data (which is also done in the app delegate as shown below)
App delegate snippet
class AppDelegate: NSObject, UIApplicationDelegate {
var handle: AuthStateDidChangeListenerHandle?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if (user != nil){
print("UserAuthentication User authenticated in delegate")
DatabaseDelegate().getUserInfo(UID: user!.uid, withCompletionHandler: {
print("got user data")
})
} else {
print(" UserAuthentication User not authenticated in delegate")
try! Auth.auth().signOut()
}
}
return true
}
This is the database code I am querying and want to listen for when the data is finished loading:
class DatabaseDelegate: ObservableObject {
#Published var userDataLoaded = Bool()
func getUserInfo(UID: String, withCompletionHandler completionHandler: #escaping () -> Void) {
database.collection("Users").document(UID).getDocument { (document, error) in
if let document = document, document.exists {
let data = document.data()!
guard let UID = data["UUID"] as? String else { return }
guard let Name = data["Name"] as? String else { return }
guard let PhoneNumber = data["PhoneNumber"] as? String else { return }
guard let StripeID = data["StripeID"] as? String else { return }
self.userDataLoaded = true
UserData.append(User(UID: UID, Name: Name, PhoneNumber: PhoneNumber, StripeID: StripeID, PurchasedContent: ["TEMP": true]))
completionHandler()
}
}
}
}
And this is the SwiftUI view I want to update based on the userDataLoaded above:
struct MainViewDelegate: View {
//MARK: VARIABLES
#State var showAnimation = true
#State var locationHandler = LocationHandler()
#ObservedObject var databaseDelegate = DatabaseDelegate()
init(){
//Check and enable user location
locationHandler.requestAuthorisation()
}
var body: some View {
VStack {
//Check if data has finished loading, if not, show loading. Listen for changes when the data is finished loading and then present the tab view when it is.
switch databaseDelegate.userDataLoaded {
case true:
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
CheckoutView()
.tabItem {
Label("Services", systemImage: "bag")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gearshape")
}
}
case false:
Text("Loading data")
}
}
}
}
Thank you in advanced. I am new to swiftui (transitioning from uikit) and I've spent too much time trying to solve this silly issue
You're using two different instances of DatabaseDelegate, one in the AppDelegate and one in the MainViewDelegate. The boolean is only updated in app delegate's instance.
Move your auth listener into your DatabaseDelegate.
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
class DatabaseDelegate: ObservableObject {
#Published var userDataLoaded = false
private var handle: AuthStateDidChangeListenerHandle?
init() {
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
// .. etc
self.getUserInfo(...)
}
}
private func getUserInfo(UID: String, withCompletionHandler completionHandler: #escaping () -> Void) {
database.collection("Users").document(UID).getDocument { (document, error) in
// .. etc
self.userDataLoaded = true
}
}
}
You need to use StateObject instead of ObservedObject since you are initializing it internally on the view, instead of injecting it from an external source.
struct MainViewDelegate: View {
#StateObject private var databaseDelegate = DatabaseDelegate()
}
If you want to use ObservedObject, you can create it externally and inject into the view like so:
var databaseDelegate = DatabaseDelegate()
MainViewDelegate(databaseDelegate: databaseDelegate)
struct MainViewDelegate: View {
#ObservedObject var databaseDelegate: DatabaseDelegate
}

SwiftUI Firebase Authentication dismiss view after successfully login

I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.

Using UIApplicationDelegateAdaptor to get callbacks from userDidAcceptCloudKitShareWith not working

I'm trying to get notified when userDidAcceptCloudKitShareWith gets called. Traditionally this was called in the App Delegate but since I am building an iOS 14+ using App as my root object. I couldn't find any documentation out yet as far as how to add userDidAcceptCloudKitShareWith to my App class, so I am using UIApplicationDelegateAdaptor to use an App Delegate class, however it doesn't seem like userDidAcceptCloudKitShareWith is ever getting called?
import SwiftUI
import CloudKit
// Our observable object class
class ShareDataStore: ObservableObject {
static let shared = ShareDataStore()
#Published var didRecieveShare = false
#Published var shareInfo = ""
}
#main
struct SocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
let container = CKContainer(identifier: "iCloud.com.TestApp")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("did finish launching called")
return true
}
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("delegate callback called!! ")
acceptShare(metadata: cloudKitShareMetadata) { result in
switch result {
case .success(let recordID):
print("successful share!")
ShareDataStore.shared.didRecieveShare = true
ShareDataStore.shared.shareInfo = recordID.recordName
case .failure(let error):
print("failure in share = \(error)")
}
} }
func acceptShare(metadata: CKShare.Metadata,
completion: #escaping (Result<CKRecord.ID, Error>) -> Void) {
// Create a reference to the share's container so the operation
// executes in the correct context.
let container = CKContainer(identifier: metadata.containerIdentifier)
// Create the operation using the metadata the caller provides.
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
var rootRecordID: CKRecord.ID!
// If CloudKit accepts the share, cache the root record's ID.
// The completion closure handles any errors.
operation.perShareCompletionBlock = { metadata, share, error in
if let _ = share, error == nil {
rootRecordID = metadata.rootRecordID
}
}
// If the operation fails, return the error to the caller.
// Otherwise, return the record ID of the share's root record.
operation.acceptSharesCompletionBlock = { error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(rootRecordID))
}
}
// Set an appropriate QoS and add the operation to the
// container's queue to execute it.
operation.qualityOfService = .utility
container.add(operation)
}
}
Updated based on Asperi's Answer:
import SwiftUI
import CloudKit
class ShareDataStore: ObservableObject {
static let shared = ShareDataStore()
#Published var didRecieveShare = false
#Published var shareInfo = ""
}
#main
struct athlyticSocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let sceneDelegate = MySceneDelegate()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
.withHostingWindow { window in
sceneDelegate.originalDelegate = window.windowScene.delegate
window.windowScene.delegate = sceneDelegate
}
}
}
}
class MySceneDelegate: NSObject, UIWindowSceneDelegate {
let container = CKContainer(identifier: "iCloud.com...")
var originalDelegate: UIWindowSceneDelegate?
var window: UIWindow?
func sceneWillEnterForeground(_ scene: UIScene) {
print("scene is active")
}
func sceneWillResignActive(_ scene: UIScene) {
print("scene will resign active")
}
// forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
originalDelegate?.scene!(scene, willConnectTo: session, options: connectionOptions)
}
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("delegate callback called!! ")
acceptShare(metadata: cloudKitShareMetadata) { result in
switch result {
case .success(let recordID):
print("successful share!")
ShareDataStore.shared.didRecieveShare = true
ShareDataStore.shared.shareInfo = recordID.recordName
case .failure(let error):
print("failure in share = \(error)")
}
}
}
}
extension View {
func withHostingWindow(_ callback: #escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
In Scene-based application the userDidAcceptCloudKitShareWith callback is posted to Scene delegate, but in SwiftUI 2.0 App-based application the scene delegate is used by SwiftUI itself to provide scenePhase events, but does not provide native way to handle topic callback.
The possible approach to solve this is to find a window and inject own scene delegate wrapper, which will handle userDidAcceptCloudKitShareWith and forward others to original SwiftUI delegate (to keep standard SwiftUI events working).
Here is a couple of demo snapshots based on https://stackoverflow.com/a/63276688/12299030 window access (however you can use any other preferable way to get window)
#main
struct athlyticSocialTestAppApp: App {
#StateObject var shareDataStore = ShareDataStore.shared
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let sceneDelegate = MySceneDelegate()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(shareDataStore)
.withHostingWindow { window in
sceneDelegate.originalDelegate = window?.windowScene.delegate
window?.windowScene.delegate = sceneDelegate
}
}
}
}
class MySceneDelegate : NSObject, UIWindowSceneDelegate {
var originalDelegate: UISceneDelegate?
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
// your code here
}
// forward all other UIWindowSceneDelegate/UISceneDelegate callbacks to original, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
originalDelegate?.scene(scene, willConnectTo: session, options: connectionOptions)
}
}
Check out this question that has a lot of useful things to check across several possible answers:
CloudKit CKShare userDidAcceptCloudKitShareWith Never Fires on Mac App
Be sure to add the CKSharingSupported key to your info.plist, and then try putting the userDidAcceptCloudKitShareWith in different places using the answers in the above link (where you put it will depend on what kind of app you're building).

How do I catch a value change that was changed via a function in SwiftUI?

My View don't check that's the value has changed. Can you tell me why or can you help me?
You can see below the important code.
My Class
import SwiftUI
import SwiftyStoreKit
class Foo: ObservableObject {
#Published var Trigger : Bool = false
func VerifyPurchase() {
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productId = "com.musevisions.SwiftyStoreKit.Purchase1"
// Verify the purchase of Consumable or NonConsumable
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let receiptItem):
print("\(productId) is purchased: \(receiptItem)")
self.Trigger.toggle()
case .notPurchased:
print("The user has never purchased \(productId)")
self.Trigger.toggle()
}
case .error(let error):
print("Receipt verification failed: \(error)")
self.Trigger.toggle()
}
}
}
}
In SceneDelegate my important code at sceneDidbecom to trigger the value to true and then if the function completed, then I want that's trigger back to false
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let foo = Foo()
let contentView = ContenView(foo: Foo).environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidBecomeActive(_ scene: UIScene) {
let foo = Foo()
foo.Trigger = true
foo.VerifyPurchase()
}
My View that's doesnt update self when the Value has changed.
struct ContentView: View {
#ObservedObject var foo: Foo
var body: some View {
Text(self.foo.Trigger ? "true" : "false")
}
}
SwiftyStoreKit.verifyReceipt is an asynchronous function and #Published variables must be updated on the main thread.
Try adding DispatchQueue.main.async when you change the Trigger variable in the background:
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
...
DispatchQueue.main.async {
self.Trigger = false
}
}
Also note that variables in Swift are usually lowercased - ie. trigger instead of Trigger.
You're also using two different Foo instances in your project.
I recommend you remove the code from the func sceneDidBecomeActive(_ scene: UIScene) and move it to the .onAppear in the ContentView:
struct ContentView: View {
#ObservedObject var foo: Foo
var body: some View {
Text(self.foo.Trigger ? "true" : "false")
.onAppear {
foo.Trigger = true
foo.VerifyPurchase()
}
}
}
Also instead of
Text(self.foo.Trigger ? "true" : "false")
you can do
Text(String(describing: self.foo.Trigger))

Using ASWebAuthentication in SwiftUI

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?
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()
}
}
}
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()
}
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()
}
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
}
}
}
}