Using ASWebAuthentication in SwiftUI - swift

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
}
}
}
}

Related

Setup UserDefaults property as Published property in View Model [duplicate]

I have an #ObservedObject in my View:
struct HomeView: View {
#ObservedObject var station = Station()
var body: some View {
Text(self.station.status)
}
which updates text based on a String from Station.status:
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.string(forKey: "status") ?? "OFFLINE" {
didSet {
UserDefaults.standard.set(status, forKey: "status")
}
}
However, I need to change the value of status in my AppDelegate, because that is where I receive my Firebase Cloud Messages:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// Print full message.
let rawType = userInfo["type"]
// CHANGE VALUE OF status HERE
}
But if I change the status UserDefaults value in AppDelegate - it won't update in my view.
How can my #ObservedObjectin my view be notified when status changes?
EDIT: Forgot to mention that the 2.0 beta version of SwiftUI is used in the said example.
Here is possible solution
import Combine
// define key for observing
extension UserDefaults {
#objc dynamic var status: String {
get { string(forKey: "status") ?? "OFFLINE" }
set { setValue(newValue, forKey: "status") }
}
}
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.status {
didSet {
UserDefaults.standard.status = status
}
}
private var cancelable: AnyCancellable?
init() {
cancelable = UserDefaults.standard.publisher(for: \.status)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.status { // avoid cycling !!
self.status = newValue
}
})
}
}
Note: SwiftUI 2.0 allows you to use/observe UserDefaults in view directly via AppStorage, so if you need that status only in view, you can just use
struct SomeView: View {
#AppStorage("status") var status: String = "OFFLINE"
...
I would suggest you to use environment object instead or a combination of both of them if required. Environment objects are basically a global state objects. Thus if you change a published property of your environment object it will reflect your view. To set it up you need to pass the object to your initial view through SceneDelegate and you can work with the state in your whole view hierarchy. This is also the way to pass data across very distant sibling views (or if you have more complex scenario).
Simple Example
In your SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView().environmentObject(GlobalState())
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
The global state should conform ObservableObject. You should put your global variables in there as #Published.
class GlobalState: ObservableObject {
#Published var isLoggedIn: Bool
init(isLoggedIn : Bool) {
self.isLoggedIn = isLoggedIn
}
}
Example of how you publish a variable, not relevant to the already shown example in SceneDelegate
This is then how you can work with your global state inside your view. You need to inject it with the #EnvironmentObject wrapper like this:
struct ContentView: View {
#EnvironmentObject var globalState: GlobalState
var body: some View {
Text("Hello World")
}
}
Now in your case you want to also work with the state in AppDelegate. In order to do this I would suggest you safe the global state variable in your AppDelegate and access it from there in your SceneDelegate before passing to the initial view. To achieve this you should add the following in your AppDelegate:
var globalState : GlobalState!
static func shared() -> AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
Now you can go back to your SceneDelegate and do the following instead of initialising GlobalState directly:
let contentView = ContentView().environmentObject(AppDelegate.shared().globalState)

How to set up Firebase Google SignIn 6.0.2 using SwiftUI 2.0 architecture?

I'm trying to set up the latest GoogleSignIn with SwiftUI. Official Firebase documentation offers outdated approach designed for UIKit, ViewControllers and AppDelegate, so there is simply no way to find out what workaround is needed to make it work. I was able to implement UIApplicationDelegateAdaptor to get access to didFinishLaunchingWithOptions method to configure FirebaseApp and GIDSignIn.
While following this documentation: https://firebase.google.com/docs/auth/ios/google-signin I ultimately stuck on the step 4. It's unclear where do I have to use this code or how to integrate it into SwiftUI paradigm:
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: self) { [unowned self] user, error in
if let error = error {
// ...
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
// ...
}
I'm aware of this guide from Google: https://developers.google.com/identity/sign-in/ios/quick-migration-guide but there is no code example of how to do it properly.
You would probably want to run that code as the result of an action the user takes. After pressing a Button, for example.
The tricky part is going to be that GIDSignIn.sharedInstance.signIn wants a UIViewController for its presenting argument, which isn't necessarily straightforward to get in SwiftUI. You can use a UIViewControllerRepresentable to get a reference to a view controller in the hierarchy that you can present from.
class LoginManager : ObservableObject {
var viewController : UIViewController?
func runLogin() {
guard let viewController = viewController else {
fatalError("No view controller")
}
//Other GIDSignIn code here. Use viewController for the `presenting` argument
}
}
struct DummyViewController : UIViewControllerRepresentable {
var loginManager : LoginManager
func makeUIViewController(context: Context) -> some UIViewController {
let vc = UIViewController()
loginManager.viewController = vc
return vc
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
struct ContentView : View {
#StateObject private var loginManager = LoginManager()
var body: some View {
VStack(spacing: 0) {
Button(action: {
loginManager.runLogin()
}) {
Text("Login")
DummyViewController(loginManager: loginManager)
.frame(width: 0, height: 0)
}
}
}
}

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.

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))

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

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.