How to access data in AppDelegate from ObservableObject class in SwiftUI - ios

With the new SwiftUI Lifecycle there normally is no AppDelegate anymore. However, in order to implement Firebase Messaging, it is recommended on the Firebase docs to implement an AppDelegate and attach it using:
#main
struct ExampleApp: SwiftUI.App {
// register app delegate for Firebase setup
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate //<--- HERE
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
Inside this AppDelegate functions one obtains an FCM Token, with which the device is identified to subsequently send it notifications remotely. This token has to get sent to the server. This could be done inside the respective function inside the AppDelegate . However, there is a class AppState (ObservableObject protocol) that handles the user data and writing to the server, so it would make a lot more sense to write pass it to this class (or retrieve in the class from the AppDelegate?) and then push it from there.
How can this be accomplished?
edit: I guess this could be achieved by using a static property in the AppDelegate as described in this answer. Is using statics to access variables globally not bad practice? Is there a other (better) way?

You can do it with the "old way" of accessing the delegate
#MainActor //Required by the app delegate
class AppState: ObservableObject{
lazy var appDelegate: AppDelegate? = {
UIApplication.shared.delegate as? AppDelegate
}()
}
Then you can just use
appDelegate?.yourToken
yourToken referencing a property in the delegate

Related

Trouble adding `UNUserNotificationCenterDelegate` in a SwiftUI app

My problem is that I don't understand how to resolve this warning: ⚠️ Warning: Instance will be immediately deallocated because property 'delegate' is 'weak'
Swift docs state:
You must assign your delegate object to the UNUserNotificationCenter object before your app finishes launching.
In a SwiftUI app that doesn't use AppDelegate, that means I should assign it in App.init().
import SwiftUI
import UserNotifications
class UNCDelegate: NSObject, UNUserNotificationCenterDelegate {
// this is where I think I can respond to the user's tapping on a notification
}
#main
struct MyApp: App {
#StateObject private var dataController: DataController()
init() {
let UNC = UNUserNotificationCenter.current()
UNC.delegate = UNCDelegate() // ⚠️ Warning: Instance will be immediately deallocated because property 'delegate' is 'weak'
// Here I would want to share the UNC across the app by putting it into the SwiftUI environment somehow, so I can schedule notifications whenever I want and still use the handlers in the delegate
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
I wouldn't be surprised if my approach is all wrong. There aren't many examples online that match my use case. My ultimate goal is to open a specific route when user taps on a notification. But with the intention of keeping the question specific, I want to understand how to assign the delegate.
The warning is telling you that the delegate property is weak, so after that line, nothing will be holding a strong reference to the newly created UNCDelegate object, and it will be deallocated.
So just put it in MyApp instead, so that it doesn't get deallocated.
#main
struct MyApp: App {
#StateObject var uncDelegate = UNCDelegate()
init() {
let UNC = UNUserNotificationCenter.current()
UNC.delegate = uncDelegate
}
...
}

Handle Auth state in WindowGroup

I'm new to SwiftUI still and don't really know how to handle best the auth state. If a user is logged in for example i want to redirect him to home screen if not to a certain screen.
I have a service that will tell me if the user is authenticated like: self.authService.isAuthenticated but in my App in WindowGroup i cannot use my service since this is all a struct and i get Cannot use mutating getter on immutable value: 'self' is immutable
I would appreciate a little snippet that can help me solve this here.
My code:
#main
struct MyApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var body: some Scene {
WindowGroup {
if !self.authService.isAuthenticated {
WelcomeView()
} else {
MainView()
}
}
}
}
I suppose you want to handle it just for this time, but i'm proposing you look deeper in SwiftUI bindings and state handlings.
So here we just save the value in a variable in the init since this is getting loaded first.
#main
struct MainApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var isAuth: Bool = false
init() {
isAuth = self.authService.isAuthenticated
}
var body: some Scene {
WindowGroup {
if isAuth {
MainView()
} else {
WelcomeView()
}
}
}
}
The problem is
private lazy var authService: AuthService = dependencies.authService()
(A) SwiftUI rebuilds views in response to, for example, a #StateObject's ObjectWillChangePublisher. Changes an unwatched variable fall silently in a forest without participating in this UI framework, but would be read if you trigger a state change by some other object. Also, I'd guess that service will be rebuilt every time the struct is first built, but I haven't had a use case for this scenario yet, so I don't know.
(B) You've got a mutating variable holding a reference type stored in a value type. As above, store your service as an #StateObject, which is one way SwiftUI gets around this problem of lifetime management.
To get "lazy" loading, call .onAppear { service.load() }.
That said, you have a services / factory container you probably already want to be an #StateObject and injected into the environment. If you store an ObservableObject inside an ObservableObject, the View will react to the outer object only. That object does not link its ObjectWillChangePublisher to inner objects. You will need to either:
(a) individually inject select services into the environment for children to observe
(b) pass those into observable view models that use Combine to subscribe to specific states
(c) use .onReceive and .onChange APIs on Views to link to specific state changes
(C) Conditionals evaluated in App can cause objects stored in that struct to be rebuilt. Good practice is to keep App super clean, like always. Move any conditional logic to a "Root" View for that Scene.

SwiftUI 2 accessing AppDelegate

I did a small prototype which uses Firebase Cloud Messaging and the new SwiftUI 2 app life cycle. I added a custom AppDelegate via
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate and disabled method swizzeling for FCM to work. Everything is working as expected.
Today a colleague asked me, if one can get that delegate object via UIApplication.shared.delegate. So I gave it a shot and noticed that there seems to be two different AppDelegate objects:
po delegate prints:
<MyProject.AppDelegate: 0x6000009183c0>
where as po UIApplication.shared.delegate prints:
▿ Optional<UIApplicationDelegate>
▿ some : <SwiftUI.AppDelegate: 0x600000b58ca0>
Now I'm wondering what is the correct way of accessing the AppDelegate? Should one get it via an #EnvironmentalObject and pass it along all views? Or use the old fashioned way via UIApplication?
Additionally I would like to understand why I end up with two AppDelegates.
Your MyProject.AppDelegate is not direct UIApplicationDelegate, it is transferred via adapter to internal private SwiftUI.AppDelegate, which is real UIApplicationDelegate and which propagates some delegate callback to your instance.
So the solution might be:
Use #EnvironmentalObject if you need access to your MyProject.AppDelegate only in SwiftUI view hierarchy (for this AppDelegate must be confirmed to ObservableObject).
Add and use MyProject.AppDelegate static property which is initialized with object created via adapter, like
class AppDelegate: NSObject, UIApplicationDelegate {
static private(set) var instance: AppDelegate! = nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
AppDelegate.instance = self // << here !!
return true
}
}
now everywhere in your code you can access your delegate via AppDelegate.instance.

