How do I correct this issue with reading an Environment Object? (SwiftUI and working without a SceneDelegate) - firebase

I wanted to write a template for apps that logs into firebase so that I can use it in future projects. I followed a tutorial online found on YouTube :
https://www.youtube.com/watch?v=DotGrYBfCuQ&list=PLBn01m5Vbs4B79bOmI3FL_MFxjXVuDrma&index=2
So the issue I'm facing is that in the video the variable userInfo was instantiated in the SceneDelegate, allowing the coder on YouTube to reference userInfo in his code. I tried doing the same in the AppDelegate and the App Struct. with no avail.
Here is the code in the App Struct:
import SwiftUI
import Firebase
#main
struct WoobApp: App {
// Adapts AppDelegate to SwiftUI
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var userInfo = UserInfo()
var body: some Scene {
WindowGroup {
InitialView()
}
}
}
class AppDelegate : NSObject, UIApplicationDelegate {
// Configure Firebase When App Launches
func application(_ application : UIApplication, didFinishLaunchingWithOptions launchOptions : [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
I think the issue is in here, however I will post the rest of my code in case Im wrong:
InitialView:
struct InitialView: View {
#EnvironmentObject var userInfo : UserInfo
var body: some View {
Group {
if userInfo.isUserAuthenticated == .undefined {
UndefinedView()
}
else if userInfo.isUserAuthenticated == .signedOut {
UndefinedView()
}
else if userInfo.isUserAuthenticated == .signedIn {
UndefinedView()
}
}
.onAppear{
self.userInfo.configureFirebaseStateDidChange()
}
}
And here is the User data :
class UserInfo : ObservableObject {
enum FBAuthState {
case undefined, signedIn, signedOut
}
#Published var isUserAuthenticated : FBAuthState = .undefined
func configureFirebaseStateDidChange() {
isUserAuthenticated = .signedIn
isUserAuthenticated = .signedOut
}
}
Thanks in advance for any help, Id really appreciate it so thank you!!!!

You have to actually pass that userInfo variable into your view hierarchy so that it's visible to InitialView and its children:
#ObservedObject var userInfo = UserInfo()
var body: some Scene {
WindowGroup {
InitialView()
.environmentObject(userInfo)
}
}
This principal of passing it via environmentObject is true whether you're using the SwiftUI lifecycle or a SceneDelegate. More reading on environmentObject: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views
You have a choice of whether to declare userInfo as an ObservedObject or StateObject that may depend on your OS target version: What is the difference between ObservedObject and StateObject in SwiftUI

Related

Cannot retrieve data back to my app from Firebase

I hope the problem is explained well on the title but what I want to do is to write the message on my message app and want to retrieve it and write it on the app's message cell but nothing is seen. When I go to my firebase console I can see the messages there but not on app. I didn't attach anything about project but I don't know what to attach. but let me add my chatViewController at least .`import UIKit
import Firebase
class ChatViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var messageTextfield: UITextField!
let db = Firestore.firestore()
var messages: [Message] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
navigationItem.hidesBackButton = true
title = K.appName
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
}
func loadMessages() {
db.collection(K.FStore.collectionName)
.order(by: K.FStore.dateField)
.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print("There was an issue retrieving data from Firestore \(e)")
}else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let messageSender = data[K.FStore.senderField] as? String, let messageBody = data[K.FStore.bodyField] as? String {
let newMessage = Message(sender: messageSender, body: messageBody)
self.messages.append(newMessage)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
}
#IBAction func sendPressed(_ sender: UIButton) {
if let messageBody = messageTextfield.text, let messageSender = Auth.auth().currentUser?.email {
db.collection(K.FStore.collectionName).addDocument(data: [K.FStore.senderField: messageSender, K.FStore.bodyField: messageBody]) { (error) in
if let e = error {
print("There was an issue saving data to firestore \(e)")
} else {
print("successfully saved data.")
}
}
}
}
#IBAction func logOutPressed(_ sender: UIBarButtonItem) {
do {
try Auth.auth().signOut()
navigationController?.popToRootViewController(animated: true)
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
}
}
extension ChatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! MessageCell
cell.label.text = messages[indexPath.row].body
return cell
}
}
`
And below is console output
'<FIRFirestore: 0x6000016d7040>
2022-10-17 17:55:19.817200+0300 Flash Chat iOS13[5876:41767] 10.0.0 - [FirebaseAnalytics][I-ACS023007] Analytics v.10.0.0 started
2022-10-17 17:55:19.817551+0300 Flash Chat iOS13[5876:41767] 10.0.0 - [FirebaseAnalytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see xxx
2022-10-17 17:55:19.824914+0300 Flash Chat iOS13[5876:41677] [Assert] UINavigationBar decoded as unlocked for UINavigationController, or navigationBar delegate set up incorrectly. Inconsistent configuration may cause problems. navigationController=<UINavigationController: 0x13984a200>, navigationBar=<UINavigationBar: 0x13950f870; frame = (0 48; 0 50); opaque = NO; autoresize = W; layer = <CALayer: 0x600000390980>> delegate=0x13984a200
2022-10-17 17:55:19.873401+0300 Flash Chat iOS13[5876:41763] 10.0.0 - [FirebaseAnalytics][I-ACS800023] No pending snapshot to activate. SDK name: app_measurement
2022-10-17 17:55:19.890442+0300 Flash Chat iOS13[5876:41765] 10.0.0 - [FirebaseAnalytics][I-ACS023012] Analytics collection enabled
2022-10-17 17:55:19.890731+0300 Flash Chat iOS13[5876:41765] 10.0.0 - [FirebaseAnalytics][I-ACS023220] Analytics screen reporting is enabled. Call Analytics.logEvent(AnalyticsEventScreenView, parameters: [...]) to log a screen view event. To disable automatic screen reporting, set the flag FirebaseAutomaticScreenReportingEnabled to NO (boolean) in the Info.plist
successfully saved data.'

SwiftUI ask Push Notifications Permissions again

So I have push notifications implemented in my App and when the app first starts up, its asks users if they would like to allow push notifications (this implementation works fine as expected).
If this user disallows the push notifications, is it possible to have a button in the app which allows the user to click on and it would ask to allow permissions again?
This is what im trying to achieve:
SettingsView
//IF PUSH NOTIFICATIONS NOT ENABLED, SHOW THIS SECTION
Section (header: Text("Push Notifications")) {
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Enable Push Notifications").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
checkPushNotifications()
}) {
Text("View").font(.system(size:12))
}
}
}
In my Push Notification Function:
class PushNotificationService: NSObject, MessagingDelegate {
static let shared = PushNotificationService()
private let SERVER_KEY = "myserverkey"
private let NOTIFICATION_URL = URL(string: "https://fcm.googleapis.com/fcm/send")!
private let PROJECT_ID = "my project name"
private override init() {
super.init()
Messaging.messaging().delegate = self
}
func askForPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted: Bool, error: Error?) in
if granted {
self.refreshFCMToken()
} else {
// Maybe tell the user to go to settings later and re-enable push notifications
}
}
}
func refreshFCMToken() {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print("Error fetching remote instance ID: \(error)")
} else if let result = result {
print("Remote instance ID token: \(result.token)")
self.updateFCMToken(result.token)
}
}
}
func updateFCMToken(_ token: String) {
guard let currentUser = Auth.auth().currentUser else { return }
let firestoreUserDocumentReference = Firestore.firestore().collection("users").document(currentUser.uid)
firestoreUserDocumentReference.updateData([
"fcmToken" : token
])
}
}
What im trying to achieve is if the user HAS NOT enabled notification only then ask them the option to reenable in SettingsView.
No you cannot. However, a good UI/UX design will be careful before burning the one-time chance of asking for permissions. Instead, use a user friendly UI to explain why you need certain permissions. For example, I often found it frustrating to implement a permission display view, and handle various async permission requests in a seperate view model. So I recently made a SwiftUI package:
PermissionsSwiftUI
                  
PermissionSwiftUI is a package to beautifully display and handle permissions.
EmptyView()
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
For a SINGLE line of code, you get a beautiful UI and the permission dialogs.
It already supports 7 OUT OF 12 iOS system permissions. More features coming 🙌
Full example
struct ContentView: View {
#State var showModal = false
var body: some View {
Button(action: {showModal=true},
label: {Text("Ask user for permissions")})
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
}
}
To use PermissionsSwiftUI, simply add the JMPermission modifier to any view.
Pass in a Binding to show the modal view, and add whatever permissions you want to show.
The short answer is no, you can't ask the user again if he once disabled the push-notifications for your app.
What you can do, is navigating the user to the settings in their phone to allow push-notifications again.
The code snippet in SwiftUI for the button would be:
Button(action: {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}, label: {
Text("Allow Push")
})
I would also refer to this question: How to ask notifications permissions if denied?

iOS 14 Widgets + SwiftUI + Firebase?

I'm still pretty new to SwiftUI and Firebase. Recently, as a hobby, I have been developing an app for my school. After the launch of Xcode 12, I decided to experiment with the new features such as Widgets. However, since my app gets its data from Firebase, I've been having some problems. My most recent problem is this "Thread 1: "Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore". I'm not entirely sure where to put "FirebaseApp.configure()" as there is no AppDelegate.swift for the widget. My code is below.
Edit:
I've rearranged my code so that I am now getting the data from the original iOS app data model. I am therefore not importing Firebase within the widgets Swift file. However, I still get the same error ("SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice Code=8" and "-> 0x7fff5bb6933a <+10>: jae 0x7fff5bb69344 ; <+20> - Thread 1: "Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore""). I've also included #Wendy Liga's code, but I still got the same error. My newer code is below :
iOS App Data Model
import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore
struct Assessment: Identifiable {
var id:String = UUID().uuidString
var Subject:String
var Class:Array<String>
var Day:Int
var Month:String
var Title:String
var Description:String
var Link:String
var Crit:Array<String>
}
class AssessmentsViewModel:ObservableObject {
#Published var books = [Assessment]()
private var db = Firestore.firestore()
// Add assessment variables
#Published var AssessmentSubject:String = ""
//#Published var AssessmentClass:Array<String> = [""]
#Published var AssessmentDay:Int = 1
#Published var AssessmentMonth:String = "Jan"
#Published var AssessmentTitle:String = ""
#Published var AssessmentDescription:String = ""
#Published var AssessmentLink:String = ""
#Published var AssessmentCrit:Array<String> = [""]
#Published var AssessmentDate:Date = Date()
func fetchData() {
db.collection("AssessmentsTest").order(by: "date").addSnapshotListener { (QuerySnapshot, error) in
guard let documents = QuerySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.map { (QueryDocumentSnapshot) -> Assessment in
let data = QueryDocumentSnapshot.data()
let Subject = data["subject"] as? String ?? ""
let Class = data["class"] as? Array<String> ?? [""]
let Day = data["day"] as? Int ?? 0
let Month = data["month"] as? String ?? ""
let Title = data["title"] as? String ?? ""
let Description = data["description"] as? String ?? ""
let Link = data["link"] as? String ?? ""
let Crit = data["crit"] as? Array<String> ?? [""]
return Assessment(Subject: Subject, Class: Class, Day: Day, Month: Month, Title: Title, Description: Description, Link: Link, Crit: Crit)
}
}
}
func writeData() {
let DateConversion = DateFormatter()
DateConversion.dateFormat = "DD MMMM YYYY"
let Timestamp = DateConversion.date(from: "20 June 2020")
db.collection("AssessmentsTest").document(UUID().uuidString).setData([
"subject": AssessmentSubject,
"month": AssessmentMonth,
"day": AssessmentDay,
"title": AssessmentTitle,
"description": AssessmentDescription,
"link": AssessmentLink,
"crit": AssessmentCrit,
"date": AssessmentDate
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
}
Widgets View
struct WidgetsMainView: View {
#ObservedObject private var viewModel = AssessmentsViewModel()
var body: some View {
HStack {
Spacer().frame(width: 10)
VStack(alignment: .leading) {
Spacer().frame(height: 10)
ForEach(self.viewModel.books) { Data in
HStack {
VStack {
Text(String(Data.Day))
.bold()
.font(.system(size: 25))
Text(Data.Month)
}
.padding(EdgeInsets(top: 16, leading: 17, bottom: 16, trailing: 17))
.background(Color(red: 114/255, green: 112/255, blue: 110/255))
.foregroundColor(Color.white)
.cornerRadius(10)
VStack(alignment: .leading, spacing: 0) {
Text("\(Data.Subject) Crit \(Data.Crit.joined(separator: " + "))")
.bold()
if Data.Title != "" {
Text(Data.Title)
} else {
Text(Data.Class.joined(separator: ", "))
}
}
.padding(.leading, 10)
}
}
.onAppear {
viewModel.books.prefix(2)
}
Spacer()
}
Spacer()
}
}
}
Widgets #main
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
#main
struct AssessmentsWidget: Widget {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private let kind: String = "Assessments Widget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
AssessmentsWidgetEntryView(entry: entry)
}
.configurationDisplayName("Assessments Widget")
.description("Keep track of your upcoming assessments.")
.supportedFamilies([.systemMedium])
}
}
Your main app needs to pass data to your extension, this can be achieved by allowing your app to use "App Groups" capability. What App Groups does is, it creates a container where your app can save data for you to share with your app extensions. So follow these steps to enable "App Groups".
1. Select your main App Target>Signing & Capabilities then tap + Capability and select "App Groups"
2. Tap on "+" to add a new container, and add a name to it after group. example : "group.com.widgetTest.widgetContainer"
Once you have created the "App Group" on your main app, you should take the same steps but on your "Widget Extension" target. This time, instead of creating a container, you should be able to select the container you already have from the main app. You can find a good video on YouTube explaining this process really well on here How to Share UserDefaults with app extensions
The next step I recommend is to create a Swift Package or a Framework, and add a new Model Object, this model object is the one you will be passing from your main app, to your widget extension. I chose a Swift Package.
To do this follow these steps:
1. File>New>Swift Package
A good video from the WWDC19 about this can be seen here
2. In your Swift Package, inside the "Sources" folder, Create a Custom Model which you will use in both your Main App, and Widget Extension
Make your object conform to "Codable" and that it is Public.
Important Make sure you import "Foundation" so that when you are decoding/encoding your object, it will do it properly.
3. Add your Package to your Main App and Widget Extension
Select your App's Target>General> Scroll to "Frameworks, Libraries, and Embedded Content"
Tap "+" and search for your Package
Do the same steps on your Widget's Extension
Now, all you need to do is "import" your module in the file that you will be creating your custom object in both your Main App, and on your WidgetExtension, then initialize your shared object on your main app and save it to UserDefaults by first encoding the object to JSON and then saving it to UserDefaults(suiteName: group.com.widgetTest.widgetContainer)
let mySharedObject = MySharedObject(name: "My Name", lastName: "My Last Name")
do {
let data = try JSONEncoder().encode(mySharedObject)
/// Make sure to use your "App Group" container suite name when saving and retrieving the object from UserDefaults
let container = UserDefaults(suiteName:"group.com.widgetTest.widgetContainer")
container?.setValue(data, forKey: "sharedObject")
/// Used to let the widget extension to reload the timeline
WidgetCenter.shared.reloadAllTimelines()
} catch {
print("Unable to encode WidgetDay: \(error.localizedDescription)")
}
Then in your widget extension, you want to retrieve your object from UserDefaults, decode it and you should be good to go.
Short Answer
Download your Firebase data, create a new object from that data, encode it to JSON, save it on your container by using UserDefaults, retrieve the object in your extension from the container, decode it and use it for your widget entry. Of course, all of this is assuming you follow the steps above.
I can confirm after testing that the following method works to use Firebase in the Widget Target without incorporating an app group, user defaults or anything else.
#main
struct FirebaseStartupSequence: Widget {
init() {
FirebaseApp.configure()
}
let kind: String = "FirebaseStartupSequence"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
FirebaseStartupSequenceEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
Simply use the init method in your widget to access a firebase instance.
This was the easiest solution for me as of today.
Taken from: https://github.com/firebase/firebase-ios-sdk/issues/6683
Additional Edit: Do you need to share authentication? No problem. Firebase has that covered here: https://firebase.google.com/docs/auth/ios/single-sign-on?authuser=1
You can add the appDelegate to your #main SwiftUI view
First create your appdelegate on your widget extension
import Firebase
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
look at #main, inside your widget extension,
#main
struct TestWidget: Widget {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private let kind: String = "ExampleWidget"
public var body: some WidgetConfiguration {
...
}
}
#main is new swift 5.3 feature that allows value type entry point, so this is will be your main entry point for your widget extension
just add #UIApplciationDelegateAdaptor, inside your #main

SwiftUI Fetching contacts duplication

I have a model view that has fetching contacts function:
class ContactsStore: ObservableObject {
#Published var contacts = [CNContact]()
func fetch() {} ...
And then in my View:
#EnvironmentObject var store: ContactsStore
var groupedContacts: [String: [CNContact]] {
.init (
grouping: store.contacts,
by: {$0.nameFirstLetter}
)
}
...
List() {
ForEach(self.groupedContacts.keys.sorted(), id: \.self) { key in ...
I've shortened my code for the convenience, will add/edit if needed. The issue I faced - every time my View is rendered the fetch function is called and my array of contacts is duplicated in my view. TIA for a help
UPD: Duplication happens due to method calling fetch in the List .onAppear. So I'm looking at how to call this method only once and not every time the View appears.
You can do fetch in Init() like that:
struct someView: View{
var list: [Settings]
init(){
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Settings> = Settings.fetchRequest()
//... some order and filter if you need
self.list = context.fetch(fetchRequest)
}
var body: some View{
...
ForEach(list){settings in
...
}
...
}
}
Didn't try it with grouping, but you asked how to fetch only once. The answer is - in init().
But you cant get the #Environment in init, so i get the context from AppDelegate. Or you can pass the context as init parameter

how to publish the data of a network request using Combine framework and SwiftUI

I'm developing an iOS app using swiftUI and Combine framework and also MVVM.
I'm want to handle the login API request in a separate class called LoginService, which is used in the LoginViewModel.
Now I want to know how should I publish and observe the attributes between view and ViewModel.
I mean ViewModel is an ObservableObject and is being observed in the View, But since I'm handling the network request in a Service class how should LoginService notify LoginViewModel and LoginView that the data is received and the View should be updated?
import Foundation
import Combine
class LoginViewModel: ObservableObject {
#Published var user = UserModel()
#Published var LoginStatus: Bool = false
#Published var LoginMessage: String = ""
var service = LoginService()
func Login(With email: String, And password: String) -> Bool {
service.validateLogin(email: email, password: password)
return false
}
}
This is the Code for LoginViewModel.
How should LoginService change the values for LoginStatus, LoginMessage and user when data is received from the server to notify the View?
I'm saying this because as far as I know you can Observe the ObservableObjects only in the View(SwiftUI).
Okay so I take your example and I would do the following:
I assumed your service returns true or false
import Foundation
import Combine
class LoginViewModel: ObservableObject {
#Published var LoginStatus: Bool = false
#Published var LoginMessage: String = ""
var service = LoginService()
func Login(_ email: String, _ password: String) -> Bool {
self.LoginStatus = service.validateLogin(email: email, password: password)
return self.LoginStatus
}
}
In your view:
import SwiftUI
struct ContentView : View {
#ObservedObject var model = LoginViewModel()
var body: some View {
VStack {
Button(action: {
_ = self.model.Login("TestUser", "TestPassword")
}, label: {
Text("Login")
})
Text(self.model.LoginStatus ? "Logged In" : "Not Logged in")
}
}
}
It should be something around that.
I removed UserModel because you shouldn't nest models.
I hope this helps.
EDIT 1:
To validate something automatically you can use onApear() on your View
Or listen to changes with olnrecieve() to update the UI or the State
import SwiftUI
struct ContentView : View {
#ObservedObject var model = LoginViewModel()
var body: some View {
VStack {
Button(action: {
_ = self.model.Login("TestUser", "TestPassword")
}, label: {
Text("Login")
})
Text(self.model.LoginStatus ? "Logged In" : "Not Logged in")
}.onAppear {
// call a function that gets something from your server
// and modifies your state
self.model.validate()
}.onReceive(self.model.$LoginMessage, perform: { message in
// here you can update your state or your ui
// according the LoginMessage... this gets called
// whenever LoginMessage changes in your model
})
}
}
You've established a one-way binding, i.e; model -> view
Now it doesn't matter how your service updates model, whenever model changes, view updates.
There are several ways for your service to update model.
Usual URLSession, update LoginStatus and LoginMessage in callback as usual.
Via Combine publishers, e.g.; URLSessionDataTaskPublisher, and do update in its sink closure.

Resources