15

So I have a postDict as [String: AnyObject] and I have a model class Post.

Is there a quick way to convert postDict to an array of Post objects so that when dequeuing the cell, it will be:

cell.textLabel.text = posts[indexPath.item].author

import UIKit
import Firebase

class ViewController: UIViewController {

var posts = [Post]()

override func viewDidLoad() {
    super.viewDidLoad()

    let ref = FIRDatabase.database().reference().child("posts").queryLimitedToFirst(5)

    ref.observeEventType(FIRDataEventType.ChildAdded, withBlock: { (snapshot) in
        let postDict = snapshot.value as! [String : AnyObject]

        print(postDict)          

        //convert postDict to array of Post objects
    })
  }
}

class Post: NSObject {
    var author: String = ""
    var body: String = ""
    var imageURL: String = ""
    var uid: String = ""
}

This is the output when printing out postDict:

enter image description here

Syscall
  • 19,327
  • 10
  • 37
  • 52
Daryl Wong
  • 2,023
  • 5
  • 28
  • 61
  • 2
    I'm not sure but this may help? http://stackoverflow.com/questions/37106246/read-data-from-firebase-and-save-into-an-array-swift – Hack-R Jun 26 '16 at 13:36
  • 1
    In this case, snapshot.value is a single node (due to .ChildAdded) so it would represent a single post. I would suggest one of two things. 1) Add a function to your Post class called: func populateFromSnapshot( aSnapshot: FDataShapshot) or 2) Create a new post inside the block and populate it from the snapshot itself. Post.author = snapshot.value["author"] as! String. Wrap that in a let for error checking incase the unwrapped is nil. Then add the post to the array Posts.append(newPost) – Jay Jun 26 '16 at 13:42
  • 2
    Ah, updated question. With the new case, you want to iterate of the snapshot with *for child in snapshot.children*. Each child will be a distinct post. We've posted a lot of answers on how to do that here on stack overflow and you can actually search by that phrase. See the answer [Adding firebase data to an array in ios swift](http://stackoverflow.com/questions/31633216/adding-firebase-data-to-an-array-in-ios-swift/31637158#31637158) for the pattern. You can use my above comment to populate the object you are adding to the array. – Jay Jun 26 '16 at 14:44

8 Answers8

22

Try using the class, protocol and extension I have created below, it will save you a lot of time trying to map the snapshots to objects.

//
//  FIRDataObject.swift
//
//  Created by Callam Poynter on 24/06/2016.
//

import Firebase

class FIRDataObject: NSObject {

    let snapshot: FIRDataSnapshot
    var key: String { return snapshot.key }
    var ref: FIRDatabaseReference { return snapshot.ref }

    required init(snapshot: FIRDataSnapshot) {

        self.snapshot = snapshot

        super.init()

        for child in in snapshot.children.allObjects as? [FIRDataSnapshot] ?? [] {
            if respondsToSelector(Selector(child.key)) {
                setValue(child.value, forKey: child.key)
            }
        }
    }
}

protocol FIRDatabaseReferenceable {
    var ref: FIRDatabaseReference { get }
}

extension FIRDatabaseReferenceable {
    var ref: FIRDatabaseReference {
        return FIRDatabase.database().reference()
    }
}

Now you can create a model that inherits the FIRDataObject class and can be initialised with a FIRDataSnapshot. Then add the FIRDatabaseReferenceable protocol to your ViewController to get access to your base reference.

import Firebase
import UIKit

class ViewController: UIViewController, FIRDatabaseReferenceable {

    var posts: [Post] = []

    override func viewDidLoad() {

        super.viewDidLoad()

        ref.child("posts").observeEventType(.ChildAdded, withBlock: {
            self.posts.append(Post(snapshot: $0))
        })
    }
}

class Post: FIRDataObject {

    var author: String = ""
    var body: String = ""
    var imageURL: String = ""
}

UPDATE for Swift 3

class FIRDataObject: NSObject {

    let snapshot: FIRDataSnapshot
    var key: String { return snapshot.key }
    var ref: FIRDatabaseReference { return snapshot.ref }

