Update data from SwiftUI to Firestore database - google-cloud-firestore

I'm working on a swiftui app that streams movies and it is linked to Firebase. Currently, I'm trying to get the time a video (through AVPlayer) has been paused and update that to my database time field so that users can start where they stopped. I'm trying to update the data using a Codable format and also have imported FirebaseFirestoreSwift. However, the code is not making any changes to the database so was wondering if I can get help. Querying the data from the database works perfectly, I just need guidance on how to update different fields of the movies collection from SwiftUI. Many thanks!
import FirebaseFirestoreSwift
...
#State var SectionX: movies
....
//when video is ended record time and update value to firestore time field
.onDisappear {
avPlayer.pause()
let db = Firestore.firestore()
let currentTime = Float(avPlayer.currentTime().seconds)
print(currentTime)
db.collection("movies").document(SectionX.id)
SectionX.time = String(currentTime)
func updateTime(_ mov: movies) {
do {
let _ = try db.collection("movies").document(SectionX.time).setData(from: currentTime)
}
catch {
print(error)
}
}
}
struct movies : Codable,Identifiable {
var id: String
var title: String
var img: String
var video: String
var description: String
var genre: String
var maturity: String
var time: String
var year: String
var acting: String
var directors: String
var writers: String
var producers: String
var watched: Int
var rating: Int
}
class getMoviesData : ObservableObject{
#Published var datas = [movies]()
init(){
let db = Firestore.firestore()
db.collection("movies").addSnapshotListener{ (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.get("id") as! String
let title = i.document.get("title") as! String
let img = i.document.get("img") as! String
let video = i.document.get("video") as! String
let description = i.document.get("description") as! String
let genre = i.document.get("genre") as! String
let maturity = i.document.get("maturity") as! String
let time = i.document.get("time") as! String
let year = i.document.get("year") as! String
let acting = i.document.get("acting") as! String
let directors = i.document.get("directors") as! String
let writers = i.document.get("writers") as! String
let producers = i.document.get("producers") as! String
let watched = i.document.get("watched") as! Int
let rating = i.document.get("rating") as! Int
self.datas.append(movies(id: id, title: title, img: img, video: video, description: description, genre: genre, maturity: maturity, time: time, year: year, acting: acting, directors: directors, writers: writers, producers: producers, watched: watched, rating: rating))
}
}
}
}

Got it working! Just had to use updateData method.
let x = db.collection("movies").document(SectionX.id).documentID
db.collection("movies").document(x).updateData(["time":currentTime])

Related

problem with adding element to swift array

I am creating an application. I make a request to the firebase store, after that I add the result to the array, but in the end, when the array is displayed from viewDidLoad or other functions, I get an empty array. But if you make a conclusion immediately after the request, then everything is displayed correctly
`
import UIKit
import Firebase
import FirebaseStorage
import FirebaseFirestore
class CatalogVC: UIViewController {
struct Item: Codable {
var title: String
var price: Int
var description: String
var imageUrl: String
}
#Published var items: [Item] = []
let database = Firestore.firestore()
#IBOutlet weak var textViewCatalog: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let settings = FirestoreSettings()
Firestore.firestore().settings = settings
itemsList()
print(items)
showCatalogVC()
}
#IBAction func showCatalogTapped() {
}
private func showCatalogVC() {
print("SHOW CATALOG")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let dvc = storyboard.instantiateViewController(withIdentifier: "CatalogVC") as! CatalogVC
self.present(dvc, animated: true, completion: nil)
}
func itemsList(){
database.collection("catalog")
.getDocuments { (snapshot, error) in
self.items.removeAll()
if let snapshot {
for document in snapshot.documents{
let docData = document.data()
let title: String = docData["title"] as? String ?? ""
let imageUrl: String = docData["imageUrl"] as? String ?? ""
let description: String = docData["description"] as? String ?? ""
let price: Int = docData["price"] as? Int ?? 0
let item: Item = Item(title: title, price: price, description: description, imageUrl: imageUrl)
self.items.append(item)
}
}
}
}
}
`
I am creating an application. I make a request to the firebase store, after that I add the result to the array, but in the end, when the array is displayed from viewDidLoad or other functions, I get an empty array. But if you make a conclusion immediately after the request, then everything is displayed correctly
Getting data from Firebase is asynchronous process. So here you should make everything after loading data in closure database.collection("catalog").getDocuments {...}.
func itemsList(){
database.collection("catalog")
.getDocuments { (snapshot, error) in
self.items.removeAll()
if let snapshot {
for document in snapshot.documents{
let docData = document.data()
let title: String = docData["title"] as? String ?? ""
let imageUrl: String = docData["imageUrl"] as? String ?? ""
let description: String = docData["description"] as? String ?? ""
let price: Int = docData["price"] as? Int ?? 0
let item: Item = Item(title: title, price: price, description: description, imageUrl: imageUrl)
self.items.append(item)
}
}
print(self.items) //print items to see them
//here use items data
}
}

