How to load initial screen after an async call SwfitUI - ios

In AppDelegate class I'am calling Auth API and it returns token. And then I set the token in UserDefaults.
However at this time also my HomeView is opening and its .onAppear{} function calling another api. I want to wait completion of Auth API result. After that want to open HomeView. How can I do that?
Also is this the right way for Authorization or getting token?
Here is my AppDelegate, Service, HomeView:
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Service.shared.authorize(userName: "username", password: "password") { (response) in
UserDefaults.standard.set(response.token, forKey: "Token")
}
return true
}
class Service {
func authorize(userName: String, password: String, completion: #escaping(AuthResponse>) -> ()) {
// Here I'm calling auth api
callApiAsync(urlRequest: urlRequest) { (apiResponse) in
completion(apiResponse)
}
}
}
struct HomeView: View {
#ObservedObject var vm = HomeViewModel()
var body: some View {
Text("HomeView")
.onAppear {
self.vm.getList() { response in
}
}
}
}

I do not know where you initialize your UIWindow. AppDelegate / SceneDelegate
My sample for the SceneDelegate but AppDelegate will be similar.
Initialize your window after the request completed.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Service.shared.authorize(userName: "username", password: "password") { (response) in
UserDefaults.standard.set(response.token, forKey: "Token")
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: HomeView())
self.window = window
window.makeKeyAndVisible()
}
}
}

Related

Cannot call value of non-function type 'GIDSignIn'

I'm trying to implement google sign in into my app. But the problem is when i add this line of code to my App.swift file i'm getting an error. I tried to remove the () from the sharedInstance but if i do this i'm getting another error (Value of type 'GIDSignIn' has no member 'clientID'). I'm beginner on swift and swiftUI
The error:
Cannot call value of non-function type 'GIDSignIn'
Line of code:
GIDSignIn.sharedInstance.clientID = FirebaseApp.app()?.options.clientID
My App.swift File:
//
// iGrow_GoalsApp.swift
// iGrow Goals
//
// Created by George Sepetadelis on 3/8/21.
//
import SwiftUI
import Firebase
import FirebaseAuth
import GoogleSignIn
import UserNotifications
#main
struct iGrow_GoalsApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
let viewModel = AppViewModel()
ContentView()
.environmentObject(viewModel)
}
}
}
extension iGrow_GoalsApp {
func setUpAthetication() {
FirebaseApp.configure()
}
}
class AppDelegate : NSObject, UIApplicationDelegate {
var window: UIWindow?
func application(
_ app: UIApplication,
open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
var handled: Bool
handled = GIDSignIn.sharedInstance.handle(url)
if handled {
return true
}
// Handle other custom URL types.
// If not handled by this app, return false.
return false
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
FirebaseApp.configure()
GIDSignIn.sharedInstance.clientID = FirebaseApp.app()?.options.clientID
return true
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
(UIApplication.shared.delegate as? AppDelegate)?.self.window = window
guard let _ = (scene as? UIWindowScene) else { return }
}
}
I was having the same issue until I used this version of GoogleSignIn
pod 'GoogleSignIn', '~> 4.4.0'

Dismissing LoginVC after successful login

In my app the user is directed to a sign up/ log in viewController if they are not signed in. This logic happens in SceneDelegate, I am stuck on trying to present the initial view controller after the user either signs up or logs in. I've tried dismissing the view controller and presenting the root view controller, but neither has worked. Here is the logic in SceneDelegate after their logged in status is checked:
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = OnboardingVC()
After logging in/creating account this is an example of what I've tried after successful login or account creation, but doesn't work:
if let vc = self.onboardingVC?.storyboard?.instantiateViewController(identifier: "JournalVC") as? JournalVC {
self.onboardingVC?.present(vc, animated: true, completion: nil)
}
and
self.onboardingVC?.dismiss(animated: true, completion: nil)
I've looked through some examples, but they all involve AppDelegate, which I am not using, can anyone point me in the right direction?
Let's say, just for the sake of example, that your way of implementing this in the app delegate was:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let rvc = self.window?.rootViewController {
let ud = UserDefaults.standard
if ud.string(forKey: "username") != nil {
print("app delegate: user has logged in")
let vc = rvc.storyboard!.instantiateViewController(withIdentifier: "userHasLoggedIn")
self.window!.rootViewController = vc
}
}
return true
}
}
Then the exactly parallel way to do it in the scene delegate would be:
class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let rvc = self.window?.rootViewController {
let ud = UserDefaults.standard
if ud.string(forKey: "username") != nil {
print("scene delegate: user has logged in")
let vc = rvc.storyboard!.instantiateViewController(withIdentifier: "userHasLoggedIn")
self.window!.rootViewController = vc
}
}
}
}
You will have to write a function in your scene delegate to present the view controller post successful login
In your scene delegate write a function
func launchJourneyViewController() {
let journeyViewController = JourneyViewController()
window?.rootViewController = journeyViewController
window?.makeKeyAndVisible()
}
In your login view controller after successful login , you can write a function
func onLoginSuccess() {
let scene = UIApplication.shared.connectedScenes.first
if let sceneDelegate : SceneDelegate = (scene?.delegate as? SceneDelegate) {
sceneDelegate.launchJourneyViewController()
}
}
This considers the fact that you have only one scene in your application