    required init(snapshot: FIRDataSnapshot) {

        self.snapshot = snapshot

        super.init()

        for child in snapshot.children.allObjects as? [FIRDataSnapshot] ?? [] {
            if responds(to: Selector(child.key)) {
                setValue(child.value, forKey: child.key)
            }
        }
    }
}
Callam
  • 11,409
  • 2
  • 34
  • 32
  • 2
    Nice! How would you handle nested objects? – doovers Jul 29 '16 at 00:29
  • After confirming the object responds to the child key as a selector, I check to see if the class of the child key as a selector is of type FIRDataObject. Then you can init a FIRDataObject from a child snapshot and set the value on the parent object for the child key. – Callam Jul 30 '16 at 02:04
  • I have completed my FIRDataObject class and it works however rather than storing relationships as just the key, you must store them as a nested object. `{ "authors": { "$authorKey": { // } }, "posts": { "$postKey": { "body": "Hello World" "author": { "$authorKey": true } } } }` – Callam Jul 30 '16 at 02:05
  • According to swift 3, there is an error in the FIRDataObject class on the line that starts "for case child let child..." I'm not a swift expert but that doesn't seem to be valid swift (at least not in swift 3). I'm trying to figure out the correction, but I havent' yet. – Rob Gorman Sep 29 '16 at 21:21
  • @RanchoSoftware Updated it – Callam Sep 29 '16 at 21:34
  • Thats awesome!! Thanks. – Rob Gorman Sep 29 '16 at 21:47
  • 2
    @callam, you should put this code in GitHub as a gist so updates can be tracked. I found multiple versions of your code around the web and am trying to keep them straight – otusweb Dec 01 '16 at 13:03
  • 1
    @otusweb okay I'll look into it v soon – Callam Dec 01 '16 at 13:06
  • 1
    How about writing "Post" object back to firebase? is there is a quick method? – Muhammad Hassan Nasr Dec 11 '16 at 10:43
  • @Callam did you ever look into committing to GitHub? – AKrush95 Jul 05 '17 at 20:55
  • This worked great for my Firebase app, now does anyone have a mod for FireSTORE?? – Louis Sankey Jan 30 '19 at 03:39
9

Thanks for all the comments and hints above. They certainly helped. So I am using the method with setValuesForKeysWithDictionary. It gets them into an array of posts.

import UIKit
import Firebase
class ViewController: UIViewController {

var posts = [Post]()

override func viewDidLoad() {
    super.viewDidLoad()

    let ref = FIRDatabase.database().reference().child("posts").queryLimitedToFirst(3)

    ref.observeEventType(.Value, withBlock: { snapshot in
        print(snapshot.value)
        self.posts = []
        if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
            for snap in snapshots {
                if let postDict = snap.value as? Dictionary<String, AnyObject> {
                    let post = Post()
                    post.setValuesForKeysWithDictionary(postDict)
                    self.posts.append(post)
                }
            }
        }
        print("post 0: \(self.posts[0].body)")
        print("post 1: \(self.posts[1].body)")
        print("post 2: \(self.posts[2].body)")
      })
   }
}

class Post: NSObject {
    var author: String = ""
    var body: String = ""
    var imageURL: String = ""
    var uid: String = ""
}
Daryl Wong
  • 2,023
  • 5
  • 28
  • 61
8

I wrote a small framework called CodableFirebase that helps using Firebase Realtime Database with Codable in swift 4. So in your case, you need to conform your Post model to Codable:

class Post: NSObject, Codable {
    var author: String = ""
    var body: String = ""
    var imageURL: String = ""
    var uid: String = ""
}

And then you can use the library to parse the object:

import CodableFirebase

ref.observeEventType(.сhildAdded, withBlock: { (snapshot) in
    guard let value = snapshot.value else { return }
    do {
        let posts = try FirebaseDecoder().decode([Post].self, from: value)
        print(posts)
    } catch let error {
        print(error)
    }
})

And that's it :) I think it's the shortest and most elegant way.

Noobass
  • 1,974
  • 24
  • 26
2

This code no longer works in swift 4 because @objc inference is disabled by default.

UPDATE for Swift 4

class FIRDataObject: NSObject {

    let snapshot: FIRDataSnapshot
    @objc var key: String { return snapshot.key }
    var ref: FIRDatabaseReference { return snapshot.ref }

    required init(snapshot: FIRDataSnapshot) {

        self.snapshot = snapshot

        super.init()

        for child in snapshot.children.allObjects as? [FIRDataSnapshot] ?? [] {
            if responds(to: Selector(child.key)) {
                setValue(child.value, forKey: child.key)
            }
        }
    }
}

class Post: FIRDataObject {

    @objc var author: String = ""
    @objc var body: String = ""
    @objc var imageURL: String = ""
}

Or you can just make @objc inferencing a default on your project by (WARNING: loss of performance): The use of Swift 3 @objc inference in Swift 4 mode is deprecated?

0

I am creating a helper to ease transforming snapshots to objects and viceversa. I haven't finished my project but it is working so far, I will update whenever I do changes.

