Map Data From Firestore to a struct In Swift - IOS - ios

I'm trying to map a "user" retrieved from my Firestore database to a user struct I've defined in my code but I don't think I properly understand the mapping features in swift.
How do I go about mapping the user retrieved into the struct?
Struct
struct User {
// Properties
var firstName: String
var lastName: String
var userName: String
var email: String
init(firstName: String, lastName: String, userName: String, email: String) {
self.firstName = firstName
self.lastName = lastName
self.userName = userName
self.email = email
}
}
Function that gets user from Firestore
func getUser(UID: String) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
db.collection("users").document(UID).getDocument { (document, error) in
if let error = error {
print("Error: \(error.localizedDescription)")
} else {
print(document!.data()!)
}
}
}

A queried Firestore document is of type [String: Any], so configure your initializer to accept that kind of dictionary. Because you're working with a database return, there is never a guarantee that the data will be fully intact, so I would suggest making your initializer failable, which just means that it can return nil (it can fail).
I took your example and made the email property optional. In the example below, only three of the four properties are necessary to instantiate the User object.
let info: [String: Any] = ["hasFriends": true]
let firestoreData: [String: Any] = ["firstName": "lance", "lastName": "stevenson", "userName": "lstevenson", "info": info]
struct User {
var firstName: String
var lastName: String
var userName: String
var info: [String: Any]
var email: String?
var hasFriends: Bool
init?(data: [String: Any]) {
guard let firstName = data["firstName"] as? String,
let lastName = data["lastName"] as? String,
let userName = data["userName"] as? String,
let info = data["info"] as? [String: Any],
let hasFriends = info["hasFriends"] as? Bool else {
return nil
}
self.firstName = firstName
self.lastName = lastName
self.userName = userName
self.info = info
self.hasFriends = hasFriends
self.email = data["email"] as? String // User will be created even if this is nil
}
}
if let user = User(data: firestoreData) {
print(user.hasFriends) // true
}
I would not suggest that you use Swift's mapping tool for this because you will likely be dealing with varying types of values within the same dictionary for different models. And the code to map this dictionary with those variables in an initializer would not be pretty.

Related

Failable Init - Variable 'self.customerType' used before being initialized

