I am creating an application in which a user, based on the permissions he has, can access the various views.
I use this method to constantly check user permissions:
func checkPermission() {
let docRef = self.DatabaseFirestore.collection("Admins").document(phoneNumber)
docRef.getDocument{(document, error) in
guard error == nil else {
return
}
if let document = document, document.exists {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
I don't know if it is the most correct function I could use, but it is one of the few that I have found that works.
This is my view:
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
administratorManager.checkPermission()
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
}
}
When I call the "administratorManager.checkPermission()" function and press the "Alert" button the message is displayed, but even if the button is pressed the alert does not disappear. If I don't call this function, everything works.
How can I solve? Can the alert go against firebase? Is there a more suitable method to read only one data?
photo of the screen when it got locked
I ran your code and I saw the behavior you described.
The reason is the function call directly in the body.
If you want to call a function when when you open a view, use the .onAppear function for that specific view. In your case
.onAppear {
administratorManager.checkPermission()
}
The following (worked for me with you code):
struct AdministratorPage: View {
#StateObject var administratorManager = AdministratorManager()
// User variables.
#AppStorage("phoneNumber") var phoneNumber: String = "" // User number.
#AppStorage("Access") var access: Bool = false
var body: some View {
return NavigationView {
HStack {
VStack {
Text("Home")
Text(phoneNumber)
// Button to log out.
Button("Logout", action: {
self.access = false
})
Button("Alert", action: {
administratorManager.message = "Error title!"
administratorManager.message = "Error message!"
administratorManager.isMessage = true
}).alert(isPresented: $administratorManager.isMessage) {
Alert(title: Text(administratorManager.title), message: Text(administratorManager.message),
dismissButton: .default(Text("Ho capito!")))
}
}
}
}
.onAppear {
administratorManager.checkPermission()
}
}
}
UPDATE: add Snapshot listener instead of polling
Your initial approach was doing a kind of polling, it called the function constantly. Please keep in mind, when you do a Firebase request, you will be billed for the documents you get back. If you do the polling, you get the same document multiple times and will be billed for it.
With my above mentioned example in this answer, you just call the function once.
If you now want to get the live updated from Firestore, you can add a snapshot listener. The approach would be:
func checkPermission() {
let docRef = db.collection("Admins").document(phoneNumber).addSnapshotListener() { documentSnapshot, error in //erca nella collezione se c'è il numero.
guard error == nil else {
print("ERROR.")
return
}
if let document = documentSnapshot {
self.controlloAdmin = true
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.permission = data["Permessi"] as? [Bool] ?? []
} else {
self.controlloAdmin = false
self.isRegistred = false
self.access = false
}
}
}
Whenever a value changed on that document in Friestore, it'll be changed on your device as well.
Best, Sebastian
Related
I am working on a SwiftUI project and using Firebase. Part of the app allows users to add business addresses to their account. In Firebase I add a BusinessAddress as a sub collection to the user.
User -> BusinessAddresses
In iOS I am using snapshotlistners and Combine to get the data from Firestore. My issue happens when one user logs out and a new user logs in. The business addresses from the first user continue to show in the view that is designated to show business addresses.
I've confirmed the Firebase code is working properly. What seems to be happening is that the creation of BusinessAddressViewModel maintains the original instance of BusinessAddressRepository from when the first user logs in.
My question is how do I "reset" the instance of businessAddressRepository in my BusinessAddressViewModel when a new user logs in?
BusinessAddressRepository
This file gets the data from Firebase. It gets the currently user that is logged in and pulls the business addresses for this user. For each log in, I can see that the proper user and addresses are being updated here.
class BusinessAddressRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var businessAddresses = [BusinessAddress]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
guard let currentUserId = Auth.auth().currentUser else {
return
}
if snapshotListener == nil {
self.snapshotListener = db.collection(FirestoreCollection.users).document(currentUserId.uid).collection(FirestoreCollection.businessAddresses).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Business Addresses.")
return
}
self.businessAddresses = documents.compactMap { businessAddress in
do {
print("This is a business address *********** \(businessAddress.documentID)")
return try businessAddress.data(as: BusinessAddress.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
}
BusinessAddressViewModel
This file uses Combine to monitor the #published property businessAddresses from bankAccountRepository. This seems to be where the problem is. For some reason, this is holding on to the instance that is created when the app starts up.
class BusinessAddressViewModel: ObservableObject {
var businessAddressRepository: BusinessAddressRepository
#Published var businessAddressRowViewModels = [BusinessAddressRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(businessAddressRepository: BusinessAddressRepository) {
self.businessAddressRepository = businessAddressRepository
self.startCombine()
}
func startCombine() {
businessAddressRepository
.$businessAddresses
.receive(on: RunLoop.main)
.map { businessAddress in
businessAddress
.map { businessAddress in
BusinessAddressRowViewModel(businessAddress: businessAddress)
}
}
.assign(to: \.businessAddressRowViewModels, on: self)
.store(in: &cancellables)
}
}
BusinessAddressViewModel is set up and an #EnvironmentObject in main.
Main
#StateObject private var businessAddressViewModel = BusinessAddressViewModel(businessAddressRepository: BusinessAddressRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(businessAddressViewModel)
}
I was able to confirm it's hanging on to the instance in the view BusinessAddressView. Here I use a List that looks at each BusinessAddress in BusinessAddressViewModel. I use print statements in the ForEach and an onAppear that show the BusinessAddresses from the first user.
BusinessAddressView
struct BusinessAddressView: View {
let db = Firestore.firestore()
#EnvironmentObject var businessAddressViewModel: BusinessAddressViewModel
var body: some View {
List {
ForEach(self.businessAddressViewModel.businessAddressRowViewModels, id: \.id) { businessAddressRowViewModel in
let _ = print("View Business Addresses \(businessAddressRowViewModel.businessAddress.id)")
NavigationLink(destination: BusinessAddressDetailView(businessAddressDetailViewModel: BusinessAddressDetailViewModel(businessAddress: businessAddressRowViewModel.businessAddress, businessAddressRepository: businessAddressViewModel.businessAddressRepository))
) {
BusinessAddressRowView(businessAddressRowViewModel: businessAddressRowViewModel)
}
} // ForEach
} // List
.onAppear(perform: {
for businessAddress in self.businessAddressViewModel.businessAddressRepository.businessAddresses {
let _ = print("On Appear Business Addresses \(businessAddress.id)")
}
}) // CurrentUserUid onAppear
} // View
}
So, how do I reset the instance of BusinessAddressViewModel and get it to look at the current BusinessAddressRepository?
I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.
I know its a really simple question but I'm just stuck on it atm so any advice would be greatly appreciated as I am new to SwiftUI.
I am trying to download text from firebase and render it to the view but I keep getting an out of range error:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
The code is as follows:
var body: some View{
ZStack {
if fetch.loading == false {
LoadingView()
}
else{
Text(names[0])
.bold()
}
}
.onAppear {
self.fetch.longTask()
}
}
Here is the Fetch Content Page:
#Published var loading = false
func longTask() {
DispatchQueue.main.asyncAfter(deadline: .now()) {
let db = Firestore.firestore()
db.collection("Flipside").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
return
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
//name = items[doc]
print("Names: ", name)
print("Descriptions: ", description)
names.append(name)
descriptions.append(description)
}
}
}
self.loading = true
}
}
So basically when the view appears, get the data from Firebase when the data has downloaded display the menuPage() until then show the Loading Data text.
Any help is welcome!
As Rob Napier mentioned, the issue is that you're accessing the array index before the array is populated.
I'd suggest a couple of improvements to your code. Also, instead of maintaining separate arrays (names, descriptions, ...) you can create a struct to hold all the properties in one place. This will allow you to use just one array for your items.
struct Item {
let name: String
let description: String
}
class Fetch: ObservableObject {
#Published var items: [Item] = [] // a single array to hold your items, empty at the beginning
#Published var loading = false // indicates whether loading is in progress
func longTask() {
loading = true // start fetching, set to true
let db = Firestore.firestore()
db.collection("Flipside").getDocuments { snapshot, err in
if let err = err {
print("Error getting documents: \(err)")
DispatchQueue.main.async {
self.loading = false // loading finished
}
} else {
let items = snapshot!.documents.map { document in // use `map` to replace `snapshot!.documents` with an array of `Item` objects
let name = document.get("Name") as! String
let description = document.get("Description") as! String
print("Names: ", name)
print("Descriptions: ", description)
return Item(name: name, description: description)
}
DispatchQueue.main.async { // perform assignments on the main thread
self.items = items
self.loading = false // loading finished
}
}
}
}
}
struct ContentView: View {
#StateObject private var fetch = Fetch() // use `#StateObject` in iOS 14+
var body: some View {
ZStack {
if fetch.loading { // when items are being loaded, display `LoadingView`
LoadingView()
} else if fetch.items.isEmpty { // if items are loaded empty or there was an error
Text("No items")
} else { // items are loaded and there's at least one item
Text(fetch.items[0].name)
.bold()
}
}
.onAppear {
self.fetch.longTask()
}
}
}
Note that accessing arrays by subscript may not be needed. Your code can still fail if there's only one item and you try to access items[1].
Instead you can probably use first to access the first element:
ZStack {
if fetch.loading {
LoadingView()
} else if let item = fetch.items.first {
Text(item.name)
.bold()
} else {
Text("Items are empty")
}
}
or use a ForEach to display all the items:
ZStack {
if fetch.loading {
LoadingView()
} else if fetch.items.isEmpty {
Text("Items are empty")
} else {
VStack {
ForEach(fetch.items, id: \.name) { item in
Text(item.name)
.bold()
}
}
}
}
Also, if possible, avoid force unwrapping optionals. The code snapshot!.documents will terminate your app if snapshot == nil. Many useful solutions are presented in this answer:
What does “Fatal error: Unexpectedly found nil while unwrapping an Optional value” mean?
The basic issue is that you're evaluating names[0] before the names array has been filled in. If the Array is empty, then you would see this crash. What you likely want is something like:
Item(title: names.first ?? "", ...)
The reason you're evaluating names[0] too soon is that you call completed before the fetch actually completes. You're calling it synchronously with the initial method call.
That said, you always must consider the case where there are connection errors or or the data is empty or the data is corrupt. As a rule, you should avoid subscripting Arrays (preferring things like .first), and when you do subscript Arrays, you must first make sure that you know how many elements there are.
I have to present an Alert on a view if the user taps on it.
My alert depends on several situations:
Item is purchased. Show the item.
Item have not be purchased. Show an alert telling the user the item have to be purchased. This alert must show two buttons, OK to purchase, Cancel to dismiss.
User taps to purchase the item.
Purchase is successful, show the item.
Purchase fails, show error.
This is how I did it.
class AlertDialog {
enum SelectedType {
case none
case purchase
case mustBePurchased
case purchaseError
}
var selectedType:SelectedType = .none
}
struct FilteredListItem: View {
#State var showAlert: Bool = false
private var alertDialog:AlertDialog?
var body: some View {
Text(item.termLowerCase)
.font(fontItems)
.foregroundColor(.white)
.onTapGesture {
DispatchQueue.main.async {
appStoreWrapper.verifyPurchase(productID: item.package!)
{ // run if purchased
purchased = true
} runIfNotPurchased: {
purchased = false
alertDialog!.selectedType = .mustBePurchased
showAlert = true
}
}
}
.alert(isPresented: $showAlert) {
if alertDialog!.selectedType == .purchase {
appStoreWrapper.purchase(productID: item.package!) {
// run if purchased
purchased = true
} runIfPurchaseFailed: { (error) in
alertDialog!.selectedType = .purchaseError
appStoreWrapper.purchaseError = error
showAlert = true
}
} else if alertDialog!.selectedType == .purchaseError {
let primaryButton = Alert.Button.default(Text("OK")) {
showAlert = false
}
return Alert(title: Text(appStoreWrapper.makeString("ERROR")),
message: Text(appStoreWrapper.purchaseError),
dismissButton: primaryButton)
}
let dismissButton = Alert.Button.default(Text(appStoreWrapper.makeString("CANCEL"))) {
showAlert = false
}
let primaryButton = Alert.Button.default(Text("OK")) {
appStoreWrapper.purchase(productID: item.package!) {
// run if purchased
purchased = true
} runIfPurchaseFailed: { (error) in
appStoreWrapper.purchaseError = error
alertDialog!.selectedType = .purchaseError
showAlert = true
print(erro)
}
}
return Alert(title: Text(appStoreWrapper.makeString("ERROR")),
message: Text(appStoreWrapper.purchaseError),
primaryButton: primaryButton,
secondaryButton: dismissButton)
}
This is my problem: the modifier .alert(isPresented: $showAlert) expects an Alert() to be returned, right? But I have these asynchronous methods
appStoreWrapper.verifyPurchase(productID: item.package!)
{ // run if purchased },
runIfNotPurchased: { }
that cannot return anything to the alert modifier. How do I do that? Is what I am doing right?
There's a lot going on in your code and you didn't post the code for appStoreWrapper, but here's some code that should be able to point you in the right direction.
FYI:
You can use a Button with an Action instead of using Text with .onTapGesture
The code within .Alert should only function to get an Alert. You shouldn't be doing other actions within the .Alert closure.
struct FilteredListItem: View {
#State var showAlert: Bool = false
private var alertDialog: AlertDialog?
var body: some View {
Button(action: {
verifyItem()
}, label: {
Text("ITEM NAME")
.foregroundColor(.white)
})
.accentColor(.primary)
.alert(isPresented: $showAlert, content: {
getAlert()
})
}
func verifyItem() {
// FUNCTION TO VERIFY ITEM HERE
var success = true //appStoreWrapper.verifyPurchase...
if success {
// Handle success
} else {
alertDialog?.selectedType = .mustBePurchased
showAlert.toggle()
}
}
func purchaseItem() {
// FUNCTION TO PURCHASE ITEM HERE
var success = true //appStoreWrapper.purchase...
if success {
// Handle success
} else {
alertDialog?.selectedType = .purchaseError
showAlert.toggle()
}
}
func getAlert() -> Alert {
guard let dialog = alertDialog else {
return Alert(title: Text("Error getting alert dialog."))
}
switch dialog.selectedType {
case .purchaseError:
return Alert(
title: Text("Error purchasing item."),
message: nil,
dismissButton: .default(Text("OK")))
case .mustBePurchased:
return Alert(
title: Text("Items have to be purchased."),
message: nil,
primaryButton: .default(Text("Purchase"), action: {
purchaseItem()
}),
secondaryButton: .cancel())
case .none, .purchase:
return Alert(title: Text("Purchased!"))
}
}
}
My first initial view for my app is a splash screen:
This screen has code to check if an authToken is available, if it's not then it will toggle the showLogin bool, which in turn causes my view to go to the LoginView(). Else if there is a authToken available, then it calls a function which fetches information about the user, stores it in an Observable Object and then sets showMain bool to true, which in turn changes my view to my main screen.
It works, but whenever it changes view, it puts the previous view at the bottom of the new view. I know I should use a navigation link for this but I can't get it to work properly with my logic and this is the closest I've gotten:
This is my code:
struct Splash: View {
let keychain = KeychainSwift()
#EnvironmentObject var user: userData
#State var showLogin = false
#State var showMain = false
var body: some View {
NavigationView{
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
Text("Splash Screen")
.onAppear(){
if(checkLogin()){
let authToken = keychain.get("authToken")
getDataAPI().getUserData(authToken: authToken!, completion: { response, error in
print("Starting")
if(error == nil){
let code = response!["code"] as? String
if(code == "100"){
print("Done")
DispatchQueue.main.async {
user.email = response?["email"] as! String
user.uid = response?["uid"] as! String
user.username = response?["username"] as! String
}
showMain.toggle()
}else{
print(error!)
}
}
})
}else{
showLogin.toggle()
}
}
}
}
}
}
Because they all in VStack, ie. one below another. To solve this you have to remove splash view from view hierarchy, explicitly, like
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
if !showLogin && !showMain {
Text("Splash Screen")
// ... other your code
}
}