What the class does is assign automatically the value for key, but if the key represents a dictionary, then it is mapped to another object again (which may be another class object)

The getMap method is pretty straight forward, converting each property to a dictionary or object. When the property is another object. You can't assign nil values so the transformation must be done to [NSNull].

I couldn't find a way to auto-detect BOOL / Double / int etc, so they should be mapped correctly on getMap method, or simply use NSNumbers in model the properties.

Interface

#import <Foundation/Foundation.h>
@import FirebaseDatabase;

#ifndef FIRModel_m

#define FIRModel_m

#define IS_OBJECT(T) _Generic( (T), id: YES, default: NO)

#endif



/** Firebase model that helps converting Firebase Snapshot to object, and converting the object
 * to a dictionary mapping for updates */
@interface FIRModel : NSObject

/** Parses the snapshot data into the object */
- (void) parseFromSnapshot: (FIRDataSnapshot*) snapshot;

/** Returns a new model for the given key */
- (FIRModel*) modelForKey: (NSString*) key;

/** Returns the dictionary representation of this object */
- (NSMutableDictionary*) getMap;

/** Returns an object value for the given preference
 * If the property is null, then NSNUll is returned
 */
- (NSObject*) objFor: (id) value;

@end

Implementation

#import "FIRModel.h"

@implementation FIRModel
/** Parses the snapshot data into the object */
- (void) parseFromSnapshot: (FIRDataSnapshot*) snapshot {

    [self setValuesFromDictionary: snapshot.value];
}

/** Custom implementation for setValuesForKeysWithDictionary 
 *  Whenever it finds a Dictionary, it is transformed to the corresponding model object
 */
- (void)setValuesFromDictionary:(NSDictionary*)dict
{

    NSLog(@"Parsing in %@ the following received info: %@", [self class], dict);


    for (NSString* key in dict) {

        NSObject* value = [dict objectForKey:key];

        if(!value || [value isKindOfClass: [NSNull class]]) {
            //do nothing, value stays null
        }


        //TODO: Do the same for arrays
        else if(value && [value isKindOfClass: [NSDictionary class]]) {

            FIRModel* submodel = [self modelForKey: key];

            if(submodel) {
                [submodel setValuesFromDictionary: (NSDictionary*)value];
                [self setValue: submodel forKey: key];
            } else {
                NSLog(@"ERROR - *** Nil model returned from modelForKey for key: %@ ***", key );
            }
        }
        else {
            [self setValue: value forKey:key];
        }

    }
}

/** Override for added firebase properties**/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"Unknown key: %@ on object: %@", key, [self class] );
}

/** Returns a new model for the given key */
- (FIRModel*) modelForKey: (NSString*) key {
    return nil; //to be implemented by subclasses
}

/** Returns the dictionary representation of this object */
- (NSMutableDictionary*) getMap {
    [NSException raise:@"getMap not implmented" format:@"ERROR - Not implementing getMap for %@", self.class];
    return [NSMutableDictionary dictionary];
}

/** Returns an object value for the given preference
 * If the property is null, then NSNUll is returned
 */
- (NSObject*) objFor: (id) value {

    if(!value || !IS_OBJECT(value)) {
        return [NSNull null];
    }

    return value;


}


@end

Example usage:

#import <Foundation/Foundation.h>
#import "FIRModel.h"


/** The user object */
@class PublicInfo;


@interface User : FIRModel

@property (nonatomic, strong) NSString* email;

@property (nonatomic, strong) NSString* phone;

@property (nonatomic, strong) PublicInfo* publicInfo;

@property (nonatomic, assign) double aDoubleValue;  

@property (nonatomic, assign) BOOL aBoolValue;  

@property (nonatomic, strong) id timestampJoined;   //Map or NSNumber

@property (nonatomic, strong) id timestampLastLogin;  //Map or NSNumber


@end



@interface PublicInfo : FIRModel

@property (nonatomic, strong) NSString* key;

@property (nonatomic, strong) NSString* name;

@property (nonatomic, strong) NSString* pic;

@end

Implementation

#import "User.h"


@implementation User

/** Returns a new model for the given key */
- (FIRModel*) modelForKey: (NSString*) key {
    if ([key isEqualToString: @"publicInfo"]) {
        return [[PublicInfo alloc] init];
    }
    return nil;
}