Where the AppDelegate get set as a delegate for the application?

Environment, Swift 5.3 and Xcode 12
Normally we do someObject.delegate = self to set the current class as the delegate for some class instance. However, inside AppDelegate there is no such assignment to make it a delegate of the application.
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {}
func applicationWillTerminate(_ aNotification: Notification) {}
}
So where the application's delegate property get set?
#UIApplicationMain attribute means that this class is the application delegate, https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID589.
It's the same as calling the UIApplicationMain(:,:,:,:). If the attribute is NOT used, supply a main.swift file with code at the top level that calls UIApplicationMain(:,:,:,:) (also applicable to NSApplication).
Adding #main seems to have the same effect as #UIApplicationMain. Replace #main with #UIApplicationMain and the app works just fine, tested.

Accessing application delegate variable delays view loading in swift

I am a newbie to Swift and i have started my new project with Swift. I am facing a delay issue while loading a viewcontroller.
On the application delegate i have a variable
var allTerms: [Dictionary<String, AnyObject>]?
This allTerms is populated with data from a local json file of 900Kb. The total json data count is 800.
So far i have a home screen and a second view. From the home screen when i navigate to second screen i need to access this allTerms from the application delegate. Referring to great tutorials,i was able to access the allTerms variable from the application delegate
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate!
self.tableData = (appDelegate.allTerms! as NSArray) as? Array
However doing so this is causing a noticeable delay in loading the secondview , which doesnot happen if i comment the line
self.tableData = (appDelegate.allTerms! as NSArray) as? Array
Appreciate any suggestions!
You might want to create a separate data manager class instead of storing it in the app delegate. You could use something like this:
class DataManager {
var allTerms: [[String:AnyObject]]?
class var sharedInstance: DataManager {
struct Singleton {
static let instance = DataManager()
}
return Singleton.instance
}
// You can access allTerms by calling DataManager.sharedInstance.allTerms
}
This probably won't solve your lag, but it's a good practice to make a DataManager class to store things. I also rewrote your allTerms declaration to use the short form for the dictionary.

Resources