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

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

Related

SwiftUI app doesn't run when supporting iOS 13

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+.

sceneDidEnterBackground is not called on iOS, swift

I am implementing the iOS app with coordinator pattern and I need to show some privacy screen when the app enters background.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var coordinator: MainCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = scene as? UIWindowScene else { return }
if let registry = DependencyResolver.shared as? DependencyRegistry {
DependencyGraph.setup(for: registry)
}
let navController = UINavigationController()
navController.setNavigationBarHidden(true, animated: false)
coordinator = MainCoordinator(navigationController: navController)
coordinator?.start()
self.window = UIWindow.init(windowScene: scene)
window?.rootViewController = navController
window?.makeKeyAndVisible()
}
func sceneWillEnterForeground(_ scene: UIScene) {
hidePrivacyProtectionWindow()
}
func sceneDidEnterBackground(_ scene: UIScene) {
showPrivacyProtectionWindow()
}
...
// MARK: Privacy Protection
private var privacyProtectionWindow: UIWindow?
private func showPrivacyProtectionWindow() {
guard let windowScene = self.window?.windowScene else {
return
}
privacyProtectionWindow = UIWindow(windowScene: windowScene)
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "splash")
privacyProtectionWindow?.rootViewController = vc
privacyProtectionWindow?.windowLevel = .alert + 1
privacyProtectionWindow?.makeKeyAndVisible()
}
private func hidePrivacyProtectionWindow() {
privacyProtectionWindow?.isHidden = true
privacyProtectionWindow = nil
}
}
On entry, the sceneWillEnterForeground() is called. On going to background, sceneDidEnterBackground() is not called. What am I missing?
Found it myself in 10 minutes after posting the question, I used initially wrong methods. This will work for what I'm trying to achieve:
func sceneDidBecomeActive(_ scene: UIScene) {
hidePrivacyProtectionWindow()
}
func sceneWillResignActive(_ scene: UIScene) {
showPrivacyProtectionWindow()
}

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

reason: '-[FaveoHelpdeskPro_Swift.AppDelegate window]: unrecognized selector sent to instance 0x282efc980'

I am getting following error when I am trying to run the project, my project is crashing.
It is giving following error,
FaveoHelpdeskPro_Swift[1400:370341]
-[FaveoHelpdeskPro_Swift.AppDelegate window]: unrecognized selector sent to instance 0x282efc980
and
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'-[FaveoHelpdeskPro_Swift.AppDelegate window]: unrecognized selector
sent to instance 0x282efc980'
and it coming to following screen,
and my SceneDelegate.swift contains already window object,
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 }
}
what is the issue, why it is crashing ?
Here is the complete project
Your AppDelegate does not contain a window reference. Add a window variable in your AppDelegate
var window: UIWindow?
An excerpt from Apple's documentation
This property contains the window used to present the app’s visual content on the device’s main screen.
Implementation of this property is required if your app’s Info.plist file contains the UIMainStoryboardFile key
When you start to init your main ViewController in SceneDelegate in the project and some UI libraries (like SVProgressHUD,...) still point to AppDelegate's window and nothing there, so it crashes. I have met and successfully fixed this problem following this way:
Declare an UIWindow in AppDelegate if it's not there, also declare a static variable for fast access to AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? // HERE, add it if it's not available.
static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate }
//...
}
In SceneDelegate, make a reference of UIWindow object back to AppDelegate:
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? // HERE
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = YOUR_VIEW_CONTROLLER()
window?.makeKeyAndVisible()
AppDelegate.shared.window = window // Connect it HERE!
}
// ....
}
Note: If you just finish step 1, it will not crash but some UI component will display wrong because they cannot determine which is the main Window.
So what I just did is I added a UIWindow var to my AppDelegate like so:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
...
Then in the SceneDelegate, when I am setting the UIWindow property:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.rootViewController = MainTabBarController()
window?.windowScene = windowScene
window?.makeKeyAndVisible()
I also set it for the AppDelegate as well right below like so:
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.window = window
}

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