SwiftUI app doesn't run when supporting iOS 13 - ios

App was working fine when the deployment target was 15.2, but then I needed to change the deployment target to iOS 13 to support more devices. Still works fine on iOS 14+, but it crashes with exception Thread 1: signal SIGABRT, when it runs on iOS 13 on the simulator. Console shows no debug information:
CoreSimulator 802.6 - Device: iPhone 11 (iOS 13.0) (6CCC2B89-002B-4E83-8175-C244984BFC46) - Runtime: iOS 13.0 (17A577) - DeviceType: iPhone 11
Message from debugger: Terminated due to signal 9
Here are the related segments of the WeatherApp.swift file code along with a screenshot of the error:
#main
struct WeatherAppWrapper {
static func main() {
if #available(iOS 14.0, *) {
WeatherApp.main()
}
else {
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(SceneDelegate.self))
}
}
}
struct WeatherApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var loginStatus = UserLoginStatus()
#available(iOS 14.0, *)
var body: some Scene {
WindowGroup {
NavigationView{
if Methods.UserAccount.getFromUserDefaults()["username"] != nil {
MainView(loggedIn: $loginStatus.loggedIn)
LoginView(username: "", password: "", loggedIn: $loginStatus.loggedIn)
} else {
LoginView(username: "", password: "", loggedIn: $loginStatus.loggedIn)
MainView(loggedIn: $loginStatus.loggedIn)
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//Some app-specific setup code
...
return true
}
}
}
class UserLoginStatus: ObservableObject {
#Published var loggedIn: Bool {
didSet {
print("The value of loggedIn changed from \(oldValue) to \(loggedIn)")
}
}
init () {
loggedIn = false
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
#StateObject var loginStatus = UserLoginStatus()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let mainView = MainView(loggedIn: $loginStatus.loggedIn)
let loginView = LoginView(username: "", password: "", loggedIn: $loginStatus.loggedIn)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if Methods.UserAccount.getFromUserDefaults()["username"] != nil {
window.rootViewController = UIHostingController(rootView: mainView)
} else {
window.rootViewController = UIHostingController(rootView: loginView)
}
self.window = window
window.makeKeyAndVisible()
}
}
}
Also, here is a screenshot of the Info.plist file, specifically expanding the related area:

The StateObject is available from SwiftUI 2.0 (iOS 14) and is designed to work in View, not in class (like SceneDelegate)
For provided scenario a possible solution is to use logicState as just property at top level (in delegate or main view), but inject it completely into child views (instead of just binding) and observe it internally.
So it should look like
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var loginStatus = UserLoginStatus() // << just hold here
// ...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let mainView = MainView(loginStatus: loginStatus) // << here !!
and
struct MainView: View {
#ObservedObject var loginStatus: UserLoginStatus // << injected here
// ...
}
similar can be done in WeatherApp

According to Apple docs, StateObject is only available on iOS 14+.

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'

How to load initial screen after an async call SwfitUI

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

How to get UIWindow value in iOS 14 #main file?

I can able to access didFinishLaunchingWithOptions by below implementation. But, I need UIWindow variable. I don't know how to get it. I'm using Xcode 12 beta. iOS14, SwiftUI lifecycle.
import SwiftUI
#main
struct SSOKit_DemoApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("hello world!!!")
return true
}
}
From iOS 13 onwards, it's safe to assume that the correct way to obtain a reference to the key window is via UIWindowSceneDelegate.
#main
struct DemoApp: App {
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
[...]
}
iOS 14.7
#main
struct TestApp: App {
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowScene = scene as? UIWindowScene else {
return nil
}
return .init(windowScene: windowScene)
}
}

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

How to write to a SwiftUI environment object from a class (-extension)

Given the following setup:
Environment Variable UserState
class UserState: ObservableObject {
#Published var loggedIn = Auth.auth().currentUser != nil
}
UserState as a variable in SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
//creating the variable
var userState = UserState()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(userState))
}
...
}
I can now perfectly read/write to this variable in SwiftUI views by declaring
struct ProfileTab: View {
#EnvironmentObject var userState: UserState
var body: some View {
// here I could easily read/write to userState
}
}
So far so good. But:
What is the proper way to write to this variable outside of a SwiftUI view? E.g. from a class or class extension.
Example
extension AppDelegate {
func something(loggedIn: Bool) {
// here I would like to access the environment variable. Something like
// userState.loggedIn = loggedIn
}
}
Here is possible approach...
class AppDelegate: UIResponder, UIApplicationDelegate {
//creating the variable in AppDelegate to have it shared
var userState = UserState()
...
so, then you can ...
extension AppDelegate {
func something(loggedIn: Bool) {
// just use it here as now it is own property
self.userState.loggedIn = loggedIn
}
}
and use it in scene delegate via shared application instance
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// safe as it is known the one AppDelegate
let appDelegate = UIApplication.shared.delegate as! AppDelegate
window.rootViewController = UIHostingController(rootView:
ContentView().environmentObject(appDelegate.userState))
}

Resources