How do you extract App Delegate data in a SwiftUI project? - ios

I have a SwiftUI-Firebase Project and I'm using an App Delegate to handle push notifications. The client side generates a cloud messsaging registration token that I'd like to store in firestore along with the userID and other meta data.
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
tokenData = [
"token": fcmToken ?? ""
]
// Store token in firestore for sending notifications from server in future...
print("dataDict: \(tokenData)")
}
}
So, I need to get the tokenData, user metadata, package it into a custom object, and finally store it in firestore. I'm doing this by first creating a tokenModel:
struct FCMToken: Codable, Identifiable {
#DocumentID var id: String? = UUID().uuidString
var userID: String?
var token: String
var createdAt: Date
enum CodingKeys: String, CodingKey {
case userID
case token
case createdAt
}
}
And then I'd like to pass tokenData from the delagate into my HomeView() so that I can test whether a user is logged in:
#main
struct erisApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
HomeView(fcmTokenData: delegate.tokenData)
}
}
}
If so, I can call a method to firestore inside HomeView() to store my tokenData. The issue is that I'm unable to extract this tokenData out of the delegate. I've pretty comfortable with SwiftUI but quite new to UIKit, Protocol-Delegate pattern, and so on. Can someone guide me on how to achieve the desired result?
Thank you.

To obtain AppDelegate properties in SwiftUI, use #UIApplicationDelegateAdaptor with the ObservableObject protocol and the #Published and #EnvironmentObject wrappers as explained in the Apple Docs here.
Add the ObservableObject protocol to your AppDelegate class. Then declare tokenData as #Published. This assumes tokenData is updated by your AppDelegate extension above.
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
#Published var tokenData: [TokenData] = []
// application functions...
}
Add the #UIApplicationDelegateAdaptor in the App declaration, as you have done already, but modify HomeView like this:
#main
struct erisApp: App {
#UIApplicationDelegateAdaptor var delegate: AppDelegate
var body: some Scene {
WindowGroup {
HomeView()
}
}
}
Set up HomeView() with an #EnvironmentObject and then use delegate.tokenData as needed:
struct HomeView: View {
#EnvironmentObject var delegate: AppDelegate
var body: some View {
Text("Token Data: \(delegate.tokenData)")
}
}

Related

NSInternalInconsistencyException: Firebase crashes SwiftUI app on launch

