Daryl Wong
Daryl Wong

Reputation: 2443

Firebase converting snapshot value to objects

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

Upvotes: 15

Views: 23188

Answers (9)

>     final class FirestoreManager {
>     
>         private func snapConvert<T: Decodable>(_ query: QuerySnapshot?, to type: T.Type) -> [T] {
>             guard let snapshot = query else {
>                 return [T]()
>             }
>             var models = [T]()
>             for document in snapshot.documents {
>                 let json = JSON(document.data())
>                 if let model = try? JSONDecoder().decode(T.self, from: try! json.rawData()) {
>                     models.append(model)
>                 }
>             }
>             return models
>         }
>     
>     }

I use generics like this, if you want you can send your model and escape it from the completion block.

Upvotes: 0

paul_f
paul_f

Reputation: 1375

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"
   }
}

Upvotes: 0

Adekola Akano
Adekola Akano

Reputation: 169

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!)")
        })

Upvotes: 0

Noobass
Noobass

Reputation: 2014

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.

Upvotes: 8

Rodrigo Guimar&#227;es
Rodrigo Guimar&#227;es

Reputation: 111

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?

Upvotes: 2

psobko
psobko

Reputation: 1568

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.

Upvotes: 0

htafoya
htafoya

Reputation: 19283

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]

Upvotes: 0

Callam
Callam

Reputation: 11539

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)
            }
        }
    }
}

Upvotes: 22

Daryl Wong
Daryl Wong

Reputation: 2443

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 = ""
}

Upvotes: 9

Related Questions