Can't populate an array with data from Firebase database - ios

Below I try to make an array ChatListings, but it doesn't work.
let chatRef = FIRDatabase.database().reference().child("chatListings")
override func viewDidLoad() {
super.viewDidLoad()
let firstQuery = chatRef.queryOrdered(byChild: "userID").queryEqual(toValue: userID)
firstQuery.observe(FIRDataEventType.value, with: { snapshot in
for child in snapshot.children {
print("child is \(child)")
if let dict = snapshot.value as? Dictionary<String, AnyObject>{
print("dict is \(dict)")
let roomKey = dict["chatRoomKey"] as! String
let oUID = dict["otherUserID"] as! String
let oUserName = dict["otherUserName"] as! String
let oProfilePic = dict["otherUserProfilePic"] as! String
let userIDTemp = dict["userID"] as! String
chatListing = ChatListing(chatRoomKey: roomKey, UID: userIDTemp, name: oUserName, otherUserID: oUID, otherUserProfilePicURL: oProfilePic)
chatListings.append(chatListing)
}
}
print("chatListings = \(chatListings)")
})
}
This crashes saying that the compiler unexpectedly found nil while unwrapping an Optional value. I don't know why it won't work. I've tried every which way I can find to extract the data that the compiler reads moments before crashing or failing to fill an array of my 'chatlisting' objects.
Here's an example of the data that the compiler reads but cannot extract with maybe 4 different coding attempts:
"-KjdSF97Q2z3afXzkwQ9": {
chatRoomKey = "-KjdSF97Q2z3afXzkwQ9";
messages = {
"-KjdSOVTsg8jEy6SeEA2" = {
MediaType = PHOTO;
fileUrl = "https://firebasestorage.googleapis.com/v0/b/preollify.appspot.com/o/mget8KN2nHe4sOhbnWTixYvCOrr2%2F515963239.371526?alt=media&token=6cb12ec1-5bdb-43a1-ab49-90c90570b341";
senderId = mget8KN2nHe4sOhbnWTixYvCOrr2;
senderName = Michael;
};
"-KjdSPxpNT0pkQ1y5-_1" = {
MediaType = VIDEO;
fileUrl = "https://firebasestorage.googleapis.com/v0/b/preollify.appspot.com/o/mget8KN2nHe4sOhbnWTixYvCOrr2%2F515963229.282051?alt=media&token=04671c8e-d7f1-49f2-81d0-09836c034ae2";
senderId = mget8KN2nHe4sOhbnWTixYvCOrr2;
senderName = Michael;
};
"-KjdVaVTfbaC-3S-91-A" = {
MediaType = TEXT;
senderId = mget8KN2nHe4sOhbnWTixYvCOrr2;
senderName = Michael;
text = The;
};
};
otherUserID = aRandomUser3611;
otherUserName = Michael;
otherUserProfilePic = "https://firebasestorage.googleapis.com/v0/b/preollify.appspot.com/o/ProfilePictures%2Fmget8KN2nHe4sOhbnWTixYvCOrr2%2FmediumProfilePicture.jpg?alt=media&token=d88afa5d-0db7-4ce2-95c9-3038ff592e9f";
userID = mget8KN2nHe4sOhbnWTixYvCOrr2;
I'm trying to extract all the data but the messages part, which I plan on doing later in the app.
This data (excluding the "messages" part) gets written in the chatViewController's viewDidLoad like this:
let preMessageRef = chatRef.childByAutoId()
chatListingID = preMessageRef.key
let initialChatRoomData = ["chatRoomKey": chatListingID, "otherUserID": otherUID, "otherUserName": otherUserName, "otherUserProfilePic": otherUserProfilePicURLString, "userID": userID]
preMessageRef.setValue(initialChatRoomData)
Retrieving data from Firebase Database has been completely hit or miss for me, with copying the successful attempts of extracting data rarely working twice. Their documentation is minimal to the point of leaving out way too much as it provides little help for how to extract data in real world contexts. Why do people like Firebase? It has been a very frustrating experience working with Firebase and I definitely regret it. But it's probably too late to turn back and go with something better, i.e. a platform that provides clear instruction for how to get it to work.

I think you just have a silly typo. Try this:
let childData = child as! FIRDataSnapshot
print("child key: \(childData.key)")
if let dict = childData.value as? Dictionary<String, AnyObject> {
...
}
that is, use child instead of snapshot.
Update. Turns out using NSDictionary, rather than Dictionary, fixed the dict constant crashes. But, besides compiler bugs, still not clear why...

Related

Return more concrete objects from Data Model in Swift

I am working on an app which fetches data from Firebase. The code I am going to show you works just fine but even as a rookie I know that the implementation should be waaaaay better. I just can't really figure out how to refactor this.
Here's the TLDR:
I have created a Data Model which I used for pasing BlogPosts. I use a Struct for this and in the initial version all of my properties were of type String.
However, apart from Strings for Title and Summary, my Posts also contain an URL to an image and also a Date (post date).
I want to be able to return from my BlogPost object more concrete objcets such as an already created URL or a Date object.
Like I said, I know my implementation is bad, and I want to learn a better way of typecasting in such a way that I can achieve the behaviour described above.
Here is the implementation of my BlogPost data model:
import Foundation
import FirebaseDatabase
struct BlogPost {
let key: String
let title: String
let body: String
let summary: String
let author: String?
let liveSince: Date
let featuredImageLink: URL?
let itemReference:DatabaseReference?
init(snapshot: DataSnapshot) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-mm-dd"
key = snapshot.key
itemReference = snapshot.ref
if let snapshotDictionary = snapshot.value as? NSDictionary, let postTitle = snapshotDictionary["Title"] as? String {
title = postTitle
} else {
title = "Cannot display Title for this item :("
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postBody = snapshotDictionary["Body"] as? String {
body = postBody
} else {
body = "Cannot display Body for this item :("
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postSummary = snapshotDictionary["Summary"] as? String {
summary = postSummary
} else {
summary = "Due to some weird error, the Summary for this item cannot be displayed. Insert more coffee and Pizza in developer"
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postAuthor = snapshotDictionary["Author"] as? String {
author = postAuthor
} else {
author = "Nobody wrote this :("
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let postImageLink = snapshotDictionary["FeaturedImage"] as? String {
featuredImageLink = URL(string: postImageLink)!
} else {
featuredImageLink = URL(string:"https://someimagelink")!
}
if let snapshotDictionary = snapshot.value as? NSDictionary, let liveDate = snapshotDictionary["LiveSince"] as? String {
if let live = dateFormatter.date(from: liveDate) {
liveSince = live
} else {
liveSince = dateFormatter.date(from: "1990-06-26")!
}
} else {
liveSince = dateFormatter.date(from: "1990-06-26")!
}
}
}
Any constructive feedback is more than welcome as I do really want to understand how to do this properly or if it even makes sense to do so in the first place!
Thank you very much for your replies in advance!
I have some suggestions. It looks you keep using conditional binding to unwrap the snapshsot.value while casting it as an NSDictionary. Since unwrapping this value is prerequisite to unwrapping the other values, why not just use a guard statement to unwrap it? In your guard statement, you can then default initialize all the properties in your struct. Alternatively, if you are not adamant about default initializing your properties, you can just use a failable initializer and just return nil in the guard statement.
guard let snapshotDictionary = snapshot.value as? NSDictionary else {
title = "Cannot display Title for this item :("
body = "Cannot display Body for this item :("
summary = "Due to some weird error, the Summary for this item cannot be
displayed. Insert more coffee and Pizza in developer"
...
...
}

How to ensure that the data is not retrieved and appended as a whole each time a new entry is added?

func generateDataForRecents() {
if URLArrayStringThisSeason.count == 0 {
self.activityIndicator2.isHidden = false
self.activityIndicator2.startAnimating()
}
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("palettes").queryLimited(toFirst: 100).observe(.value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict as [String:AnyObject]{
let URL = each.value["URL"] as! String
self.URLArrayStringRecents.append(URL)
//print(self.URLArrayString.count)
//print(snapshot)
//let pictureTitle = each.value["title"] as! String
print(self.URLArrayStringRecents.count)
}
}
self.whatsNewCollectionView?.reloadData() //Reloads data after the number and all the URLs are fetched
self.activityIndicator2.stopAnimating()
self.activityIndicator2.isHidden = true
})
}
The following code does a retrieval of data each time the function is called, or when a new data is added.
This is extremely useful when the app is first started up or closed and then restarted. However, when the app is running, whenever a new entry is added, the code seemed to run again and thus appending twice the amount of new data.
For example, when there are already 15 entries identified and then suddenly a new entry is added, the array of the URL would contain 15+16 thus amounting to a total of 31.
How do I make it such that the new data is added to the array instead of adding the entire snapshot in?
You do that by listening for .childAdded events, instead of listening for .value:
var query = databaseRef.child("palettes").queryLimited(toFirst: 100)
query.observe(.childAdded, with: { (snapshot) in
let URL = snapshot.childSnapshot(forPath/: "URL").value as! String
self.URLArrayStringRecents.append(URL)
}
Since you have a limit-query, adding a 101st item means that one item will be removed from the view. So you'll want to handle .childRemoved too:
query.observe(.childRemoved, with: { (snapshot) in
// TODO: remove the item from snapshot.key from the araay
})
I recommend that you spend some time in the relevant documentation on handling child events before continuing.
Please check below method. I have use this method not getting any duplicate entry.
func getallNotes()
{
let firebaseNotesString: String = Firebase_notes.URL
let firebaseNotes = FIRDatabase.database().referenceFromURL(firebaseNotesString).child(email)
firebaseNotes.observeEventType(.Value, withBlock: { snapshot in
if snapshot.childSnapshotForPath("Category").hasChildren()
{
let child = snapshot.children
self.arrNotes = NSMutableArray()
self.arrDictKeys = NSMutableArray()
for itemsz in child
{
let childz = itemsz as! FIRDataSnapshot
let AcqChildKey : String = childz.key
if AcqChildKey == AcqIdGlobal
{
if (childz.hasChildren() == true)
{
let dictChild = childz.value as! NSMutableDictionary
self.arrDictKeys = NSMutableArray(array: dictChild.allKeys)
for i in 0..<self.arrDictKeys.count
{
let _key = self.arrDictKeys.objectAtIndex(i).description()
print(_key)
let dictData : NSMutableDictionary = NSMutableDictionary(dictionary: (dictChild.valueForKey(_key)?.mutableCopy())! as! [NSObject : AnyObject])
dictData.setObject(_key, forKey: "notesId")
self.arrNotes.addObject(dictData)
}
}
}
}
self.tableviewNote.reloadData()
}
})
}
As for the query for removed child,
query.observe(.childRemoved, with: { (snapshot) in
print(snapshot)
let URL = snapshot.childSnapshot(forPath: "URL").value as! String
self.URLArrayStringThisSeason = self.URLArrayStringThisSeason.filter() {$0 != URL}
self.thisSeasonCollectionView.reloadData()
})
it will obtain the URL of the removed child and then update the array accordingly.

Firebase chat issue - two messages at the same time

I'm new here and I just can't figure how to solve this:
func sendMessage(msgText : String) {
let defaults = NSUserDefaults.standardUserDefaults()
let myRootRef = Firebase(url:"https://boiling-heat-3478.firebaseio.com/")
self.messageCount = String(Int(self.messageCount)!+1)
let messagesRef = myRootRef.childByAppendingPath("messages")
let messageIDRef = messagesRef.childByAppendingPath(self.messageCount)
let messageInfo = ["Text": self.messageText.text!, "SenderID": defaults.objectForKey("userID") as! String]
messageIDRef.updateChildValues(messageInfo)
}
On view did load:
let myRootRef = Firebase(url:"https://boiling-heat-3478.firebaseio.com/")
let messagesRef = myRootRef.childByAppendingPath("messages")
let ref1 = Firebase(url: String(messagesRef))
// Attach a closure to read the data at our posts reference
ref1.queryOrderedByKey().queryStartingAtValue(self.messageCount).observeEventType(.ChildAdded, withBlock: { snapshot in
dispatch_async(dispatch_get_main_queue()){
let path = String(snapshot.ref)
let parts = path.characters.split("/")
if(Int(self.messageCount) < Int(String(parts.last!))){ self.messageCount = String(Int(self.messageCount)!+1) }
let messageText = snapshot.value.objectForKey("Text") as! String
let senderNameAge = snapshot.value.objectForKey("SenderID") as! String
let msg = Message(nameAge: senderNameAge, image: nil, text: messageText)
self.messages.append(msg)
self.messageTableView.reloadData()
}
}, withCancelBlock: { error in
print(error.description)
})
Well, this is working fine. I can chat with 1 person and see replies in real-time, but when I try to send like "aaaa" from person 1 to 2 and "bbbb" from person 2 to 1: message "aaaa" shows only on person 1 screen and "bbbb" on person 2.
Also, I can see on firebase that one of those messages replaced the other on the same destination! How can I avoid or solve this? Thank you
You're creating a problem by trying to use a count to index your messages.
The Firebase documentation explicitly warns against using arrays, since (as you're finding out) it is difficult to keep the message count in sync between multiple clients.
Instead of keeping the count synchronized between clients, you can simply call childByAutoId on a Firebase reference to generate a new child ID that is guaranteed to be unique and always incrementing.
let messagesRef = myRootRef.childByAppendingPath("messages")
let messageIDRef = messagesRef.childByAutoId()
let messageInfo = ["Text": self.messageText.text!, "SenderID": defaults.objectForKey("userID") as! String]
messageIDRef.updateChildValues(messageInfo)
See this section of the documentation for more information and examples.
Also see this example of a chat app written in Swift, that you can use for inspiration.

strange error with plist file

I have a plist file where I added level info. It is setup as follows: It has a dictionary called patterns, then it has an array called 1, and that array contains n items, which are all dictionaries. These items have the 3 booleans and three numbers. This is my code for reading that info:
func readPlstForBlocks(){
let levelPlist = NSBundle.mainBundle().pathForResource("level\(levelToShow)", ofType: "plist")
let levelData = NSDictionary(contentsOfFile: levelPlist!) as NSDictionary!
let patterns = levelData["patterns"] as! NSDictionary
let firstPat = patterns["1"] as! NSArray
for item in firstPat {
let i = item["x"]?!.floatValue
let j = item["y"]?!.floatValue
let movingUpwards = item["movingUpwards"] as! Bool
let movingSidewards = item["movingSidewards"] as! Bool
let spinning = item["spinning"] as! Bool
let typ = item["type"]?!.floatValue
let type = Int16(typ!)
let posx = CGFloat(i!)
let posy = CGFloat(j!)
}
}
Now the lines let movingUpwards = item["movingUpwards"] as! Bool
let movingSidewards = item["movingSidewards"] as! Bool
let spinning = item["spinning"] as! Bool gives me a strange error, saying that i am casting from SKNode to bool and it always fails. That totally confuses me, since I have been accessing booleans from other plist with similar code plenty of times and it seemed to work just fine, and secondly, why on earth xcode would think its SKNode? Anyone knows how to solve this?
EDIT:
I am adding the plist file photo:
In general, when writing Swift code, I would recommend using Swift types as much as possible, not Objective-C types. So, where you have this:
let levelData = NSDictionary(contentsOfFile: levelPlist!) as NSDictionary!
let patterns = levelData["patterns"] as! NSDictionary
let firstPat = patterns["1"] as! NSArray
I would write this:
let levelData = NSDictionary(contentsOfFile: levelPlist!) as! [NSObject:AnyObject]
let patterns = levelData["patterns"] as! [NSObject:AnyObject]
let firstPat = patterns["1"] as! [[NSObject:AnyObject]]
Now, if we get that far without crashing, Swift knows that firstPat is an array of dictionaries, which is much more than you were telling it before. This means that for item in firstPat can correctly type item as a dictionary. And that, in turn, means that you can subscript it and extract values by key:
for item in firstPat {
let i = item["x"] as! Float
let j = item["y"] as! Float
let movingUpwards = item["movingUpwards"] as! Bool
// ...
}
None of that is "good" code; the forced casts are dangerous. In real life, you should be unwrapping / casting safely the whole way down. But I think this will give you a more coherent approach up front.

What is the best way to parse JSON files and create Model objects and then map them to the Core Data database?

I have the following instance method in my class, where "jsonObj" is a Dictionary:
func getSurvey(countryId: String, languageId: String, surveyId: String, userId: String, completionHandler: ((ICSurvey!, NSError!) -> Void)?)
{
ICWebServicesManager.downloadSurvey("", languageId: "", surveyId: "", userId: "") {
(jsonObj, error) -> Void in
if completionHandler != nil
{
if error != nil
{
completionHandler!(nil, error)
}
else
{
let surveyJSONObject = jsonObj
let survey = ICSurvey()
if let surveyIdObj = surveyJSONObject["id"] as? Dictionary<String, String>
{
self.dateFormatter!.dateFormat = "y-M-d'T'HH:mm:ssZZZZ"
survey.id = ICSurveyId(
surveyId : surveyIdObj["survey_id"]!,
countryId : surveyIdObj["country_id"]!,
languageId : surveyIdObj["language_id"]!
)
survey.uri = surveyJSONObject["uri"] as? String
survey.title = surveyJSONObject["title"] as? String
survey.startDate = self.dateFormatter!.dateFromString(surveyJSONObject["start_date"] as! String)
survey.endDate = self.dateFormatter!.dateFromString(surveyJSONObject["end_date"] as! String)
survey.type = surveyJSONObject["type"] as? String
survey.questionGroups = Array()
if let questionGroupsJSONObjects = surveyJSONObject["question_groups"] as? Array<Dictionary<String, AnyObject>>
{
for questionGroupObj in questionGroupsJSONObjects
{
let questionGroup = ICQuestionGroup()
questionGroup.questions = Array()
questionGroup.text = questionGroupObj["text"] as? String
if let questionsArrayObj = questionGroupObj["questions"] as? Array<Dictionary<String, AnyObject>>
{
for questionObj in questionsArrayObj
{
var question = ICQuestion()
if let questionIdObj = questionObj["id"] as? Dictionary<String, String>
{
question.id = ICQuestionId(
questionId : questionIdObj["question_id"]!,
languageId : questionIdObj["language_id"]!
)
question.type = questionObj["type"] as? String
var requiredString = questionObj["required"]
as? String
question.required = (requiredString == "True" ? true : false)
question.setValue(questionObj["text"] as? String, forKey: "text")
if let questionResponseObj = questionObj["response"] as? Dictionary<String, AnyObject>
{
question.response = ICResponse(
type : questionResponseObj["type"] as! String,
value : questionResponseObj["value"] as! Int,
clientTimestamp : self.dateFormatter!.dateFromString(questionResponseObj["client_timestamp"] as! String)!
)
}
if let questionResponseObj = questionObj["options"] as? Array<Dictionary<String, AnyObject>>
{
question.options = questionResponseObj
}
}
questionGroup.questions!.append(question)
}
}
survey.questionGroups!.append(questionGroup)
}
}
}
self.currentSurvey = survey
completionHandler!(self.currentSurvey, error)
}
}
}
}
After doing some code review with my mentor, he told me everything is pretty good, except I need to "abstract out hard wired properties". My understanding is that he does not want to have code that looks like this:
survey.startDate = self.dateFormatter!.dateFromString(surveyJSONObject["start_date"] as! String)
, because "start_date" for instance is hard-coded.
Instead, I should find what objects the JSON file represent and map that data correspondingly. While I agree to that to some extent, as the app code will not need many changes if it do it that way, it seem to be an overhead for me because I need to map everything to Core Data, and if a property changes, many things may change or crash.
My question is: What is the best way to parse JSON files and create Model objects and then map them to the Core Data database?
How do we have to "abstract out hard wired properties"?
If anyone has more experience with Web Services integration, please give me some advice.
Thanks in advance!
I was using SwiftyJSON on previous projects but now I've converted everything to Swift 1.2 (if let,) syntax and I honestly like it more since it feels like I have more flexibility and it alleviates 3rd party framework dependency.
As far as your "hard-coded" values, simply utilize optionals for testing. For instance, the following line requires a value to be present. Test for that value prior to unwrapping it. His fear is that if that object doesn't exist for whatever reason your program will crash. (This isn't tested but you should get the idea)
surveyId : surveyIdObj["survey_id"]!,
if let surveyId = surveyIdObj["survey_id"] as? String,
let countryId = surveyIdObj["country_id"] as? String,
let languageId = surveyIdObj["language_id"] as? String {
survey.id = ICSurveyId(
// You could also test here for each and add the values to your survey object
surveyId : surveyIdObj["survey_id"]!,
countryId : surveyIdObj["country_id"]!,
languageId : surveyIdObj["language_id"]!
)
}
Personally I prefer just creating a new NSManagedObject and assigning the values directly.
var survey = NSEntityDescription.insertNewObjectForEntityForName("Survey", inManagedObjectContext: self.appDelegate.managedObjectContext!) as! Survey
// Then just assign the values to the object using dot notation
survey.surveyId = surveyIdObj["survey_id"]!
// and so on
// Then save the context
self.managedObjectContext?.save(nil)

Resources