I've read up and down regarding this, and understand the basics here - I just can't understand why I get this error. I use the second init to instantiate a customer from Firebase, but even if I comment out everything inside it, I still get the error
Variable 'self.customerType' used before being initialized at the declaration of init?
class Customer {
enum customerTypes {
case consumer
case business
}
// MARK - properties
var id: String?
let customerType: customerTypes
var name1: String
var name2: String?
var address: Address?
var phone: String?
var email: String?
struct Address {
var street: String
var postalCode: String
var city: String
var country: String = "Norway"
}
init(type: customerTypes, name1: String, name2: String?, phone: String, email: String, address: Address? ) {
self.customerType = type
self.name1 = name1
self.name2 = name2
self.phone = phone
self.email = email
self.address = address
}
init?(data: [String: Any]) {
guard let type = data["customerType"] as? String else { return }
guard let name1 = data["name1"] as? String else { return }
self.customerType = type == "Consumer" ? .consumer : .business
self.name1 = name1
// if let name2 = data["name2"] as? String { self.name2 = name2 }
// if let phone = data["phone"] as? String { self.phone = phone }
// if let email = data["email"] as? String{ self.email = email }
// if let address = data["address"] as? [String: Any] {
// let street = address["street"] as? String
// let postCode = address["postCode"] as? String
// let city = address["city"] as? String
// if street != nil && postCode != nil && city != nil {
// self.address = Address(street: street!, postalCode: postCode!, city: city!)
// }
// }
}
What simple issue am I overlooking here?
You declare an initializer which promises to either return an initialized Customer or no Customer (because it is fallible). You alo declare let customerType: customerTypes as one of the properties of the class.
That means that if you successfully return from the initializer (that means, not returning nil), this property has to be initialized to some value.
The error is not very helpful in the location of the error, as the error is actually on the line below. By simply putting return in your guard, you are saying that your object is successfully initialized, which it is not, as you have not yet set customerType to a value.
So if you put a return nil in your guard clause, you will say that your initialization failed, and then you do not need to put a value in customerType.
The properties that don't have an initial value needs to set inside an init. You can fix the issue by either setting them as Optional or by setting a default value:
init?(data: [String: Any]) {
customerType = .consumer
name1 = ""
}
or:
var customerType: customerTypes?
var name1: String?
Note: By setting the properties Optional the compiler assumes that the initial value is nil.

Firebase print snapshot works fine and fetches the dictionary but when tried to access individual field data, returns nil for certain fields

I have a problem accessing values from certain fields in my firebase database. Right now this is how my structure looks in firebase:
messages:
messageId:
fromId:
text:
timestamp:
toId:
I am able to successfully upload the data to firebase when a user inputs a message to another user. And I am also able to successfully print the snapshot. But when I set the dictionary values and access it, only "fromId" and "toId" works but "timestamp" and "text" returns a nil value.
Pretty sure there is some sort of a wrong implementation in terms of taking the snapshot values and setting it. For your reference, I have included 3 files, one where the data model is defined, one where I upload data to firebase and one where I am trying to print it but I get nil.
The file where I am trying to print data but I get nil. Note: I am only getting nil when I am trying to print "text" and "timestamp" field values. "fromId" and "toId" works.
import UIKit
import Firebase
class MessagesController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
observeMessages()
}
var messages = [Message]()
func observeMessages(){
let ref = Database.database().reference().child("messages")
ref.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as?
Dictionary<String, AnyObject>{
let message = Message(dictionary: dictionary)
print(message.text)
print(message.fromId)
print(message.toId)
print(timestamp)
}
})
}
}
This is how I am uploading the data to firebase using a handle send function once the user has entered some text in the text box
#objc func handleSend(){
let ref = Database.database().reference().child("messages")
let childRef = ref.childByAutoId()
let toId = user!.uid!
let fromId = Auth.auth().currentUser!.uid
let timestamp: Int = Int(NSDate().timeIntervalSince1970)
let values = ["fromId": fromId, "text":
inputTextField.text!, "timestamp": timestamp, "toId": toId] as
[String : Any]
childRef.updateChildValues(values)
}
Finally this is how I have declared my messages class:
class Message{
var fromId: String!
var text: String!
var timestamp: Int!
var toId: String!
init(dictionary: Dictionary<String, AnyObject>) {
if let text = dictionary["messageText"] as? String {
self.text = text
}
if let fromId = dictionary["fromId"] as? String {
self.fromId = fromId
}
if let toId = dictionary["toId"] as? String {
self.toId = toId
}
if let timestamp = dictionary["creationDate"] as? Int {
self.timestamp = timestamp
}
}
}
When I print message.fromID, I get the data results in the console but when I print message.text or message.timestamp I get
nil
nil
nil
So in summary snapshot works, fromId, toID fields also work but for some reason the data from the text and timestamp fields are returned as nil
your are accessing values from dictionary with invalid key use text instead of messageText and use timeSamp instead of creationDate. like below
class Message{
var fromId: String!
var text: String!
var timestamp: Int!
var toId: String!
init(dictionary: Dictionary<String, AnyObject>) {
if let text = dictionary["text"] as? String {
self.text = text
}
if let fromId = dictionary["fromId"] as? String {
self.fromId = fromId
}
if let toId = dictionary["toId"] as? String {
self.toId = toId
}
if let timestamp = dictionary["timestamp"] as? Int {
self.timestamp = timestamp
}
}
}

How to work with Firebase without allowing optional values