Thread 1: EXC_BAD_INSTRUCTION when fetching data

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
}
}

Change a value in my UserModel (class) based on a userid

I have a UserModel:
class UserModel {
var uid: String?
var username : String?
var email: String?
var profileImageUrl: String?
var dateOfBirth: String?
var registrationDate: Int?
var isFollowing: Bool?
var accessLevel: Int?
var onlineStatus: Bool?
init(dictionary: [String : Any]) {
uid = dictionary["uid"] as? String
username = dictionary["username"] as? String
email = dictionary["email"] as? String
profileImageUrl = dictionary["profileImageUrl"] as? String
dateOfBirth = dictionary["dateOfBirth"] as? String
registrationDate = dictionary["userRegistrationDate"] as? Int
accessLevel = dictionary["accessLevel"] as? Int
onlineStatus = dictionary["onlineStatus"] as? Bool
}
}
And I also have a value like [12ih12isd89 : True]
I want to change the value "onlineStatus" for the user "12ih12isd89" to True and I thought the right way to do this is updateValue(:forKey:). But my class UserModel does not have updateValue(:forKey:).
How can I use this in my existing model?
Edit:
How I get the data:
func fetchAllUsers (completion: #escaping ([UserModel]) -> Void) {
let dispatchGroup = DispatchGroup()
var model = [UserModel]()
let db = Firestore.firestore()
let docRef = db.collection("users")
dispatchGroup.enter()
docRef.getDocuments { (querySnapshot, err) in
for document in querySnapshot!.documents {
let dic = document.data()
model.append(UserModel(dictionary: dic))
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(model)
}
}
To me it looks like you need to find the right object in the array and update the property
let dict = ["12ih12isd89" : true]
var model = [UserModel]()
if let user = model.first(where: {$0.uid == dict.keys.first!}) {
user.onlineStatus = dict.values.first!
}
Depending on what ["12ih12isd89" : true] really is you might want to change the access from dict.keys.first! that I used
If your value dictionary contains more than one user, you can use a for loop like this:
var model = [UserModel]()
//Some initalization ...
let values = ["12ih12isd89" : true]
for (k, v) in values {
model.filter({$0.uid == k}).first?.onlineStatus = v
}

Swift Firebase chat room

I have chat app with possibility send messages one to one (fromId/toId). I want to upgrade it with chat rooms. How i can do that? What DB structure do i need for ChatingRoom? What else i need to do that?
My User.swift model:
import Foundation
import Firebase
class User: NSObject {
var id: String?
var name: String?
var login: String?
var email: String?
var profileImageUrl: String?
var role: String?
var isOnline: String?
init(dictionary: [String: AnyObject]) {
self.isOnline = dictionary["isOnline"] as? String
self.id = dictionary["userID"] as? String
self.name = dictionary["name"] as? String
self.login = dictionary["username"] as? String
self.email = dictionary["email"] as? String
self.profileImageUrl = dictionary["profileImageUrl"] as? String
self.role = dictionary["role"] as? String
}
}
Message.swift model:
import UIKit
import Firebase
class Message: NSObject {
var fromId: String?
var text: String?
var timestamp: NSNumber?
var toId: String?
var imageUrl: String?
var videoUrl: String?
var imageWidth: NSNumber?
var imageHeight: NSNumber?
init(dictionary: [String: Any]) {
self.fromId = dictionary["fromId"] as? String
self.text = dictionary["text"] as? String
self.toId = dictionary["toId"] as? String
self.timestamp = dictionary["timestamp"] as? NSNumber
self.imageUrl = dictionary["imageUrl"] as? String
self.videoUrl = dictionary["videoUrl"] as? String
self.imageWidth = dictionary["imageWidth"] as? NSNumber
self.imageHeight = dictionary["imageHeight"] as? NSNumber
}
func chatPartnerId() -> String? {
return fromId == Auth.auth().currentUser?.uid ? toId : fromId
}
}
Well currently you have From / To.
So To is going to be a room rather than a person.
Users will need to be able to join a room (or rooms) in order to see the messages that are sent to that room.
So your need a Rooms node.
If you have a messages node then you can just sort by To (room) instead of To (user) to get all the messages sent in that chat room. From will always be the User that wrote the message

create Realm DB for each user in Chat App

this one for sending message and save it to realm db
var messageIndex = try! Realm().objects(MessageRealm.self).sorted(byKeyPath: "timeStamp")
func didPressSend(text: String) {
if self.inputContinerView.inputTextField.text! != "" {
let messageDB = MessageRealm()
let realm = try! Realm()
let userRealm = UsersRealm()
messageDB.textDownloadded = text
messageDB.fromId = user!.fromId
messageDB.timeStamp = Date()
print(messageDB)
try! realm.write ({
print(realm.configuration.fileURL)
userRealm.msgs.append(messageDB)
//realm.create(MessageRealm.self, value: ["textDownloadded": text, "fromId": user!.fromId, "timeStamp": Date()])
})
if let userTitleName = user?.toId {
print(userTitleName)
OneMessage.sendMessage(text, thread: "AAAWatree", to: userTitleName, isPhoto: false, isVideo: false, isVoice: false, isLocation: false, timeStamp: date, completionHandler: { (stream, message) in
DispatchQueue.main.async {
OneMessage.sharedInstance.deleteCoreDataMessage()
}
self.inputContinerView.inputTextField.text! = ""
})
}
}
}
This for when recieving message im trying to save user (send id )
let realm = try! Realm()
userData.sender = sender
userData.toId = toUser
print(userData.sender)
print(userData.toId)
try! realm.write ({
realm.add(userData, update: true)
})
this my Realm Object Class
class MessageRealm: Object {
dynamic var textDownloadded = String()
dynamic var imageDownloadded = NSData()
dynamic var videoDownloadded = String()
dynamic var voiceDownloadded = String()
dynamic var fromId = String()
dynamic var timeStamp = Date()
dynamic var messageId = NSUUID().uuidString
let userSelect = List<UsersRealm>()
override class func primaryKey() -> String? {
return "messageId"
}
}
class UsersRealm: Object {
dynamic var sender = String()
dynamic var fromId = String()
dynamic var toId = String()
dynamic var lastMessage = String()
dynamic var timeStamp = Date()
dynamic var profileImage = NSData()
let msgs = List<MessageRealm>()
override class func primaryKey() -> String {
return "sender"
}
}
sending and reciving is ok and its save to realm db but all any user send message i recived in one user i want to seprate for every user have his sending and recive database i miss something here but i dont know i try to search nothing its long question but i cant figure out the soluation
and sorry for my week english
Thank you
If I understood your case correctly you're using a single realm url for all users that's why all your clients have the same data. You should probably create a separate realm for the conversation and share it between the users who participate in that chat. Please learn more about sharing realms in our docs at https://realm.io/docs/swift/latest/#access-control.