- (NSMutableDictionary *)getMap {

    NSMutableDictionary* map = [NSMutableDictionary dictionary];
    map[@"email"] =  [self objFor: self.email];
    map[@"phone"] = [self objFor: self.phone];
    map[@"aDoubleValue"] = @(self.aDoubleValue);
    map[@"aBoolValue"] = @(self.aBoolValue);
    map[@"publicInfo"] = self.publicInfo ? [self.publicInfo getMap] : [NSNull null];
    map[@"timestampJoined"] =  [self objFor: self.timestampJoined];
    map[@"timestampLastLogin"] = [self objFor: self.timestampLastLogin];

    return map;
}

@end


#pragma mark -

@implementation PublicInfo

- (NSMutableDictionary *)getMap {

    NSMutableDictionary* map = [NSMutableDictionary dictionary];
    map[@"name"] =  [self objFor: self.name];
    map[@"pic"] =  [self objFor: self.pic];
    map[@"key"] = [self objFor: self.key];

    return map;
}

@end

Usage

//Parsing model
User *user = [[User alloc] init];
[user parseFromSnapshot: snapshot];

//Getting map for updateChildValues method
[user getMap]
htafoya
  • 18,261
  • 11
  • 80
  • 104
0

Here's an Objective-C version of Callam's code above.

@import Firebase;

@interface FIRDataObject : NSObject

@property (strong, nonatomic) FIRDataSnapshot *snapshot;
@property (strong, nonatomic, readonly) NSString *key;
@property (strong, nonatomic, readonly) FIRDatabaseReference *ref;

-(instancetype)initWithSnapshot:(FIRDataSnapshot *)snapshot;

@end

@implementation FIRDataObject

-(NSString *)key
{
    return _snapshot.key;
}

-(FIRDatabaseReference *)ref
{
    return _snapshot.ref;
}

-(instancetype)initWithSnapshot:(FIRDataSnapshot *)snapshot
{
 if (self = [super init])
 {
     _snapshot = snapshot;
     for (FIRDataSnapshot *child in snapshot.children.allObjects)
     {
         if ([self respondsToSelector:NSSelectorFromString(child.key)])
         {
             [self setValue:child.value forKey:child.key];
         }
     }
 }
    return self;
}

Now all we need is model cascading and property type enforcement.

psobko
  • 1,548
  • 1
  • 14
  • 24
0

I found a simpler method.

Swift Object:

import Foundation

class FirebaseTransactionData : NSObject{

    var customer : FirebaseTransactionDataCustomer!
    var driver : FirebaseTransactionDataCustomer!
    var status : String!

    init(fromDictionary dictionary: [String:Any]){
        status = dictionary["status"] as? String
        if let customerData = dictionary["customer"] as? [String:Any]{
            customer = FirebaseTransactionDataCustomer(fromDictionary: customerData)
        }
        if let driverData = dictionary["driver"] as? [String:Any]{
            driver = FirebaseTransactionDataCustomer(fromDictionary: driverData)
        }
    }
}

class FirebaseTransactionDataCustomer : NSObject{

    var lat : Double!
    var longField : Double!

    init(fromDictionary dictionary: [String:Any]){
        lat = dictionary["lat"] as? Double
        longField = dictionary["lng"] as? Double
    }
}

Firebase Method

ref.observe(DataEventType.value, with: { (snapshot) in
            let value = snapshot.value as? [String:Any]
            let datt = FirebaseTransactionData(fromDictionary: value!)
            print("snapshot \(datt.status!)")
            print("snapshot \(datt.customer.lat!)")
        })
Adekola Akano
  • 169
  • 2
  • 7
0

The easiest solution I have found is to convert the object to JSON Data like so:

let jsonData =  try! JSONSerialization.data(withJSONObject: snapshot.value!, options: .prettyPrinted)

Then parse the object using a JSONDecoder:

try! JSONDecoder().decode(UserOtherDetails.self, from: jsonData)

Complete solution looks like this:

Database.database().reference().child("USER").observe(.value) { (snapshot) in
    let jsonData =  try! JSONSerialization.data(withJSONObject: snapshot.value!, options: .prettyPrinted)
    self.userOtherDetails = try! JSONDecoder().decode(UserOtherDetails.self, from: jsonData)
}

An example of the Object is:

struct UserOtherDetails: Codable {
   let addressBlock, addressCity: String
   let addressIDS: [Int]
   let addressUnit, displayName, phone: String
   let rememberMe: Int

   enum CodingKeys: String, CodingKey {
      case addressBlock = "address_block"
      case addressCity = "address_city"
      case addressIDS = "address_ids"
      case addressUnit = "address_unit"
      case displayName = "display_name"
      case phone
      case rememberMe = "remember_me"
   }
}
paul_f
  • 1,296
  • 17
  • 20