Currently, I am using a combination of Firebase authentication, database, and storage for my sign up process.
#objc func handleSignUp() {
guard let email = emailTextField.text else { return }
guard let password = passwordTextField.text else { return }
guard let fullName = fullNameTextField.text else { return }
guard let username = usernameTextField.text else { return }
Auth.auth().createUser(withEmail: email, password: password) { (user, error) in
//handle error
if let error = error {
print("Failed to create user with error", error.localizedDescription)
return
}
//set profile image
guard let profileImage = self.plusPhotoButton.imageView?.image else { return }
//upload data
guard let uploadData = profileImage.jpegData(compressionQuality: 0.3) else { return }
//place image in firbase storage
let filename = NSUUID().uuidString
let storage = Storage.storage()
let storageReference = storage.reference()
let imagesReference = storageReference.child("profile_images")
let uploadReference = imagesReference.child(filename)
uploadReference.putData(uploadData, metadata: nil) { (metadata, error) in
//handle error
if let error = error {
print("Failed to upload image to Firebase Storage with error", error.localizedDescription)
}
//profile image url
let profileImageURL = uploadReference.downloadURL
//user id
let uid = user?.user.uid
let dictionaryValues = ["name": fullName, "username": username, "profileImageURL": profileImageURL]
let values = [uid: dictionaryValues]
//save user info to database
Database.database().reference().child("users").updateChildValues(values) { (error, ref) in
print("Successfully created user and saved information to database")
}
}
}
This should be the code, which should be working fine.
However, xCode is giving me this error:
Heterogeneous collection literal could only be inferred to '[String : Any]'; add explicit type annotation if this is intentional
and is advising me to change
let dictionaryValues = ["name": fullName, "username": username, "profileImageURL": profileImageURL]
to
let dictionaryValues = ["name": fullName, "username": username, "profileImageURL": profileImageURL] as [String : Any]
As a result, I can compile and run the project.
However, when I press the sign-up button and therefore run the given function, the console is showing this error:
2019-09-30 19:15:13.049986+0200 5iveli0ns[965:28839] [] nw_connection_receive_internal_block_invoke [C2] Receive reply failed with error "Operation canceled"
and in the end I receive a Runtime Exception:
*** Terminating app due to uncaught exception 'InvalidFirebaseData', reason: '(updateChildValues:withCompletionBlock:) Cannot store object of type __SwiftValue at profileImageURL. Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.'
Is this an xCode 11 / Firebase 5.x.x related issue?
Generally speaking, Dictionaries infer their structure from the data they contain. So for example
let dict = [
"name": "Leroy",
"fav_food": "Pizza"
]
is a [String: String] Dictionary
Likewise
let dict = [
"name": "Leroy"
"age": 25
]
is a [String: Int] Dictionary.
What you have is
let dict = [
"name": "Leroy",
"username": "some username"
"profileImageUrl": URL //oops, not a string, its a URL!
]
When Dictionary values are consistent, they can be defined [String: String]. However, when the value types will vary, they are [String: Any]
The other issue is that Firebase doesn't support the URL type, so even if you used a Dictionary of [String: Any], Firebase will complain - as shown by the error.
The fix is easy - just use the path, which is a string of the url
if let url = uploadReference.downloadURL {
let path = url
let dict = [
"name": "Leroy",
"username": "some username",
"path": path //it's a string
]
//write dict to Firebase
}
COnvert
let profileImageURL = uploadReference.downloadURL
to String then feed it into the dictionaryValues array.
I hope that helps
Related
I am writing a new bit of code that allows a user to upload an image to a post that will be saved in Firebase database and storage. Then it will be shown in a UITableView on a separate Viewcontroller. On the part of code where I want to save the photo url to my database I am getting an error "Variable used within its own initial value". I have looked at other threads covering this issue and when I follow their advice I get a new set of errors. Here is what my code looks like and where I am getting the error. If there are any suggestions that would be great, thank you!
Here is code:
func postWithImageAndText() {
var imagesArray = [AnyObject]()
let postLength = textView.text.count
let numImages = imagesArray.count
let postRef = Database.database().reference().child("posts").childByAutoId()
let storageRef = Storage.storage().reference()
let uid = Auth.auth().currentUser
let pictureStorageRef = storageRef.child("users/profile/\(uid)/media")
let lowResImageData = (imagesArray[0] as! UIImage).jpegData(compressionQuality: 0.50)
if(postLength>0 && numImages>0)
{
let uploadTask = pictureStorageRef.putData(lowResImageData!,metadata: nil)
{metadata,error in
if (error == nil) {
let downloadUrl = storageRef.downloadURL { (url, error) in
guard let userProfile = UserService.currentUserProfile else { return }
let childUpdates = [
"author": [
"uid": userProfile.uid,
"username": userProfile.fullname,
"photoURL": userProfile.photoURL.absoluteString
],
"text": self.textView.text!,
"picture": downloadUrl!.absoluteString,
"timestamp": ["sv.":"timestamp"]
] as [String:Any]
}
}
}
}
}
`
Error is happening on this part: "picture": downloadUrl!.absoluteString,
As i can see yo got an "Variable used within its own initial value" error, that means you try to access initial value of variable within this variable, its not a good behavior.
I also see you dont use any data from your completion block, so try to use nil instead of your completion block and then try to access your downloadURL variable
Your updated code:
func postWithImageAndText() {
var imagesArray = [AnyObject]()
let postLength = textView.text.count
let numImages = imagesArray.count
let postRef = Database.database().reference().child("posts").childByAutoId()
let storageRef = Storage.storage().reference()
let uid = Auth.auth().currentUser
let pictureStorageRef = storageRef.child("users/profile/\(uid)/media")
if let lowResImageData = (imagesArray[0] as! UIImage).jpegData(compressionQuality: 0.50) {
if(postLength>0 && numImages>0)
{
pictureStorageRef.putData(lowResImageData, metadata: metaData) { (metadata, error) in
if error != nil {
print("Failed to upload image:", error as Any)
}
pictureStorageRef.downloadURL(completion: { (url, error) in
if error != nil {
print(error!)
}
if let imageUrl = url?.absoluteString {
guard let userProfile = UserService.currentUserProfile else { return }
let childUpdates = [
"author": [
"uid": userProfile.uid,
"username": userProfile.fullname,
"photoURL": userProfile.photoURL.absoluteString
],
"text": self.textView.text!,
"picture": imageUrl,
"timestamp": ["sv.":"timestamp"]
] as [String:Any]
}
})
}
}
}
}
How can I get the value from firstName from the inside:
func saveImage(name: String, postURL:URL, completion: #escaping ((_ url: URL?) -> ())){
//Get sspecific document from current user
let docRef = Firestore.firestore().collection("users").whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
var firstName = ""
// Get data
docRef.getDocuments { (querySnapshot, err) in
var firstName = ""
if let err = err {
print("ERROR: ")
print(err.localizedDescription)
return
} else if querySnapshot!.documents.count != 1 {
print("More than one documents or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
firstName = dataDescription?["firstname"] as! String
}
}
// This uploads the data
let dict = ["title": postDescriptionTitle.text!,
"description": postDescription.text!,
"Address": addressField.text!,
"Zipcode": zipcodeField.text!,
"timestamp": [".sv":"timestamp"],
"Author":firstName,
"postUrl": postURL.absoluteString]
as [String: Any]
self.ref.child("post").childByAutoId().setValue(dict)
}
It looks like it's out of scope, how can I store it or access it without storing it in another variable?
As you can see, I'm trying to upload the variable firstName to the database. So in this part:
"Author":firstName,
I should be getting the value so I can give it to Author
Just move the "upload data" part inside the completion block like this:
func saveImage(name: String, postURL:URL, completion: #escaping ((_ url: URL?) -> ())) {
//Get sspecific document from current user
let docRef = Firestore.firestore().collection("users").whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
// Get data
docRef.getDocuments { (querySnapshot, err) in
if let err = err {
print("ERROR: ")
print(err.localizedDescription)
return
} else if querySnapshot!.documents.count != 1 {
print("More than one documents or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
let firstName = dataDescription?["firstname"] as! String
// This uploads the data
let dict = ["title": self.postDescriptionTitle.text!,
"description": self.postDescription.text!,
"Address": self.addressField.text!,
"Zipcode": self.zipcodeField.text!,
"timestamp": [".sv":"timestamp"],
"Author": firstName,
"postUrl": postURL.absoluteString] as [String: Any]
self.ref.child("post").childByAutoId().setValue(dict)
}
}
}
Also for what are you using the completion argument in your saveImage function?
I am having a bit of an issue checking Firebase Storage when a user logs into my app. When a user logs in an image is created and stored successfully in Firebase database and storage, but every time a user logs out and logs back into the app the profileImage file (not the child value url string) is duplicated in Firebase Storage as a new and separate image file.
I would like the app when logging in to check the Firebase storage for the profileImage file, and if it exists then do not re-create that same image in storage. I understand the logic of checking Firebase for image and if does not exist, than create new image. I am just having some trouble with the syntax. Thanks for help in advance!
// login user with Facebook
func loginWithFacebook() {
let accessToken = FBSDKAccessToken.current()
guard let accessTokenString = accessToken?.tokenString else { return }
let credentials = FIRFacebookAuthProvider.credential(withAccessToken: accessTokenString)
FIRAuth.auth()?.signIn(with: credentials, completion: { (user, error) in
if error != nil {
print("Something went wrong with our FB user: ", error ?? "")
return
}
guard let uid = user?.uid else {
return
}
let imageName = NSUUID().uuidString
let storageRef = FIRStorage.storage().reference().child("profile_images").child("\(imageName).png")
let photoUrl = user?.photoURL
// check to see if current user already has stored image with URL
if FIRStorage.storage().reference().child("profile_images").child("\(imageName).png") == nil {
if let imageData = NSData(contentsOf: photoUrl!) {
storageRef.put(imageData as Data, metadata:nil) {
(metadata, error) in
if error != nil {
print(error!)
return
} else {
if let profileImageUrl = metadata?.downloadURL()?.absoluteString {
let values = ["name": user!.displayName!, "email": user!.email!, "profileImageUrl": profileImageUrl]
self.registerUserWithUID(uid: uid, values: values as [String : AnyObject])
}
}
}
}
} else {
print("image already exists")
}
print("Successfully logged in with our user: ", user ?? "")
self.delegate?.finishLoggingIn()
})
private func registerUserWithUID(uid: String, values: [String: AnyObject]) {
// create items in database upon creating user ---------------
let ref = FIRDatabase.database().reference()
let usersReference = ref.child("users").child(uid)
usersReference.updateChildValues(values, withCompletionBlock: { (err, ref) in
if err != nil {
print(err!)
return
}
print("user has been saved to Firebase database")
})
}
I think the issue is with the "if" condition. You are referencing the location, but not checking with the actual data in that location.
You can implement that using .observerSingleEventOf().
private func checkUser (imageName:String, _ completionHandler: #escaping(Bool) -> Void)
{
let userExists = FIRStorage.storage().reference().child("profile_images")
userExists.observeSingleEvent(of: .value, with:{(snapshot) in
if snapshot.hasChild(imageName){
print ("Image already exists")
completionHandler(false)
}
else
{
print ("Image does not exist")
print ("store the image")
}
})
}
`
I have a Registration Class that has 3 textfields for a username, email and password. When the user hits the Sign Up button, a handleRegister() function is called. This function takes the three values from these textfields.text and sends them to my Firebase database under a child node, containing their user id like in the image below:
My problem is that I want to be able to UPDATE any 3 of these values (email, name, password) OUTSIDE of the registration class. How do I do achieve this? Thank you. Here registration my code:
func handleRegister() {
guard let email = emailTextField.text, let password = passwordTextField.text, let name = usernameTextField.text else {
return
}
FIRAuth.auth()?.createUser(withEmail: email, password: password, completion: { (user: FIRUser?, error) in
if error != nil {
return
}
guard let uid = user?.uid else {
return
}
//successfully registered user.
let imageName = NSUUID().uuidString
let storageRef = FIRStorage.storage().reference().child("profile_images").child("\(imageName).png")
if let uploadData = UIImagePNGRepresentation(self.profileImageView.image!) {
storageRef.put(uploadData, metadata: nil, completion: { (metadata, error) in
if error != nil {
return
}
if let profileImageUrl = metadata?.downloadURL()?.absoluteString {
let values = ["name": name, "email": email, "password": password, "profileImageUrl": profileImageUrl]
self.registerUserIntoDatabaseWithUID(uid: uid, values: values as [String : AnyObject])
}
})
}
})
}
private func registerUserIntoDatabaseWithUID(uid: String, values: [String: AnyObject]) {
let ref = FIRDatabase.database().reference()
let usersReference = ref.child("users").child(uid)
usersReference.updateChildValues(values, withCompletionBlock: { (err, ref) in
if err != nil {
return
}
print("Successfully saved user to database.")
self.dismiss(animated: true, completion: nil)
})
}
You have two options:
Option 1: You need to save your user's database ID somewhere so you can use it later on in the app for situations just like this. You can save the ID in your Userdefaults or somewhere else that's a bit more secure.
Option 2: You can retrieve the ID of the logged in user by using Auth.auth()?.currentUser?.uid
guard let uid = Auth.auth()?.currentUser?.uid else { return }
When you have this ID you can update the value in the database like you are doing in registerUserIntoDatabaseWithUID().
func updateEmailAddress(text: String) {
guard let uid = Auth.auth()?.currentUser?.uid else { return }
let userReference = Database.database().reference.child("users/(uid)")
let values = ["email": text]
// Update the "email" value in the database for the logged in user
userReference.updateChildValues(values, withCompletionBlock: { (error, ref) in
if error != nil {
print(error.localizedDescription)
return
}
print("Successfully saved user to database.")
self.dismiss(animated: true, completion: nil)
})
}
Nota Bene
If you have any questions regarding this answer please add a comment.
The difference between this and the other answer is that I am
indicating how you can update in multiple locations as requested.
You'd want to look at using the data fan out approach. It deals with writing the data at mulitple locations. Here is a quick code sample:
let key = ref.child("posts").childByAutoId().key
let post = ["uid": userID,
"author": username,
"title": title,
"body": body]
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)
To read more about this approach see the documentation at:
https://firebase.google.com/docs/database/ios/read-and-write#update_specific_fields
I am rewriting my graph requests with the latest Swift3. I am following the guide found here - https://developers.facebook.com/docs/swift/graph.
fileprivate struct UserProfileRequest: GraphRequestProtocol {
struct Response: GraphResponseProtocol {
init(rawResponse: Any?) {
// Decode JSON into other properties
}
}
let graphPath: String = "me"
let parameters: [String: Any]? = ["fields": "email"]
let accessToken: AccessToken? = AccessToken.current
let httpMethod: GraphRequestHTTPMethod = .GET
let apiVersion: GraphAPIVersion = .defaultVersion
}
fileprivate func returnUserData() {
let connection = GraphRequestConnection()
connection.add(UserProfileRequest()) {
(response: HTTPURLResponse?, result: GraphRequestResult<UserProfileRequest.Response>) in
// Process
}
connection.start()
However, I am getting this error in the connection.add method:
Type ViewController.UserProfileRequest.Response does not conform to protocol GraphRequestProtocol.
I can't seem to figure this out what to change here. It seems like the developer guide is not up to date on Swift3, but I am not sure that is the issue.
Is anyone able to see what is wrong here?
Thanks.
Browsing on the github issues, i found a solution.
https://github.com/facebook/facebook-sdk-swift/issues/63
Facebook documentation for Swift 3.0 and SDK 0.2.0 is not yet updated.
This works for me:
let params = ["fields" : "email, name"]
let graphRequest = GraphRequest(graphPath: "me", parameters: params)
graphRequest.start {
(urlResponse, requestResult) in
switch requestResult {
case .failed(let error):
print("error in graph request:", error)
break
case .success(let graphResponse):
if let responseDictionary = graphResponse.dictionaryValue {
print(responseDictionary)
print(responseDictionary["name"])
print(responseDictionary["email"])
}
}
}
enjoy.
This code works for me, first I make a login with the correct permissions, then I build the GraphRequest for get the user information.
let login: FBSDKLoginManager = FBSDKLoginManager()
// Make login and request permissions
login.logIn(withReadPermissions: ["email", "public_profile"], from: self, handler: {(result, error) -> Void in
if error != nil {
// Handle Error
NSLog("Process error")
} else if (result?.isCancelled)! {
// If process is cancel
NSLog("Cancelled")
}
else {
// Parameters for Graph Request without image
let parameters = ["fields": "email, name"]
// Parameters for Graph Request with image
let parameters = ["fields": "email, name, picture.type(large)"]
FBSDKGraphRequest(graphPath: "me", parameters: parameters).start {(connection, result, error) -> Void in
if error != nil {
NSLog(error.debugDescription)
return
}
// Result
print("Result: \(result)")
// Handle vars
if let result = result as? [String:String],
let email: String = result["email"],
let fbId: String = result["id"],
let name: String = result["name"] as? String,
// Add this lines for get image
let picture: NSDictionary = result["picture"] as? NSDictionary,
let data: NSDictionary = picture["data"] as? NSDictionary,
let url: String = data["url"] as? String {
print("Email: \(email)")
print("fbID: \(fbId)")
print("Name: \(name)")
print("URL Picture: \(url)")
}
}
}
})
Here is my code like. I use Xcode 8, Swift 3 and it works fine for me.
let parameters = ["fields": "email, id, name"]
let graphRequest = FBSDKGraphRequest(graphPath: "me", parameters: parameters)
_ = graphRequest?.start { [weak self] connection, result, error in
// If something went wrong, we're logged out
if (error != nil) {
// Clear email, but ignore error for now
return
}
// Transform to dictionary first
if let result = result as? [String: Any] {
// Got the email; send it to Lucid's server
guard let email = result["email"] as? String else {
// No email? Fail the login
return
}
guard let username = result["name"] as? String else {
// No username? Fail the login
return
}
guard let userId = result["id"] as? String else {
// No userId? Fail the login
return
}
}
} // End of graph request
Your UserProfileRequest should look like this:
fileprivate struct UserProfileRequest: GraphResponseProtocol {
fileprivate let rawResponse: Any?
public init(rawResponse: Any?) {
self.rawResponse = rawResponse
}
public var dictionaryValue: [String : Any]? {
return rawResponse as? [String : Any]
}
public var arrayValue: [Any]? {
return rawResponse as? [Any]
}
public var stringValue: String? {
return rawResponse as? String
}
}