I want to gather information about user with firebase firestore, I have in a Class a function which do that, but I want share this information with my struct ContentView.
class UserData : ObservableObject {
// Use Firebase library to configure APIs
#Published var db = Firestore.firestore()
#Published var WholeDocument : Array<String> = []
func GetAllData(completion: #escaping (Array<String>) -> ()) {
db.collection("Users").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
completion([""])
} else {
for document in querySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
self.WholeDocument.append(document.documentID)
}
completion(self.WholeDocument)
}
}
I try to gather this array in my contentView
func Connection()
{
self.Database.GetAllData{(tab) in
if tab.count > 0 {
self.WholeDocumentContentView = tab
}
else {
print("Not found")
}
}
}
How to retrieve WholeDocument array with closure ?
Thank you for the help.
If I'm understanding the structure/question correctly, your goal is to access the WholeDocument array in another SwiftUI view. The way to do that could be:
class UserData : ObservableObject {
// Use Firebase library to configure APIs
private var db = Firestore.firestore()
#Published var WholeDocument : Array<String> = []
init() {
db.collection("Users").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
completion([""])
} else {
for document in querySnapshot!.documents {
// print("\(document.documentID) => \(document.data())")
self.WholeDocument.append(document.documentID)
}
}
}
}
}
struct MyContentView : View {
#ObservedObject var database: UserData
var body: some View {
// access database.WholeDocument
List(database.WholeDocument, id: \.self) {
// do something
}
}
That way, when you initialize MyContentView with an instance of UserData, you will be able to observe WholeDocument in that view.
Related
How could I return a Bool from this sign in with apple function that uses the authorizationController? I have been looking at withCheckedThrowingContinuation but with no luck. I have also seen some examples of using completion handlers but I would like to use async.
func signInWithApple(withState state: SignInState) async -> Bool {
let request = signInWithAppleRequest(withState: state)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
//...
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
switch state {
case .signIn:
Task {
authenticationState = .authenticating
do {
let result = try await Auth.auth().signIn(with: credential)
if result.additionalUserInfo?.isNewUser == true {
self.isNewUser = true
}
return true
} catch {
self.authenticationState = .unauthenticated
print(error.localizedDescription)
return false
}
}
case .reauth:
Task {
if let user = Auth.auth().currentUser {
do {
try await user.reauthenticate(with: credential)
return true
} catch {
print(error.localizedDescription)
return false
}
}
}
}
}
}
I don't have all your types, so here is a simple example of a continuation which at least compiles.
The class must inherit from NSObject and adopt ASAuthorizationControllerDelegate.
The continuation is set in the same scope where the request is executed.
The most important rule is: The continuation must be resumed exactly once
import AuthenticationServices
class Authorization: NSObject, ASAuthorizationControllerDelegate {
private var continuation : CheckedContinuation<ASAuthorization,Error>?
func signInWithApple() async throws -> ASAuthorization {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
continuation?.resume(returning: authorization)
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
continuation?.resume(throwing: error)
}
}
I get this Error -> Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) randomly. I don't quite understand when exactly it happens. Most of the times it is when the view refreshes. The Error appears at the line where group.leave() gets executed.
What am I trying to do:
I want to fetch albums with their image, name and songs that also have a name and image from my firebase database. I checked for the values and they're all right as far as I can tell. But when trying to show them it is random what shows. Sometimes everything is right, sometimes one album gets showed twice, sometimes only one album gets showed at all, sometimes one album has the songs of the other album.
My firebase database has albums stored as documents, each document has albumimage/name and 2 subcollections of "unlocked" with documents(user uid) that store "locked":Bool and "songs" with a document for each song that stores image/name
This is the function that fetches my albums with their songs:
let group = DispatchGroup()
#State var albums: [Album] = []
#State var albumSongs: [AlbumSong] = []
func fetchAlbums() {
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
guard let documents = querySnapshot?.documents else {
return
}
let uid = FirebaseManager.shared.auth.currentUser?.uid
documents.forEach { document in
let data = document.data()
let name = data["name"] as? String ?? ""
let artist = data["artist"] as? String ?? ""
let releaseDate = data["releaseDate"] as? Date ?? Date()
let price = data["price"] as? Int ?? 0
let albumImageUrl = data["albumImageUrl"] as? String ?? ""
let docID = document.documentID
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
if let error = error {
return
}
guard let documents = querySnapshot?.documents else {
return
}
self.albumSongs = documents.compactMap { document -> AlbumSong? in
do {
return try document.data(as: AlbumSong.self)
} catch {
return nil
}
}
group.leave()
}
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
if let error = error {
return
}
guard let document = docSnapshot?.data() else {
return
}
group.enter()
group.notify(queue: DispatchQueue.global()) {
if document["locked"] as! Bool == true {
self.albums.append(Album(name: name, artist: artist,
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
print("albums: ",albums)
}
}
}
}
}
}
I call my fetchAlbums() in my view .onAppear()
My AlbumSong:
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
My Album:
struct Album: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let artist: String
let songs: [AlbumSong]
let releaseDate: Date
let price: Int
let albumImageUrl: String
}
I tried looking into how to fetch data from firebase with async function but I couldn't get my code to work and using dispatchGroup worked fine when I only have one album. I would appreciate answers explaining how this code would work with async, I really tried my best figuring it out by myself a long time. Also I would love to know what exactly is happening with DispatchGroup and why it works fine having one album but not with multiple ones.
I think you are over complicating something that is very simple with async await
First, your Models need some adjusting, it may be the source of some of your issues.
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct AlbumSong: Identifiable, Codable {
//No need to set a UUID `#DocumentID` provides an ID
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
struct Album: Identifiable, Codable {
//No need to set a UUID `#DocumentID` provides an ID
#DocumentID var id: String?
let name: String
let artist: String
//Change to var and make nil, the initial decoding will be blank
//If any of the other variables might be optional add the question mark
var songs: [AlbumSong]?
let releaseDate: Date
let price: Int
let albumImageUrl: String
}
Then you can create a service that does the heavy lifting with the Firestore.
struct NestedFirestoreService{
private let store : Firestore = .firestore()
let ALBUM_PATH = "albums"
let SONG_PATH = "songs"
///Retrieves Albums and Songs
func retrieveAlbums() async throws -> [Album] {
//Get the albums
var albums: [Album] = try await retrieve(path: ALBUM_PATH)
//Get the songs, **NOTE: leaving the array of songs instead of making a separate collection might work best.
for (idx, album) in albums.enumerated() {
if let id = album.id{
albums[idx].songs = try await retrieve(path: "\(ALBUM_PATH)/\(id)/\(SONG_PATH)")
}else{
print("\(album) :: has invalid id")
}
}
//Add another loop for `unlocked` here just like the one above.
return albums
}
///retrieves all the documents in the collection at the path
public func retrieve<FC : Identifiable & Codable>(path: String) async throws -> [FC]{
let querySnapshot = try await store.collection(path)
.getDocuments()
return try querySnapshot.documents.compactMap { document in
try document.data(as: FC.self)
}
}
}
Then you can implement it with just a few lines in your presentation layer.
import SwiftUI
#MainActor
class AlbumListViewModel: ObservableObject{
#Published var albums: [Album] = []
private let svc = NestedFirestoreService()
func loadAlbums() async throws{
albums = try await svc.retrieveAlbums()
}
}
struct AlbumListView: View {
#StateObject var vm: AlbumListViewModel = .init()
var body: some View {
List(vm.albums, id:\.id) { album in
DisclosureGroup(album.name) {
ForEach(album.songs ?? [], id:\.id){ song in
Text(song.title)
}
}
}.task {
do{
try await vm.loadAlbums()
}catch{
print(error)
}
}
}
}
struct AlbumListView_Previews: PreviewProvider {
static var previews: some View {
AlbumListView()
}
}
If you get any decoding errors make the variables optional by adding the question mark to the type like I did with the array.
Just use them in the correct order:
let group = DispatchGroup()
#State var albums: [Album] = []
#State var albumSongs: [AlbumSong] = []
func fetchAlbums() {
group.enter()
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
group.leave()
return
}
guard let documents = querySnapshot?.documents else {
group.leave()
return
}
let uid = FirebaseManager.shared.auth.currentUser?.uid
documents.forEach { document in
let data = document.data()
let name = data["name"] as? String ?? ""
let artist = data["artist"] as? String ?? ""
let releaseDate = data["releaseDate"] as? Date ?? Date()
let price = data["price"] as? Int ?? 0
let albumImageUrl = data["albumImageUrl"] as? String ?? ""
let docID = document.documentID
group.enter()
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
if let error = error {
group.leave()
return
}
guard let documents = querySnapshot?.documents else {
group.leave()
return
}
self.albumSongs = documents.compactMap { document -> AlbumSong? in
do {
group.leave()
return try document.data(as: AlbumSong.self)
} catch {
group.leave()
return nil
}
}
}
group.enter()
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
if let error = error {
group.leave()
return
}
guard let document = docSnapshot?.data() else {
group.leave()
return
}
if document["locked"] as! Bool == true {
self.albums.append(Album(name: name, artist: artist,
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
print("albums: ",albums)
}
group.leave()
}
}
group.leave()
}
group.notify(queue: DispatchQueue.global()) {
// do your stuff
}
}
I am working on SwiftUI and using Resolver for Dependency Injection. As a backend I am using Firebase. I've created an AuthSession file that handles all of my user authentication stuff. In the project I also have a number of other repositories that populate data throughout the app. In AuthSession I am creating properties for each repository so that I can start and stop Firestore Listeners on login and logout. In a couple of these repositories I want to access AuthSession through #InjectedObject so that when a user logs in I can be notified and can get updates via Combine. My issues is that when I start the app, it crashes with an odd Firebase error.
AuthSession.swift
class AuthSession: ObservableObject {
let db = Firestore.firestore()
var offerRepository: OfferRepository = Resolver.resolve()
var handle: AuthStateDidChangeListenerHandle?
#Published var currentUser: User?
#Published var loggedIn = false
#Published var currentUserUid = ""
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Intitalizer
init() {
}
func listen() {
print("AuthSession - listen called")
// Monitor Authentication chagnes using Firebase Auth.
handle = Auth.auth().addStateDidChangeListener{ (auth, user) in
// Check to see if a user is returned from a sign in or sign up event.
if let user = user {
// Set loggedIn to true. This will also be set when a new User is created in SignUpView.
print("User Exists.")
self.loggedIn = true
self.currentUserUid = user.uid
self.currentUser = user
} else {
print("Not logged in")
}
}
}
}
Below is OfferRepository. When the line below is added it crashes. If the line is removed it does not crash. I'm not sure why. The Combine code is not included.
Line causing the crash.
#InjectedObject var authSession: AuthSession
OfferRepository.swift
class OfferRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#InjectedObject var authSession: AuthSession
#Published var offers = [Offer]()
private var cancellables = Set<AnyCancellable>()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
if snapshotListener == nil {
self.snapshotListener = db.collection(FirestoreCollection.offers).order(by: "created", descending: true).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Offers.")
return
}
self.offers = documents.compactMap { offer in
do {
return try offer.data(as: Offer.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
}
For reference here is my AppDelegate+Registering file.
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
register { AuthSession() }.scope(.application)
register { OfferRepository() as OfferRepository }.scope(.application)
}
}
The app crashed on the line below from the Firestore package.
- (NSString *)keyForDatabase:(NSString *)database {
return [NSString stringWithFormat:#"%#|%#", self.app.name, database];
}
Thread 1: EXC_BAD_ACCESS (code=2, address=0x16d317ff8)
While I can start and stop listeners from login and logout views, I'd prefer to keep this in the AuthSession file. Is there a way around this?
#InjectedObject is intended to be used to inject ObservableObjects into SwiftUI views - see the docs: https://github.com/hmlongco/Resolver#property-wrappers
As you want to reference the AuthenticationService inside your repositories (which are ObservableObjects, you should use #Injected instead.
Here is a snippet from one of my apps:
public class ArtifactRepository: ObservableObject {
// MARK: - Dependencies
#Injected var db: Firestore
#Injected var authenticationService: AuthenticationService
// MARK: - Publishers
#Published public var artifacts = [Artifact]()
// MARK: - Private attributes
private var statusFilter: Status
private var userId: String = "unknown"
private var listenerRegistration: ListenerRegistration?
private var cancellables = Set<AnyCancellable>()
let logger = Logger(subsystem: "dev.peterfriese.App", category: "persistence")
public init(statusFilter: Status = .inbox, liveSync: Bool = true) {
// filtering
self.statusFilter = statusFilter
// observe user ID
authenticationService.$user
.compactMap { user in
user?.uid
}
.assign(to: \.userId, on: self)
.store(in: &cancellables)
// if live sync is on, (re)load data when user changes
if liveSync {
authenticationService.$user
.receive(on: DispatchQueue.main)
.sink { [weak self] user in
if self?.listenerRegistration != nil {
self?.unsubscribe()
self?.subscribe()
}
}
.store(in: &cancellables)
}
}
deinit {
unsubscribe()
}
public func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
public func subscribe() {
if listenerRegistration == nil {
var query = db.collection("artifacts")
.whereField("userId", isEqualTo: self.userId)
if (statusFilter != .all) {
query = query.whereField("status", isEqualTo: statusFilter.rawValue)
}
listenerRegistration = query.order(by: "dateAdded", descending: true)
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.logger.debug("No documents")
return
}
self?.logger.debug("Mapping \(documents.count) documents")
self?.artifacts = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Artifact.self)
}
}
}
}
}
public class AuthenticationService: ObservableObject {
private let logger = Logger(subsystem: "dev.peterfriese.App", category: "authentication")
#Published public var user: User?
private var handle: AuthStateDidChangeListenerHandle?
public init() {
setupKeychainSharing()
registerStateListener()
}
public func signIn() {
if Auth.auth().currentUser == nil {
Auth.auth().signInAnonymously()
}
}
public func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print("error when trying to sign out: \(error.localizedDescription)")
}
}
private let accessGroup = "XXXXXXX.dev.peterfriese.App"
private func setupKeychainSharing() {
do {
let auth = Auth.auth()
auth.shareAuthStateAcrossDevices = true
try auth.useUserAccessGroup(accessGroup)
}
catch let error as NSError {
print("Error changing user access group: %#", error)
}
}
private func registerStateListener() {
if handle == nil {
handle = Auth.auth().addStateDidChangeListener({ (auth, user) in
self.user = user
if let user = user {
if user.isAnonymous {
self.logger.debug("User signed in anonymously with user ID \(user.uid).")
}
else {
self.logger.debug("User signed in with user ID \(user.uid). Email: \(user.email ?? "(empty)"), display name: [\(user.displayName ?? "(empty)")]")
}
}
else {
self.logger.debug("User signed out.")
self.signIn()
}
})
}
}
}
UPDATED: I have a project that is using Firebase Firestore. I have snapshot listeners set up to my model objects. I start the snapshot listeners when the app starts. My understanding is that if you start a snapshot listener once that's all you need.
My issue is that when I create a new object in the app I must restart the snapshot listener in order for it to see the changes. I am wondering if I am making a mistake somewhere. Here are the files I think are important. If anything else is needed please let me know.
Main
Main where I create the environment objects for the project and start the UserRepository snapshot listener.
#main
struct TheExchangeApp: App {
// #EnvironmentObjects
#StateObject private var userRepository = UserRepository()
#StateObject private var authListener = AuthSession(userRepository: UserRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userRepository)
.environmentObject(authListener)
}
}
}
UserRepository.swift
This is the user repository were I create the snapshot listener.
class UserRepository: ObservableObject {
// Access to Firestore Database
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistraion?
#Published var users = [User]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
// Add a SnapshotListener to the User Collection.
if snapshotListner == nil {
db.collection("users").addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Users.")
return
}
self.users = documents.compactMap { user in
do {
return try user.data(as: User.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
func stopSnapshotListener() {
if snapshotListener != nil {
snapshotListener?.remove()
snapshotListener = nil
}
}
func createNewUser(userUid: String, user: User) {
do {
let _ = try self.db.collection(FirestoreCollection.users).document(userUid).setData(from: user)
if snapshotListener == nil {
print("createNewUser snapshotListener is nil")
} else if snapshotListener != nil {
print("createNewUser snapshotListener is not nil")
}
} catch let error as NSError {
print("UserRepository - createNewUser Error: \(error)")
}
}
}
AuthSession
Here I start a combine publisher on the userRepository snapshot listener. The publisher is not getting an update when I create a new user in SignUpView below. If I restart the snapshot listener in UserRepository I get the published value.
class AuthSession: ObservableObject {
let db = Firestore.firestore()
var userRepository: UserRepository
#Published var currentUser: User?
private var cancellables = Set<AnyCancellable>()
init(userRepository: UserRepository) {
self.userRepository = userRepository
self.startCombine()
}
func startCombine() {
userRepository
.$users
.receive(on: RunLoop.main)
.map { users in
users
.first(where: { $0.id == self.currentUserUid})
}
.assign(to: \.currentUser, on: self)
.store(in: &cancellables)
}
}
SignUp View
Here is where I create a new user. The snapshot listener does not work unless I restart. I know because the combine publisher in AuthSession does not fire.
struct SignUpView: View {
#EnvironmentObject var authSession: AuthSession
let db = Firestore.firestore()
var body: some View {
Button(action: {
self.signUp()
}, label: {
Text("Sign Up")
})
}
func signUp() {
authSession.listen()
let newUser = User(email: myemail#email.com, name: "Hans")
self.authSession.userRepository.createNewUser(userUid: user.uid, user: newUser)
}
}
Any help here would be greatly appreciated.
I believe I've sorted out the issue. A developer I am working with changed the rules on Firestore. These new rules included the following for the User collection.
match /users/{userId} {
allow create: if request.auth.uid == userId;
allow read: if request.auth != null;
allow write: if request.auth.uid == userId;
Once I removed these rules everything works as expected. I hope this helps someone else out there.
I have an application that is:
The first view is a list of stores (the list has been read from Firebase successfully) because there is a (List)
The second view is the store details (here, I couldn't read the data from Firebase)
#ObservedObject var viewModel = StoreViewModel()
var body: some View {
ScrollView {
VStack {
Text(viewModel.stores.name)}}
How read this data from Firebase ?
class StoreViewModel: ObservableObject {
#Published var stores = [StoreView]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("Stores").addSnapshotListener{(querySnapshot, error)in
guard let documents = querySnapshot?.documents else {
print("No documents in Firebease")
return
}
self.stores = documents.compactMap { queryDocumentSnapshot -> StoreView? in
return try? queryDocumentSnapshot.data(as: StoreView.self)
}
}
}
}
First, your view model never reads from the database because fetchData() is never called. Therefore, consider calling it automatically in the view model's initializer:
class StoreViewModel: ObservableObject {
#Published var stores = [StoreView]()
private let db = Firestore.firestore()
init() {
db.collection("Stores").addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents in Firebease")
if let error = error {
print(error)
}
return
}
self.stores = documents.compactMap { queryDocumentSnapshot -> StoreView? in
return try? queryDocumentSnapshot.data(as: StoreView.self)
}
}
}
}
Second, stores is an array of StoreView objects, so you must pick one the elements of the array to display any data from it:
Text(viewModel.stores[0].name) // this will display the name of the first store added to the array