Update root view controller after user login + iOS 13 and later

Using scene delegate I'm able to set the root view controller.(I'm using Xcode 11.3 and iOS version 13.3 and running my app on iPhone 6+ with iOS 12.4)
What I want is when user login, I need to update the root view controller. For that I did the following
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
static let shared = SceneDelegate()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//some code is here
}
}
#available(iOS 13.0, *)
extension SceneDelegate {
func setRootViewControllerBasedOnLogin() {
if let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogin"), isLoggedIn {
let tabbar = UIStoryboard(name: "Other", bundle: nil).instantiateViewController(withIdentifier: "Tabbar") as! UITabBarController
if var vcs = tabbar.viewControllers {
vcs.remove(at: 2)
tabbar.viewControllers = vcs
}
self.window?.rootViewController = tabbar
} else {
//other stuff
}
}
}
So when user login in to the app I need to remove a tab item from tab bar and update the root view controller.
So I'm doing as follows.
func processLogin() {
//performing login in this method so when login successful we setting root view controller
callLoginAPI { response in
if response.isSuccess {
UserDefaults.standard.set(true, forKey: "isLogin")
if #available(iOS 13.0, *) {
SceneDelegate.shared.setRootViewControllerBasedOnLogin()
} else {
// Fallback on earlier versions
}
}
}
}
When I'm doing this nothing happened. I'm not able to change the root view controller of the app after user successfully login into the app?
Any suggestions? what am I doing wrong?
This is how I managed navigation for both the older version and the new version. So when the user has the latest iOS we need to setup root from sceneDelegate and for older version we need to setup root from appDelegate
AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13, *) {
} else {
setupRoot()
}
return true
}
// MARK: UISceneSession Lifecycle
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
#available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
func setupRoot() {
//Setup Your Root Here
//window?.rootViewController = objNavigationVC
//window?.makeKeyAndVisible()
}
}
SceneDelegate.swift
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window = window
appDelegate.setupRoot()
}
}
func presentYourView(from view: YourViwe) {
if #available(iOS 13, *) {
let mySceneDelegate = view.view.window?.windowScene?.delegate
if let sceneDelegate = mySceneDelegate as? SceneDelegate {
sceneDelegate.changeRootViewController(newViewController())
}
} else {
(UIApplication.shared.delegate as? AppDelegate)?.changeRootViewController(newViewController())
}
}
Update Swift 5+ , Xcode 13+
To change rootViewController depending upon user has logged in or not, here is the full code for SceneDelegate
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).
guard let _ = (scene as? UIWindowScene) else { return }
if UserDefaultHelper.isLoggedIn! {
print("User logged in")
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) // this assumes your storyboard is titled "Main.storyboard"
let yourVC = mainStoryboard.instantiateViewController(withIdentifier: "CustomerMainViewController") as! CustomerMainViewController // inside "YOUR_VC_IDENTIFIER" substitute the Storyboard ID you created in step 2 for the view controller you want to open here. And substitute YourViewController with the name of your view controller, like, for example, ViewController2.
self.window?.rootViewController = yourVC
self.window?.makeKeyAndVisible()
}
else {
print("User Not logged in")
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
The problem with PinkeshGjr's answer is that it discardings the window object provided by the scene. Here's what I feel is a better/simpler approach:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
static var current: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13, *) {
} else {
window = UIWindow();
setUpRoot()
}
return true
}
func setUpRoot() {
window?.rootViewController = ViewController(nibName: nil, bundle: nil)
window?.makeKeyAndVisible()
}
}
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { fatalError() }
let firstWindow = windowScene.windows.first ?? UIWindow(windowScene: windowScene)
AppDelegate.current.window = firstWindow
AppDelegate.current.setUpRoot()
}
}

LogIn Navigation/Segue for iOS 13.2 using Firebase

I am trying to implement SignIn with Google for my iOS 13.2 app using Firebase. How do I implement a segue from the LogIn Page to HomeScreen(a ViewController) as soon as the User has signed in.
There is a method attached to AppDelegate through GIDSignInDelegate which informs us when the user has signed in. I want to segue at that point to the home screen. This code is in the AppDelegate, and I am not able to use AppDelegate's window to load from StoryBoard because of the new SceneDelegate behaviour.
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if (error == nil) {
// Perform any operations on signed in user here.
// ...
print("User signed in")
//place to perform segue
//write code for segue here
}
else {
print("\(error.localizedDescription)")
}
if user != nil
{
guard let authentication = user.authentication else { return }
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
accessToken: authentication.accessToken)
// ...
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
// ...
return
}
// User is signed in
// ...
}
}
}
Expected result : https://stackoverflow.com/a/27136455/6311566
but this is not working in iOS 13.2
OLD WAY
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let rootVC = window?.rootViewController
return true
}
This is because AppDelegate.swift doesn't have window property anymore. Now you must use SceneDelegate.swift to change root view controller. Like shown in this example:
WHAT TO DO NOW
Now, you’ll be moving that code over to your app’s SceneDelegate Swift file:
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).
guard let _ = (scene as? UIWindowScene) else { return }
// Create the root view controller as needed
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
// Create the window. Be sure to use this initializer and not the frame one.
let win = UIWindow(windowScene: winScene)
win.rootViewController = nc
win.makeKeyAndVisible()
window = win
}
// ... the rest of SceneDelegate
}