I am trying to add Firebase auth via Apple Sign-in and have got this error:
crashed due to an uncaught exception `NSInternalInconsistencyException`. Reason: The default FirebaseApp instance must be configured before the default Authinstance can be initialized. One way to ensure this is to call `FirebaseApp.configure()` in the App Delegate's `application(_:didFinishLaunchingWithOptions:)` (or the `#main` struct's initializer in SwiftUI)
This is how I launch Firebase:
import Firebase
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
FirebaseApp.configure()
return true
}
}
I did not install the Firebase package directly, but instead added this package. which I believe added Firebase as one of its dependency. I have checked other solutions but they all rely on CocoaPods while I'm using the Swift Package Manager. Not sure how I can resolve this error.
App calling AppDelegate:
import FirebaseService
import SwiftUI
#main
struct SomeApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#State private var authState = AuthState()
var body: some Scene {
WindowGroup {
switch authState.value {
case .undefined:
ProgressView()
case .authenticated:
ContentView()
case .notAuthenticated:
AuthView()
}
}
}
}
This error message indicates that another part of your app tries to access Firebase Auth before Firebase is initialised in your your app delegate.
Looking at the code of Rebeloper's package which you're using, we can see that it calls Auth.auth().addStateDidChangeListener in line 16 of AuthListener. Since you create an instance of AuthState (another class from Rebeloper's library), this is executed before Firebase is initialised.
Instead of using an undocumented third party library, I would recommend implementing this yourself - especially since it's only a few lines of code.
To see how this is done, check out Getting started with Firebase Auth for Apple platforms - Firebase Fundamentals, which not only explains Firebase Auth and how to handle authentication state in detail, but also includes the corresponding source code.
As an added bonus, this contains a beautiful signup/login form:
For reference, here is the source code for registering an authentication state listener in an ObserableObject:
#MainActor
class AuthenticationViewModel: ObservableObject {
#Published var email = ""
#Published var password = ""
#Published var confirmPassword = ""
#Published var flow: AuthenticationFlow = .login
#Published var isValid = false
#Published var authenticationState: AuthenticationState = .unauthenticated
#Published var errorMessage = ""
#Published var user: User?
#Published var displayName = ""
init() {
registerAuthStateHandler()
// ...
}
private var authStateHandler: AuthStateDidChangeListenerHandle?
func registerAuthStateHandler() {
if authStateHandler == nil {
authStateHandler = Auth.auth().addStateDidChangeListener { auth, user in
self.user = user
self.authenticationState = user == nil ? .unauthenticated : .authenticated
self.displayName = user?.email ?? ""
}
}
}
// ...
}

SwiftUI not updating with Bluetooth events that change object variables

I’m playing around with some Bluetooth objects and having issues getting SwiftUI to update the view.
Essentially I have a CBCentralManagerDelegate object (btManager) that has a published array of CBPeripheralDelegate objects (btPeripheral) that store their state in variables that get updated when the subscribed Bluetooth messages get processed. However the SwiftUI display (a row in a List) doesn’t update immediately but usually when something else happens to trigger an update (e.g. a different button press).
Im guessing it’s to do with the asynchronous way the peripheral’s delegate methods get called, but despite both classes being ObservableObjects and the array in the manager being #Published it doesn’t update the display straight away.
Any tips would be welcome!
Difficult to show all the code, but the gist of it is:
class BTDataManager: NSObject, ObservableObject, CBCentralManagerDelegate {
#Published var peripherals: [BTPeripheral] = []
private var centralManager: CBCentralManager!
// CBCentralManagerDelegate methods
}
class BTPeripheral: NSObject, ObservableObject, Identifiable, CBPeripheralDelegate {
var manager: BTDataManager?
var peripheral: CBPeripheral?
var RSSI: Int?
var data = "Test"
var id = UUID()
// CBPeripheralDelegate methods
}
So for example when a peripheral delegate method is called and the data var is updated, the SwiftUI view doesn't immediately update.
My ListView is:
struct PeripheralList: View {
#EnvironmentObject var modelData: BTDataManager
var body: some View {
List{
ForEach (modelData.peripherals){ peripheral in
PeripheralDetailRow(peripheral: peripheral)
.environmentObject(modelData)
}
}
}
}
struct PeripheralDetailRow: View {
#EnvironmentObject var modelData: BTDataManager
var peripheral: BTPeripheral
var index: Int {
modelData.peripherals.firstIndex(where: { $0.id == peripheral.id })!
}
var body: some View {
VStack {
Text(peripheral.id.uuidString.suffix(5))
Text(modelData.peripherals[index].data)
.font(caption)
}
}
When a peripheral's delegate function is fired and updates the data var, then the view doesn't update until something else triggers a refresh.
Ok, after typing this out I think I have solved it.
var data becomes #Published var data
In the DetailRow, I have #EnvironmentObject var peripheral: BTPeripheral and I don't need the modelData or index stuff.
In the List View I use .environmentObject(peripheral) to pass it through.
Seems to work now, although I'm not 100% that this is the correct way.

Swift UI - Using UIVIewControllerRepresentable to update logs

The logic I'm trying to create for my logging in the app is:
A ScrollView with a frame to control the height and allow the user to see logs from actions in the app, the logs should be scrollable to scroll up on previous appended logs.
I've created a log view model which allows the log to be set and then appends to a log array and then get.
The logs are set through actions in callbacks from various view controllers and actions from the user.
currently I have the logs being retrieved in the UIViewControllerRepresentable - updateUIViewController method.
The code works for each callback and for the user actions, the problems are: 5a. It's not scrollable to go to the top of the log messages, 5b. The log messages keep showing on the screen as updateUIViewController is continuously being called.
I was trying to think of a way to empty the array after each action, but not sure the best way to go about this.
Code:
LogViewModel:
import UIKit
import SwiftUI
class LogViewModel: ObservableObject{
#Published var mTime: String = ""
#Published var id: String = "#"
#Published var mMessage: String = ""
private var fullLogMessages: [String] = [""]
func setTimeFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter
}
func setTheTime(date: Date){
self.mTime = setTimeFormatter().string(from: date)
}
func getTheTime() -> String{
return self.mTime
}
func setTheMessage(mMessage: String) {
ThreadUtil.runAsyncOnMainThread { [weak self] in
self?.mMessage = mMessage
}
}
func getTheMessage() -> String {
return self.mMessage
}
func getTheFullLogMessage() -> [String] {
let fullLog: String = getTheTime() + " - " + getTheGivenId() + " - " + getTheMessage()
self.fullLogMessages.append(fullLog)
return self.fullLogMessages
}
func setTheGivenId(id: String) {
ThreadUtil.runAsyncOnMainThread { [weak self] in
self?.id = id
}
}
func getTheGivenId() -> String {
return self.id
}
}
Controllers:
In each controller I've created a method like this to set the log messages:
func setTheLogMessages(message: String) {
self.logViewModel.setTheTime(date: date)
self.logViewModel.setTheMessage(mMessage: message)
}
In the view I have the UIViewControllerRepresentable:
struct MyScreenView_UI: UIViewControllerRepresentable{
#ObservedObject var viewModel: myScreenViewModel
#ObservedObject var logViewModel: LogViewModel
#Binding var fullLogMessage: [String]
func makeUIViewController(context: Context) -> some myViewController {
print(#function)
return myViewController(viewModel: viewModel, logViewModel: logViewModel)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
print(#function)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
fullLogMessage = logViewModel.getTheFullLogMessage()
}
}
}
and the ScrollView for the UI:
ScrollView{
VStack(alignment: .leading, content: {
Text("Logs")
.font(.footnote).fontWeight(.medium)
ForEach($fullLogMessage, id: \.self) { $logMessage in
Text(logMessage)
.font(.custom("FONT_NAME", size: 12))
.disabled(true)
}
})
.frame(width: 400, height: 50, alignment: .leading)
}
You haven't provided a Minimal Reproducible Example but here is a simplified version of what seems to be what you are trying to do.
First, add a LogManager that can be created by ANY class or struct
struct LogManager{
var name: String
///Simplified post that takes in the String and uses the name as the source
func postMessage(message: String){
postMessage(message: .init(timestamp: Date(), message: message, source: name))
}
//MARK: Notification
///Sends a Notification with the provided message
func postMessage(message: Message){
NotificationCenter.default.post(name: .logManagerMessage, object: message)
}
///Adds an observer to the manager's notification
func observeMessageNotification(observer: Any, selector: Selector){
NotificationCenter.default.addObserver(observer, selector: selector, name: .logManagerMessage, object: nil)
}
}
Put at class or struct level the declaration for the manager
private let log = LogManager(name: "YourClassStructName")
Then call
log.postMessage(message: "your message here")
When you want to log a message.
In the ViewModel you would
subscribe to the notifications
maintain the array
Like below
class AppLogSampleViewModel: ObservableObject{
static let shared: AppLogSampleViewModel = .init()
private let manager = LogManager(name: "AppLogSampleViewModel")
#Published var messages: [Message] = []
private init(){
//Observe the manager's notification
manager.observeMessageNotification(observer: self, selector: #selector(postMessage(notification:)))
}
///Puts the messages received into the array
#objc
func postMessage(notification: Notification){
if notification.object is Message{
messages.append(notification.object as! Message)
}else{
messages.append(.init(timestamp: Date(), message: "Notification received did not have message", source: "AppLogSampleViewModel :: \(#function)"))
}
}
}
If your View won't be at the highest level you need to call.
let startListening: AppLogSampleViewModel = .shared
In the ContentView or AppDelegate so the ViewModel starts listening as quickly as possible. You won't necessarily use it for anything but it needs to be called as soon as you want it to start logging messages.
Only the View that shows the messages uses the instance of the ViewModel.
struct AppLogSampleView: View {
#StateObject var vm: AppLogSampleViewModel = .shared
//Create this variable anywhere in your app
private let log = LogManager(name: "AppLogSampleView")
var body: some View {
List{
Button("post", action: {
//Post like this anywhere in your app
log.postMessage(message: "random from button")
})
DisclosureGroup("log messages"){
ForEach(vm.messages, content: { message in
VStack{
Text(message.timestamp.description)
Text(message.message)
Text(message.source)
}
})
}
}
}
}
Here is the rest of the code you need to get this sample working.
struct AppLogSampleView_Previews: PreviewProvider {
static var previews: some View {
AppLogSampleView()
}
}
extension Notification.Name {
static let logManagerMessage = Notification.Name("logManagerMessage")
}
struct Message: Identifiable{
let id: UUID = .init()
var timestamp: Date
var message: String
var source: String
}
Only the one View that shows the messages needs access to the ViewModel and only one ViewModel subscribes to the notifications.
All the other classes and structs will just create an instance of the manager and call the method to post the messages.
There is no sharing/passing between the classes and structs everybody gets their own instance.
Your manager can have as many or as little methods as you want, mine usually mimics the Logger from osLog with log, info, debug and error methods.
The specific methods call the osLog and 3rd party Analytics Services corresponding methods.
Also, the error method sends a notification that a ViewModel at the top level receives and shows an Alert.
To get all this detail working it takes a lot more code but you have the pattern with the code above.
In your code, in the updateUIViewController you break the single source if truth rule by copying the messages and putting them in another source of truth, right here.
fullLogMessage = logViewModel.getTheFullLogMessage()
This is also done without a check to make sure that you don't go into an infinite loop. Anytime there is code in an update method you should check that the work actually needs to be done. Such as comparing that the new location doesn't already match the old location.
It seems like you made it very complicated. Let's do this with a simple approach.
1. Define what is a Log
Each log should be identifiable
Each log should represent it's creation date
Each log should have a message
struct Log: Equatable, Hashable {
let id = UUID()
let date = Date()
let message: String
}
2. Define where logs should be
Changes in the logs should be observable
Logs should be accessible in any view
import Combine
class LogManager: ObservableObject {
#Published var logs = [Log]()
}
Note: An EnvironmentObject is a good choice for such an object!
3. Define how to show logs
import SwiftUI
extension Log: Identifiable { }
struct ContentView: View {
#EnvironmentObject private var logManager: LogManager
var body: some View {
List(logManager.logs) { log in
HStack {
Text(log.message)
Text(log.date.ISO8601Format()) // Or any format you like
}
}
}
}
This UI is just a simple sample and could be replaced by any other UI
Note: ScrollToTop is automatically enabled in the List
Note: You may want to use a singleton or injected logger because of the nature of the logger
Note: Don't forget to create and pass in the environment object

How to use my #EnvironmentObject in AppDelegate in Swiftui (iOS 15 / Xcode 13.2.1)?

I have a class :
class AppInfos_M: ObservableObject {
#Published var currentUser: User_M = User_M()
#Published var userTo: User_M = User_M()
}
where i declare it from main as environmentObject :
...
#main
struct TestApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
...
#StateObject var appInfos_M = AppInfos_M()
var body: some Scene {
WindowGroup {
LaunchScreen_V()
.environmentObject(appInfos_M)
...
}
}
}
the class works very good in my app. Now i need to modify it from AppDelegate because i need to get appInfos_M.userTo.id when i get a notification. I tried several things but no one works. How can i access it?
In all my views where I need it I declare this way and it works fine but not in AppDelegate, why? :
#EnvironmentObject var appInfos_M: AppInfos_M
Here is one of the tests I tried that did not work:
Note that the 3 small dots (...) are for the useless code to put here.
...
class AppDelegate: NSObject, UIApplicationDelegate {...}
...
#available(iOS 10, *)
extension AppDelegate : UNUserNotificationCenterDelegate {
...
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
#EnvironmentObject var appInfos_M: AppInfos_M
let userInfo = response.notification.request.content.userInfo
appInfos_M.userTo.id = "just for testing here" // <- i get this error : Thread 1: Fatal error: No ObservableObject of type AppInfos_M found. A View.environmentObject(_:) for AppInfos_M may be missing as an ancestor of this view.
...
You can always store AppInfos_M in your AppDelegate Like this
class AppDelegate: NSObject, UIApplicationDelegate {
var appInfos = AppInfos_M()
(...)
}
You can then use it as EnvironmentObject as:
...
#main
struct TestApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
...
var body: some Scene {
WindowGroup {
LaunchScreen_V()
.environmentObject(appDelegate.appInfos)
...
}
}
}

SwiftUI A View.environmentObject(_:) for may be missing as an ancestor of this view.: file

I'm building my first app on IOS using SwiftUI and Firebase for authentication and storage
For login i use the default Firebase UI which is customized through a subclass of the FUIAuthPickerViewController called MyFUIAuthPickerViewController as detailed in https://firebase.google.com/docs/auth/ios/firebaseui
The defaultUI is initialized and shown in the scene delegate file.
// Create the SwiftUI view that provides the window contents.
//let contentView = ContentView()
self.authUI = _createAuthUI()
guard self.authUI != nil else {
print("No authUI")
return
}
self.authUI?.delegate = self
self.authUI?.shouldHideCancelButton = true
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//window.rootViewController = UIHostingController(rootView: contentView)
window.rootViewController = UINavigationController(rootViewController: MyFUIAuthPickerViewController(authUI: authUI!))
self.window = window
window.makeKeyAndVisible()
}
The MyFUIAuthPickerViewController subclass contains not a lot at the moment but will be used to add a default background to the authorization screen
import Foundation
import FirebaseUI
import Firebase
class MyFUIAuthPickerViewController: FUIAuthPickerViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
For tracking if a user is logged in i use an Observable object called Sessionstore. The code i adapted from https://benmcmahen.com/authentication-with-swiftui-and-firebase/ which was using the old style Bindable protocol
import Foundation
import SwiftUI
import Firebase
import Combine
class SessionStore : ObservableObject
{
#Published var user: AppUser?
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// if we have a user, create a new user model
print("Got user: \(user) \(user.displayName!)")
self.user = AppUser(uid: user.uid,displayName: user.displayName, email: user.email)
} else {
// if we don't have a user, set our session to nil
self.user = nil
}
}
}
func signOut () -> Bool {
do {
try Auth.auth().signOut()
print("signed out")
self.user = nil
print("user object set to nil")
return true
} catch {
print("Problem encountered signing the user out")
return false
}
}
}
The environment object is present on my contentview and my scenedelegate
scenedelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate, FUIAuthDelegate{
var window: UIWindow?
var authUI: FUIAuth?
#EnvironmentObject var appSession: SessionStore
contentview
import SwiftUI
import FirebaseUI
struct ContentView: View {
#EnvironmentObject var session: SessionStore
var body: some View {
Group{
if session.user != nil {
Text("Welcome \(session.user!.displayName!)")
} else {
Text("Please login")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
let nav = UINavigationController()
static var previews: some View {
ContentView().environmentObject(SessionStore())
}
}
In an extension on my sceneDelegate i implement the needed firebase functions.
On succesfull login i create a new appuser object which i place in the sessionStore and then change the rootviewcontroller to the contentview passing in the environmentObject
Extension SceneDelegate {
func authUI(_ authUI: FUIAuth, didSignInWith user: User?, error: Error?) {
guard user != nil else {
print("No User")
return
}
print(user!.displayName!)
let user = AppUser(uid: user!.uid,displayName: user?.email,email: user?.displayName)
self.appSession.user = user
let contentView = ContentView()
self.window?.rootViewController = UIHostingController(rootView: contentView.environmentObject(SessionStore()))
self.window?.makeKeyAndVisible()
}
func authPickerViewController(for authUI: FUIAuth) -> FUIAuthPickerViewController {
return MyFUIAuthPickerViewController(authUI: authUI)
}
}
Now when i test my app i get following error after entering my username and password
Fatal error: No ObservableObject of type SessionStore found.
A View.environmentObject(_:) for SessionStore may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-39.4.3/Core/EnvironmentObject.swift, line 55
I suspect this has to do with the fact that de environmentObject is lost in MyFUIAuthPickerViewController between the flow from my sceneDelegate to the ContentView but how do i prevent this from happening ? I need to somehow extend the MyFUIAuthPickerViewController to allow passing of the environmentObject but how ?
Hope my problem is clear and you guys can help.
Your code in the SceneDelegate is let contentView = ContentView() I think it should be something like let contentView = ContentView().environmentObject(SessionStore())
It also seems that you SessionStore is missing var didChange = PassthroughSubject<SessionStore, Never>()
The first lines of your SessionStore should be something like:
import Foundation
import SwiftUI
import Firebase
import Combine
class SessionStore : ObservableObject
{
#Published var user: AppUser? { didSet { self.didChange.send(self) }}
var didChange = PassthroughSubject<SessionStore, Never>()
var handle: AuthStateDidChangeListenerHandle?
You want to make sure that changes are propagating to listeners (subscribers).
And if I'm correct #EnvironmentObject var appSession: SessionStore should not be mentioned in the SceneDelegate

Resources