Reputation: 2443
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:
Upvotes: 15
Views: 23188
Reputation: 1
> 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
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
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
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
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
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
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
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
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