I'm new to iOS development and I understand that allowing optional values when an object is initialized is not a 'good citizen' technique. That being said, I've read that it is good practice to always have values set, like this:
class Item{
var name: String
var color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
}
This looks nice and tidy but how can I do something like that working with Firebase? Look what I've got so far:
private func loadPosts(){
databaseHandle = ref.child("users/\(self.user.uid)/posts").observe(.value, with:{(snapshot) in
var newPosts = [Post]()
for itemSnapShot in snapshot.children {
let post = Post(snapshot: itemSnapShot as! FIRDataSnapshot)
newPosts.append(post!)
}
self.posts = newPosts
self.tableView.reloadData()
})
}
This guy is placed in my PostsViewController where I have my table view. This is my model:
class Post {
var ref: FIRDatabaseReference?
var title: String?
var answer: String?
var contentUrl: String?
var photoUrl: String?
var createdAt: String?
var feeling: String?
var kind: String?
var text: String?
var uid: String?
var measurements: Dictionary<String, String>?
//MARK: Initialization
init?(snapshot: FIRDataSnapshot){
ref = snapshot.ref
let data = snapshot.value as! Dictionary<String, Any>
title = data["title"]! as? String
answer = data["answer"] as? String
contentUrl = data["content_url"] as? String
photoUrl = data["photo_url"] as? String
createdAt = data["created_at"] as? String
feeling = data["feeling"] as? String
kind = data["kind"] as? String
text = data["text"] as? String
uid = data["uid"] as? String
measurements = data["measurements"] as? Dictionary<String, String>
}
}
I don't know exactly why but those question marks doesn't feel quite right and now and then I get some nil pointer error, which I think I should be able to avoid by using the 'good citizen' technique.
So, does anybody know how can I use Firebase following Swift best practices?
Either you wish to allow the properties of your Post class to be nil or you don't.
If you do, that's fine. The code you posted allows any of them to be nil. You just need to safely access each property every time you need it.
If you don't, then don't make them optional. Then in your init you need to ensure none of the properties are set to nil by giving each a default if there is no value in the snapshot.
class Post {
var ref: FIRDatabaseReference
var title: String
var answer: String
var contentUrl: String
var photoUrl: String
var createdAt: String
var feeling: String
var kind: String
var text: String
var uid: String
var measurements: [String : String]
//MARK: Initialization
init?(snapshot: FIRDataSnapshot) {
if let data = snapshot.value as? [String : Any] {
self.ref = snapshot.ref
title = data["title"] as? String ?? ""
answer = data["answer"] as? String ?? ""
contentUrl = data["content_url"] as? String ?? ""
photoUrl = data["photo_url"] as? String ?? ""
createdAt = data["created_at"] as? String ?? ""
feeling = data["feeling"] as? String ?? ""
kind = data["kind"] as? String ?? ""
text = data["text"] as? String ?? ""
uid = data["uid"] as? String ?? ""
measurements = data["measurements"] as? [String : String] ?? [:]
} else {
return nil
}
}
}
Note how this ensures there is a proper snapshot. Note how a default value is set to each property if there is no value in the snapshot. Obviously you can assign any default you wish. I use the empty string as an example.
Even if you want to allow the properties to be nil, you should at least update your code to check for a valid snapshot like in the code above.
Of course you can have a combination where some properties can't be nil and some can. That's up to your needs.
First it is fine for you to have optionals in your data model, as long as you assign value to it later on in the future.
I would recommend to use ObserveSingleEvent() and you should make use of completion handler to make it easy. If you don't know completion handler: Link
I recommend:
• not to put database ref in your class model, and instead of using Dictionary<String, String>? just use [String: AnyObject]?
• make your post array public so that it can be accessed into the tableview.
Here's example:
class func getPosts(uid: String, _ completion: #escaping (_ posts: [Post]?, _ error: Error?) -> Void) {
//update inside users node
var posts = [Post]()
Firebase.databaseRef.child("users").child(uid).child("posts").observeSingleEvent(of: FIRDataEventType.value, with: { (dataSnapshot) in
guard let postsDictionary = dataSnapshot.value as? [String: AnyObject] else {
completion(nil, nil)
return
}
let n = postsDictionary.count
for postDictionary in postsDictionary {
let post = Post()
post.userID = uid
if let content = postDictionary.value["content"] as? String {
post.content = content
}
if let imageURL = postDictionary.value["imageURL"] as? String {
post.imageURL = imageURL
}
if let timeStamp = postDictionary.key as String! {
if let date = timeStamp.convertToDate() {
post.timeStamp = date
}
post.postIdentifier = timeStamp
}
posts.append(post)
if posts.count == n {
// Sort the array by the newest post
let sortedPosts = posts.sorted(by: { $0.timeStamp.compare($1.timeStamp) == .orderedDescending })
completion(sortedPosts, nil)
}
}
}) { (error) in
completion(nil, error)
}
}
Assigning to tableview be like:
getPosts(uid: Current.user.userID!) { (posts, error) in
guard error == nil else {
print(error.debugDescription)
return
}
cell.label.text = posts[indexPath.item].content

Cannot convert value of type '(User) -> Dictionary<String, String>' to expected argument type 'Dictionary<String, String>'

I am still new to learning Swift. I reviewed a few other posts where it was suggested that I need to unwrap any optionals, I believe I have done so but, I am still getting the error as explained below:
Given the code snippet below
let user = User(uid: authData.uid! , email: email!, firstName: firstName!,
lastName: lastName!, provider: authData.provider!)
let userDictionary = User.getUserDictionary(user)
// Add new account to the Firebase database
UserAccountService.firebaseDBService.createNewAccount(authData.uid, user: userDictionary)
Getting error
Cannot convert value of type '(User) -> Dictionary<String, String>' to
expected argument type 'Dictionary<String, String>'
on the line
UserAccountService.firebaseDBService.createNewAccount(authData.uid, user: userDictionary)
Function:
func createNewAccount(uid: String, user: Dictionary<String, String>) {
// A User is born.
USER_REF.childByAppendingPath(uid).setValue(user)
}
User.swift:
import Foundation
import Firebase
class User: NSObject {
let uid: String
let email: String
let firstName: String
let lastName: String
let provider: String
// Initialize from Firebase
init(authData: FAuthData, firstName: String, lastName: String) {
self.uid = authData.uid!
self.email = authData.providerData["email"] as! String
self.firstName = firstName
self.lastName = lastName
self.provider = authData.provider!
}
// Initialize from arbitrary data
init(uid: String, email: String, firstName: String, lastName: String, provider: String) {
self.uid = uid
self.email = email
self.firstName = firstName
self.lastName = lastName
self.provider = ""
}
// Return a Dictionary<String, String> from User object
func getUserDictionary(user: User) -> Dictionary<String, String> {
//let provider = user.provider as String!
let email = user.email as String!
let firstName = user.firstName as String!
let lastName = user.lastName as String!
let userDictionary: [String : String] = [
"provider" : user.provider as String!,
"email" : email,
"firstName" : firstName,
"lastName" : lastName
]
return userDictionary
}
}
The User needs to be a lowercase 'u' when you call the getUserMethod as you are referring to the instance of your user class rather than the User class itself. It should look like this:
let user = User(uid: authData.uid! , email: email!, firstName: firstName!,
lastName: lastName!, provider: authData.provider!)
//CHANGE HERE: lowercase u on user
let userDictionary = user.getUserDictionary(user)
See if that helps.
The function you've used
User.getUserDictionary(user)
is actually the method of the class User.
So that means you need to call the method on one of the class' objects that you've instantiated instead of on the class itself.
I don't think the issue is related to optional unwrapping.

iOS Creating an Object Best Practice

I've created a wizard for user's to sign up using my app, however, I'm having some doubts as to how I should store their information along the way.
I have a User model, which is filled out when users are pulled from the database, however, there are some required fields on this model that wouldn't be filled out if I were to use it as the object that is passed along as the user goes through the the wizard.
Here is my User model:
final class User: NSObject, ResponseObjectSerializable, ResponseCollectionSerializable {
let id: Int
var facebookUID: String?
var email: String
var firstName: String
var lastName: String
var phone: String?
var position: String?
var thumbnail: UIImage?
var timeCreated: CVDate
init?(response: NSHTTPURLResponse, var representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
representation = dataRepresentation
}
self.id = representation.valueForKeyPath("id") as! Int
self.facebookUID = (representation.valueForKeyPath("facebook_UID") as? String)
self.email = (representation.valueForKeyPath("email") as? String) ?? ""
self.firstName = (representation.valueForKeyPath("first_name") as? String) ?? ""
self.lastName = (representation.valueForKeyPath("last_name") as? String) ?? ""
self.phone = (representation.valueForKeyPath("phone") as? String)
self.position = (representation.valueForKeyPath("position_name") as? String)
self.thumbnail = UIImage(named: "ThomasBaldwin")
if let timeCreated = representation.valueForKeyPath("time_created") as? String {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
if let date = formatter.dateFromString(timeCreated) {
self.timeCreated = CVDate(date: date)
} else {
self.timeCreated = CVDate(date: NSDate())
}
} else {
self.timeCreated = CVDate(date: NSDate())
}
}
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [User] {
var users: [User] = []
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [NSDictionary]) {
if let dataRepresentation = dataRepresentation as? [[String: AnyObject]] {
for userRepresentation in dataRepresentation {
if let user = User(response: response, representation: userRepresentation) {
users.append(user)
}
}
}
}
return users
}
}
Notice the variables id and timeCreated. These are both generated when a new row is added to the Users table in the database, therefore, I wouldn't have values for those variables until the user is actually created.
Also, I would like to add some methods to the model, such as validateUser which will be a method that makes sure all the fields are filled out, and validateEmail which will be a method that makes sure the email is in proper syntax, and so on...
My question is, should I
A. just make those constants optional and add those methods to my current User model
B. make another model called CreateUserModel that only has variables for the information the user will be filling out and put the extra methods in there
UPDATE
I updated my User class to use a dictionary as the storage mechanism and it already looks a lot cleaner. However, the issue that comes to mind is, how will another programmer know which fields he can grab from the User model since I'm not individually storing them as variables anymore. Would they just have to check the DB and look at the structure of the table?
Here's my updated User class:
final class User: NSObject, ResponseObjectSerializable, ResponseCollectionSerializable {
var properties = NSDictionary()
init?(response: NSHTTPURLResponse, representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
properties = dataRepresentation
}
properties = representation as! NSDictionary
}
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [User] {
var users: [User] = []
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [NSDictionary]) {
if let dataRepresentation = dataRepresentation as? [[String: AnyObject]] {
for userRepresentation in dataRepresentation {
if let user = User(response: response, representation: userRepresentation) {
users.append(user)
}
}
}
}
return users
}
}
I would make them Optionals. That is the beauty of Optionals - you can use nil to mean exactly "no data here".
The other grand strategy that comes to mind is to use a dictionary as the storage mechanism inside your model, because that way either it has a certain key or it doesn't. You could make your User object key-value coding compliant, and thus effectively transparent, by passing keys on to the dictionary.

Resources