How do you pass data from a custom url scheme to Views in SwiftUI?

There's no shortage of tips and tutorials on handling custom URL schemes in iOS. What ALL fail to do is actually show you how to pass data parsed from those URLs to your app/views. Yea, I can use a global variable, but that's not the "right" way and plus if you want your Swift view to react to a change in that global variable, you can't.
For example, I have,
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>){
let urlContext = URLContexts.first // Because I don't know how to properly deal with Sets
if let url = urlContext?.url{
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let params = components.queryItems else {
print("Invalid URL or album path missing")
return
}
if let token = params.first(where: { $0.name == "token" })?.value {
print("Token: \(token)")
MyGlobalToken = token
}
}
}
You'll see the MyGlobalToken option in there which works, but I can't respond to a change in that variable. Do I have to do something with the self.window?.rootViewController but I can't find any documentation on what to do. Or do you set up a "notification" so that you view responds? Or is this not implemented yet in SwiftUI?
FWIW I'm new to iOS development.
Here is a great blog to learn about SceneDelegate in iOS 13.
First answer is not a great answer.
If you run your app from a completely inactive state -- i.e. when you run it from XCode aka when it's restarted or not running in the background -- the app will call the func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) method to initialize the scenes infrastructure.
If you are running your app in the background when you point to the URL in Safari the app will call the func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) method.
In order to be thorough, you will have to attempt to get the url parameters in both methods. You can pass this query to the AppDelegate for future use by ViewControllers in both methods as well.
NOTE: If the first ViewController the app opens needs the URL query information you will need to do a few extra steps. To get your ViewControllers to actually update with the information, you will need to use the sceneDidBecomeActive() method for when the app is run from an inactive state/in the background. This method will have to call a method in the ViewController in order for it to pull the variable from the app delegate when the user enters your app. In this case I used viewDidLoad() methods to pull the updated variable from the AppDelegate.
Below is the full code for reference:
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let appDelegate = UIApplication.shared.delegate as! AppDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
appDelegate.query = connectionOptions.urlContexts.first?.url.query ?? "No query"
guard let _ = (scene as? UIWindowScene) else { return }
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
appDelegate.query = URLContexts.first?.url.query
}
func sceneDidDisconnect(_ scene: UIScene) {}
// Only needed if first ViewController needs updated AppDelegate Variable
func sceneDidBecomeActive(_ scene: UIScene) {
self.window?.rootViewController?.viewDidLoad()
}
func sceneWillResignActive(_ scene: UIScene) {}
func sceneWillEnterForeground(_ scene: UIScene) {}
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
I had many trouble in this problem.
I don't know many things in IOS.
But There is no answer. I write it.
If you don't use sceneDelegate, You may use your global variable.
(I don't know why it doesn't work)
For this, I do like below.
Delete scenedelegate.swift.
delete sceneDelegate thing in Info.plist
initialLize check variable
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.check = ""
return true
}
Add lines in Appdelegate for setting global variable.
open func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
//set Global variable
// ... You should add check url for your application ...
UserDefaults.check = "checked"
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if let uservc = self.storyboard?.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController
{
if #available(iOS 13.0, *) {
uservc.isModalInPresentation = true
}
uservc.debugText.text = "This is openUrl State"
self.present(uservc, animated: false) {
}
}
}
Make IntroViewController and MainViewController
IntroViewController is rootviewcontroller.
and MainViewController is second view controller.
if check variable is "checked", Viewdidload in IntroView is not excuted.
and this time is application-open is excuted with safari or chrome.
//IntroViewController
class IntroViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
if UserDefaults.check.elementsEqual("") {
if let uservc =
self.storyboard?.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController
{
if #available(iOS 13.0, *) {
uservc.isModalInPresentation = true
}
uservc.debugText.text = "This is normal State"
self.present(uservc, animated: false) {
}
}
}
}
6.Global Variable
//UserDefaults.swift
import Foundation
fileprivate let keyCheck = "keyCheck"
extension UserDefaults {
static var check: String {
get {
return UserDefaults.standard.string(forKey: keyCheck) ?? ""
}
set(value) {
UserDefaults.standard.set(value, forKey: keyCheck)
UserDefaults.standard.synchronize()
}
}
}
When I use this logic in scenedelegate, It didn't work well.(I couldn't check "check variable".)

Resources