How can I save an image using CloudKit? - ios

I've run into a problem saving images using iCloud. After following a few tutorials I was able to save and fetch records containing strings from iCloud but I can't figure out how to do the same with images. Here is some of my code below so that maybe someone can help me figure out what to do next. Any help is appreciated!
// saving to cloud
static func save(user: AppUser, completion: #escaping (Result<AppUser, Error>) -> ()) {
let userRecord = CKRecord(recordType: RecordType.User)
userRecord["displayName"] = user.displayName as CKRecordValue
userRecord["pfp"] = user.pfp as CKRecordValue // <-- **not sure if this is right?**
userRecord["username"] = user.username as CKRecordValue
userRecord["password"] = user.password as CKRecordValue
userRecord["bio"] = user.bio as CKRecordValue
CKContainer.default().privateCloudDatabase.save(userRecord) { (record, err) in
DispatchQueue.main.async {
if let err = err {
completion(.failure(err))
return
}
guard let record = record else {
completion(.failure(CloudFuncUserError.recordFailure))
return
}
let recordID = record.recordID
guard let displayName = record["displayName"] as? String else {
completion(.failure(CloudFuncUserError.castFailure))
return
}
guard let pfp = record["pfp"] as? CKAsset else {
completion(.failure(CloudFuncUserError.castFailure))
return
} // <-- **not sure if this is right?**
guard let username = record["username"] as? String else {
completion(.failure(CloudFuncUserError.castFailure))
return
}
guard let password = record["password"] as? String else {
completion(.failure(CloudFuncUserError.castFailure))
return
}
guard let bio = record["bio"] as? String else {
completion(.failure(CloudFuncUserError.castFailure))
return
}
let appUser = AppUser(recordID: recordID, displayName: displayName, pfp: pfp, username: username, password: password, bio: bio)
completion(.success(appUser))
}
}
}
initialization
#main
struct TestForReleaseApp: App {
var user = appUser()
var isUser = AppUser(displayName: "", pfp: CKAsset <-- (this is a placeholder, I don't know what goes here), username: "", password: "", bio: "")
var posts = Posts()
var post = Post(displayName: "", username: "", content: "", createdAt: Date(), star: 0, stared: 0, repost: 0, reposted: 0, share: 0, shared: 0, comment: 0, commented: 0, comments: [""], isPublic: 0, isPremium: 0)
var body: some Scene {
WindowGroup {
ContentView(post: post, user: isUser, appUser: user).environmentObject(user).environmentObject(posts).preferredColorScheme(.dark)
}
}
}

Related

Unexpectedly found nil while unwrapping... MessageKit

I have a struct set up for Messages and each time I got to load messages in app, I receive an unexpectedly found nil error on this line of code -->
var chatPartnerId: String {
return isFromCurrentUser ? toID! : fromID! // where I get the error
}
I can't figure out what Im doing wrong here at all.
Here's the class setup:
struct Message: MessageType {
let id: String?
var messageId: String {
return id ?? UUID().uuidString
}
var content: String?
var toID: String?
var fromID: String?
var isFromCurrentUser = Bool()
var chatPartnerId: String {
return isFromCurrentUser ? toID! : fromID!
}
let sentDate: Date
let sender: SenderType
var image: UIImage?
var downloadURL: URL?
var kind: MessageKind {
if let image = image {
let mediaItem = ImageMediaItem(image: image)
return .photo(mediaItem)
} else {
return .text(content ?? "")
}
}
init(user: User, content: String, fromID: String, toID: String) {
sender = Sender(senderId: user.uid!, displayName: user.name!)
self.content = content
self.fromID = fromID
self.toID = toID
sentDate = Date()
id = nil
}
init(user: User, image: UIImage) {
sender = Sender(senderId: user.uid!, displayName: user.name!)
self.image = image
content = ""
fromID = ""
toID = ""
sentDate = Date()
id = nil
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard
let sentDate = data["created"] as? Timestamp,
let senderId = data["senderId"] as? String,
let fromID = data["fromID"] as? String,
let toID = data["toID"] as? String,
let senderName = data["senderName"] as? String
else {
return nil
}
id = document.documentID
self.sentDate = sentDate.dateValue()
sender = Sender(senderId: senderId, displayName: senderName)
self.isFromCurrentUser = fromID == Auth.auth().currentUser?.uid
if let content = data["content"] as? String {
self.content = content
downloadURL = nil
} else if let urlString = data["url"] as? String, let url = URL(string: urlString) {
downloadURL = url
content = ""
} else {
return nil
}
}
}
// MARK: - DatabaseRepresentation
extension Message: DatabaseRepresentation {
var representation: [String: Any] {
var rep: [String: Any] = [
"created": sentDate,
"senderId": sender.senderId,
"fromID": fromID,
"toID": toID,
"senderName": sender.displayName
]
if let url = downloadURL {
rep["url"] = url.absoluteString
} else {
rep["content"] = content
}
return rep
}
}
// MARK: - Comparable
extension Message: Comparable {
static func == (lhs: Message, rhs: Message) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: Message, rhs: Message) -> Bool {
return lhs.sentDate < rhs.sentDate
}
}
Both toID and fromID are optionals and may be nil. Avoid force unwrapping the optional (and actually avoid force unwrapping anything else, with very rare exceptions), like you do in the problematic statement.
Instead, you can:
Don't be afraid to return an optional:
var chatPartnerId: String? { // <-- returns optional
return isFromCurrentUser ? toID : fromID
}
In many cases it's much better to deal with the nil as a condition that helps you understand the state of the app. For example nil may mean you should skip the processing of such message.
You can return a default bogus ID, or an empty string:
var chatPartnerId: String {
guard let id = isFromCurrentUser ? toID : fromID else {
return "" // <-- returns bogus ID
}
return id
}
You can change the property to be required:
var toID: String // <-- not optional
var fromID: String // <-- not optional
Looking at all of your inits I see none of them allows these paramters to be nil. So you don't need to make them optional.

How do I take the name of the Firebase document? Swift

How do I take the name of the Firebase document, directly from the information of the logged in user?
Basically I have a user collection and a Degree Course collection.
When I use the func
GetCorsodiLaurea
I don't want to manually insert the document name in .document ("")
But I would like to automatically take the name of the document directly from the user's info
The field that declares which course is connected to the user is "TipoCorso".
As you can see in the Degree Courses collection there is the value of the "TipoCorso" field
Here is the code of the function and a screen of the Firebase Database:
import SwiftUI
import Firebase
import FirebaseFirestoreSwift
import FirebaseDatabase
class ProfileViewModel : ObservableObject{
#Published var userInfo = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
#Published var userDegree = userDegreeModel(Name: "", TotalSubjects: "")
var ref = Firestore.firestore()
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
fetchUser()
GetCorsodiLaurea()
}
func fetchUser() {
ref.collection("users").document(uid).getDocument { [self] (doc, err) in guard let user = doc else { return }
let Nome = user.data()?["Nome"] as! String
let Cognome = user.data()?["Cognome"] as! String
let photoURL = user.data()?["photoURL"] as! String
let Nomeintero = user.data()?["Nomeintero"] as! String
let Tipocorso = user.data()?["Tipocorso"] as! String
let Corsodilaurea = user.data()?["Corsodilaurea"] as! String
DispatchQueue.main.async {
self.userInfo = UserModel(Nome: Nome, Cognome: Cognome, photoURL: photoURL, Nomeintero: Nomeintero, Corsodilaurea: Corsodilaurea, Tipocorso: Tipocorso) }
}
}
func GetCorsodiLaurea() {
db.collection("DegreeCourses").document(self.userInfo.Tipocorso).getDocument { [self] (doc, err) in guard let DegreeCourses = doc else { return }
let Name = DegreeCourses.data()?["Name"] as! String
let TotalSubjects = DegreeCourses.data()?["TotalSubjects"] as! String
// [END doc_reference]
// [END collection_reference]
DispatchQueue.main.async {
self.userDegree = userDegreeModel(Name: Name, TotalSubjects: TotalSubjects)
}
}
}
}
User
DegreeCourses
When you call the fetchUeser() function it looks like you are populating the UserModel with the specific user's Tipocorso.
So in the GetCorsodiLaurea function you can call Tipocorso member in userInfo variable.
ref.collection("DegreeCourses").document(self.userInfo.Tipocorso).getDocument { [self] (doc, err) in guard let DegreeCourses = doc else { return }
Edit: You are most likely getting the error because the fetchUsers() function hasn't completed fully (as it is waiting for Firebase to respond) but the execution has already proceeded to the GetCorsodiLaurea() function.
To fix this add, a escaping closure to the fetchUsers() function and call the GetCorsodiLaurea() function in the closure. This way, the compiler won't try and execute the functions asynchronously.
import SwiftUI
import Firebase
import FirebaseFirestoreSwift
import FirebaseDatabase
class ProfileViewModel : ObservableObject{
#Published var userInfo = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
#Published var userDegree = userDegreeModel(Name: "", TotalSubjects: "")
var ref = Firestore.firestore()
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
//GetCorsodilaurea() will only get called after fetchUser() is complete
fetchUser(completion: {
GetCorsodiLaurea()
})
}
func fetchUser(completion: #escaping () -> Void)) {
ref.collection("users").document(uid).getDocument { [self] (doc, err) in guard let user = doc else { return }
let Nome = user.data()?["Nome"] as! String
let Cognome = user.data()?["Cognome"] as! String
let photoURL = user.data()?["photoURL"] as! String
let Nomeintero = user.data()?["Nomeintero"] as! String
let Tipocorso = user.data()?["Tipocorso"] as! String
let Corsodilaurea = user.data()?["Corsodilaurea"] as! String
//don't do this async
self.userInfo = UserModel(Nome: Nome, Cognome: Cognome, photoURL: photoURL, Nomeintero: Nomeintero, Corsodilaurea: Corsodilaurea, Tipocorso: Tipocorso)
completion()
}
}
func GetCorsodiLaurea() {
db.collection("DegreeCourses").document(self.userInfo.Tipocorso).getDocument { [self] (doc, err) in guard let DegreeCourses = doc else { return }
let Name = DegreeCourses.data()?["Name"] as! String
let TotalSubjects = DegreeCourses.data()?["TotalSubjects"] as! String
// [END doc_reference]
// [END collection_reference]
DispatchQueue.main.async {
self.userDegree = userDegreeModel(Name: Name, TotalSubjects: TotalSubjects)
}
}
}

Display last message in chat with firestore

How I can get and display last message in my chat?
For test, I created four users with test messages. Now I can display only last message for all users. I mark red color.
Also I use firebase to save messages and create channels.
Struct in firebase look like this:
- Chats
- channel id
- document data (then be stored ID and NAME of channel)
- collection thread
- documents data (then be stored MESSAGES)
My struct in channel:
struct Channel {
let id: String?
let name: String
init(name: String) {
id = nil
self.name = name
}
init?(document: DocumentSnapshot) {
let data = document.data()!
guard let name = data["name"] as? String else {
return nil
id = document.documentID
self.name = name
}
}
extension Channel: DatabaseRepresentation {
var representation: [String : Any] {
var rep = ["name": name]
if let id = id {
rep["id"] = id
}
return rep
}
}
And my struct message, I use MessageKit:
struct Message: MessageType {
let id: String?
let content: String
let sentDate: Date
let sender: SenderType
var kind: MessageKind {
if let image = image {
return .photo(ImageMediaItem.init(image: image))
} else {
return .text(content)
}
}
var messageId: String {
return id ?? UUID().uuidString
}
var image: UIImage? = nil
var downloadURL: URL? = nil
init(profile: Profile, content: String) {
sender = Sender(id: profile.id, displayName: profile.name)
self.content = content
sentDate = Date()
id = nil
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let sentDate = (data["created"] as? Timestamp)?.dateValue() else {
return nil
}
guard let senderID = data["senderID"] as? String else {
return nil
}
guard let senderName = data["senderName"] as? String else {
return nil
}
id = document.documentID
self.sentDate = sentDate
sender = Sender(id: senderID, displayName: senderName)
if let content = data["content"] as? String {
self.content = content
downloadURL = nil
} else if let urlString = data["url"] as? String, let url = URL(string: urlString) {
downloadURL = url
content = ""
} else {
return nil
}
}
}
extension Message: DatabaseRepresentation {
var representation: [String : Any] {
var rep: [String : Any] = [
"created": sentDate,
"senderID": sender.senderId,
"senderName": sender.displayName
]
if let url = downloadURL {
rep["url"] = url.absoluteString
} else {
rep["content"] = content
}
return rep
}
}
For load my chennels I use code below:
fileprivate func observeQuery() {
guard let query = query else { return }
listener = query.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { (change) in
self.handleDocumentChange(change)
}
}
}
private func handleDocumentChange(_ change: DocumentChange) {
guard let channel = Channel(document: change.document) else {
return
}
switch change.type {
case .added:
addChannelToTable(channel)
case .modified:
updateChannelInTable(channel)
case .removed:
removeChannelFromTable(channel)
}
}
private func addChannelToTable(_ channel: Channel) {
guard !channels.contains(channel) else {
return
}
channels.append(channel)
channels.sort()
guard let index = channels.index(of: channel) else {
return
}
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func updateChannelInTable(_ channel: Channel) {
guard let index = channels.index(of: channel) else {
return
}
channels[index] = channel
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func removeChannelFromTable(_ channel: Channel) {
guard let index = channels.index(of: channel) else {
return
}
channels.remove(at: index)
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
I think need update my Channel struct. But how to do it?
And how to correct load and display last message from firebase?
If need more info pls tell me, I will update my question.
If the question is how to get only the last message from Firestore, you need to define how to determine what the last message is. That's usually done via a timestamp - the latest timestamp will be the last message.
The structure in the question is a little unclear so let me provide a simple example.
messages //collection
document_0 //documentID auto-generated
msg: "Last Message"
timestamp: "20191201"
document_1
msg: "First message"
timestamp: "20190801"
document_2
msg: "A message in the middle"
timestamp: "20191001"
As you can see, no matter what order they are written to Firestore, it's clear that the one with the latest timestamp (20191201 ) is the last message.
To get the last message we need a query that does two things:
1) Query the messages node, sort descending, which will put the last message 'at the top'
2) Limit the query to 1, which will get that message.
func readLastMessage() {
let ref = Firestore.firestore().collection("messages").order(by: "timestamp", descending: true).limit(to: 1)
ref.getDocuments(completion: { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
if let doc = snapshot.documents.first {
let docID = doc.documentID
let msg = doc.get("msg")
print(docID, msg)
}
})
}
and the output
Last Message
The above code gets the last message but could be expanded upon by adding an observer instead of getDocuments in the same fashion that will notify the app when there's a new last message.

Firestore iOS - Delete function in UITableview

I am trying to delete a document from Firestore that appears as a UITableViewCell on my UITableView using the swipe to delete function.
var sourseArray : [Sourse] = []
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let sourseItem = sourseArray[indexPath.row]
Firestore.firestore().collection("sourses").document(sourseItem.documentId).delete(completion: { (error) in
if let error = error {
debugPrint("Could not delete thought: \(error.localizedDescription)")
}
})
}
}
When I swipe and hit the "delete" button. This error appears
*** Terminating app due to uncaught exception 'FIRInvalidArgumentException', reason: 'Invalid document reference.
Document references must have an even number of segments, but sourses
has 1'
I adjusted the "rules" of my Firestore database to allow for deleting.
After some research it appears that I'm not referencing the correct document somehow. Is it a bad reference or is the error something else?
Also here is what a "sourse" model is.
class Sourse {
private(set) var name: String!
private(set) var content: String!
private(set) var timeStamp: Date!
private(set) var documentId: String!
private(set) var userId: String!
init(name: String, timeStamp: Date, content: String, documentId: String, userId: String) {
self.name = name
self.content = content
self.timeStamp = timeStamp
self.documentId = documentId
self.userId = userId
}
}
//EDIT
I just noticed I did not add a documentId when creating a new sourse. As seen below.
#IBAction func addSourse(_ sender: Any) {
Firestore.firestore().collection(SOURSES_REF).addDocument(data: [
NAME : sourseTextField.text ?? "",
CONTENT : contentTextField.text ?? "",
TIMESTAMP : FieldValue.serverTimestamp(),
USERNAME : Auth.auth().currentUser?.displayName ?? "",
USER_ID : Auth.auth().currentUser?.uid ?? ""
]) { (err) in
if let err = err {
debugPrint("Error adding document document: \(err)")
} else {
self.navigationController?.popViewController(animated: true)
}
}
}
However, that is also the way it was in my tutorial and it worked fine.
///Edit 2 To show how I am fetching it.
func loadData() {
db.collection("sourses").getDocuments() { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date ?? Date()
let documentId = data["documentId"] as? String ?? ""
let userId = data["userId"] as? String ?? ""
let newSourse = Sourse(name:name, timeStamp: timeStamp, content:content, documentId: documentId, userId: userId)
self.sourseArray.append(newSourse)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
Answer: The function to swipe to delete was correct the whole time. As Dopapp pointed out, I was incorrectly loading my document.Id.
If the problem is indeed that your documentId is wrong, you may be retrieving it incorrectly. Here is a quick example of how to create your object with the right id:
collection.addSnapshotListener { (snapshot, error) in
if let documents = snapshot?.documents {
for document in documents {
guard let data = document.data() else { continue }
let id = document.documentID
let sourseItem = Sourse(name: data['name'], ..., documentId: id, ...)
// use sourseItem
}
}
}
If you are doing something similar, I would check if the document ids are being swapped between objects. If so, that might suggest an async-related problem.
For your particular case, loadData() should look like this:
func loadData() {
db.collection("sourses").getDocuments() { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date ?? Date()
let documentId = document.documentID
let userId = data["userId"] as? String ?? ""
let newSourse = Sourse(name:name, timeStamp: timeStamp, content:content, documentId: documentId, userId: userId)
self.sourseArray.append(newSourse)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}

User info singleton with userdefaults returns nil

I am trying to store user info into userDefaults and assign those to singleton. I am saving the info after successful API call. I know for sure that it saves all those values. But for some reason the singleton returns nil.
This is my User class:
class User {
var id: Int
var name: String
var email: String
var profile_image_url: String
var token: String
init(id: Int, name: String, email: String, profile_image_url: String, token: String) {
self.id = id
self.name = name
self.email = email
self.profile_image_url = profile_image_url
self.token = token
}
init?(dict: [String: JSON]) {
guard let id = dict["id"]?.intValue,
let name = dict["name"]?.stringValue,
let email = dict["email"]?.stringValue,
let profile_image_url = dict["profile_image_url"]?.stringValue,
let token = dict["token"]?.stringValue
else{ return nil }
self.id = id
self.name = name
self.email = email
self.profile_image_url = profile_image_url
self.token = token
}
class var sharedInstance: User {
struct Singleton {
static var instance : User = {
let userDefaults = UserDefaults.standard
let keychain = KeychainSwift()
let user = User(
id: userDefaults.integer(forKey: "user_id"),
name: userDefaults.string(forKey: "name")!,
email: userDefaults.string(forKey: "email")!,
profile_image_url: userDefaults.string(forKey: "profile_image_url")!,
token: keychain.get("token")!
)
return user
}()
}
return Singleton.instance
}
}
And this is how I save values after API call:
class func saveUserData(user_id: Int, name: String, email: String, profile_image_url: String, token:String) {
let userDefaults = UserDefaults.standard
let keychain = KeychainSwift()
keychain.clear()
userDefaults.set(user_id, forKey: "user_id")
userDefaults.set(name, forKey: "name")
userDefaults.set(email, forKey: "email")
userDefaults.set(profile_image_url, forKey: "profile_image_url")
keychain.set(token, forKey: "token")
}
What I am doing wrong, is it even right approach?
As I have mentioned, if you are coding in Swift 4 you should take advantage of the Codable protocol. Make your user a struct that conforms to Codable. You can provide your User struct custom coding keys if needed (not required). Add a method to save your user data to user defaults and create a fallible initializer to read your user data from it:
struct User: Codable {
let id: Int
let name: String
let email: String
let profileImageUrl: String
let token: String
init(id: Int, name: String, email: String, profileImageUrl: String, token: String) {
self.id = id
self.name = name
self.email = email
self.profileImageUrl = profileImageUrl
self.token = token
}
func saveToDefaults() -> Bool {
let encoder = JSONEncoder()
do {
let json = try encoder.encode(self)
UserDefaults.standard.set(json, forKey: "User\(id)Key")
return true
} catch {
print(error)
return false
}
}
init?(data: Data) {
do {
self = try JSONDecoder().decode(User.self, from: data)
} catch {
print(error)
return nil
}
}
init?(id: Int) {
guard let data = UserDefaults.standard.data(forKey: "User\(id)Key") else { return nil }
self.init(data: data)
}
}
Playground testing
let user = User(id: 1, name: "a name", email: "name#email.com", profileImageUrl: "https://i.stack.imgur.com/Xs4RX.jpg", token: "anyTokenString")
if user.saveToDefaults() {
print("success")
if let user = User(id: 1) {
print(user.id)
print(user.name)
print(user.email)
print(user.profileImageUrl)
print(user.token)
} else {
print("there is no user with id 1")
}
}
This will print
success
1
a name
name#email.com
https://i.stack.imgur.com/Xs4RX.jpg
anyTokenString

Resources