An easy-to-use object wrapper for Firebase's Realtime Database
Dependencies: FirebaseDatabase, FirebaseStorage
For demonstration purposes, we'll use the database structure defined below, comprised of murals & artists:
{
"murals" :
{
"-KaQYfs3kbt4XgDY0ftb" :
{
"artists" :
{
"-KbJbPknFNECn07m1yzy" : true,
"-KbJXK4aoXc6NZ6VwD7W" : true
},
"description" : "A beautiful mural in Orange, CA",
"images" :
{
"m1" :
{
"location" : "/murals/m1.jpg"
},
"m2" :
{
"location" : "/murals/m2.jpg"
}
},
"name" : "A really great mural"
}
},
"artists" :
{
"-KbJXK4aoXc6NZ6VwD7W" :
{
"country" : "US",
"firstName" : "Mary",
"lastName" : "Smith"
},
"-KbJbPknFNECn07m1yzy" :
{
"country" : "US",
"firstName" : "Kerry",
"lastName" : "Winston"
}
}
Subclass FIRModel
to make your models serialize & deserialize from objects returned directly from your Firebase Realtime Database. The structure of a simple read-only FIRModel
representing a mural may look like this:
class MuralModel: FIRModel
{
static var FIELD_NAME = "name"
static var FIELD_DESCRIPTION = "description"
static var FIELD_IMAGES = "images"
static var FIELD_ARTISTS = "artists"
var name: String? { return self.get(MuralModel.FIELD_NAME) }
var desc: String? { return self.get(MuralModel.FIELD_DESCRIPTION) }
var images: [ImageModel] { return self.get(MuralModel.FIELD_IMAGES) }
var artists: [ArtistModel] { return self.get(MuralModel.FIELD_ARTISTS) }
}
Artist:
class ArtistModel: FIRModel
{
static var FIELD_FIRSTNAME = "firstName"
static var FIELD_LASTNAME = "lastName"
static var FIELD_COUNTRY = "country"
var firstName: String? { return self.get(ArtistModel.FIELD_FIRSTNAME) }
var lastName: String? { return self.get(ArtistModel.FIELD_LASTNAME) }
var country: String? { return self.get(ArtistModel.FIELD_COUNTRY) }
}
Image:
class ImageModel: FIRModel
{
static var FIELD_LOCATION = "location"
var location: String? { return self.get(ImageModel.FIELD_LOCATION) }
}
FIRModel
mirrors the functionality of Firebase's FIRDataSnapshot
, and is therefore constructed using one:
let mural = MuralModel(snapshot: muralSnapshot)
Properties can be as nested as necessary. Notice that images
and artists
in MuralModel
are of complex object types. These are too subclasses of FIRModel
. Look back at the database structure. As recommended in Firebase's database structure guidelines, in our database, the artists
node consists only of keys. Because of this, the artists
node, for example, will consist of a number of ArtistModel
, but only the key
property will be populated. This is where FIRQueryable
comes in.
FIRQueryable
is a protocol that can be adopted by any FIRModel
that belongs to a top level collection in the Firebase database. By adopting this protocol, you'll simply need to define which collection your model belongs to. See example below of ArtistModel
.
class ArtistModel: FIRModel, FIRQueryable
{
static var COLLECTION_NAME = "artists"
...
}
When FIRQueryable
is adopted, you may use getExternal(completion: () -> ())
to retrieve a partially populated model. See example below.
let firstArtist = self.mural.artists[0]
firstArtist.getExternal {
self.artistLabel.text = firstArtist.firstName
}
FIRQueryable
additionally contains several static query-convenience functions. FIRQueryable.Where()
is demonstrated below.
MuralModel.Where(child: MuralModel.FIELD_NAME, equals: "Some value", limit: 1000) { (murals: [MuralModel]) in
// Do something
}
By nature, FIRModel
is a read-only model. FIRPropertyWritable
can be adopted to allow modifying properties in a FIRModel
. A more sophisticated MuralModel
is demonstrated below.
class MuralModel: FIRModel, FIRPropertyWritable
{
static var FIELD_NAME = "name"
static var FIELD_DESCRIPTION = "description"
static var FIELD_IMAGES = "images"
static var FIELD_ARTISTS = "artists"
var name: String? {
get { return self.get(MuralModel.FIELD_NAME) }
set { self.set(value: newValue, for: MuralModel.FIELD_NAME) }
}
var desc: String? {
get { return self.get(MuralModel.FIELD_DESCRIPTION) }
set { self.set(value: newValue, for: MuralModel.FIELD_DESCRIPTION) }
}
var images: [ImageModel] { return self.get(MuralModel.FIELD_IMAGES) }
var artists: [ArtistModel] { return self.get(MuralModel.FIELD_ARTISTS) }
}
FIRInsertable
can be adopted by a FIRModel
that belongs to a database collection that can be written to.
class ArtistModel: FIRModel, FIRInsertable
{
static var COLLECTION_NAME = "artists"
static var FIELD_FIRSTNAME = "firstName"
static var FIELD_LASTNAME = "lastName"
static var FIELD_COUNTRY = "country"
var firstName: String? { return self.get(ArtistModel.FIELD_FIRSTNAME) }
var lastName: String? { return self.get(ArtistModel.FIELD_LASTNAME) }
var country: String? { return self.get(ArtistModel.FIELD_COUNTRY) }
class func Create(firstName: String, lastName: String, country: String, completion: @escaping (ArtistModel) -> Void)
{
let data = [
FIELD_FIRSTNAME: firstName,
FIELD_LASTNAME: lastName,
FIELD_COUNTRY: country
]
self.Insert(data: data, completion: completion)
}
}
FIRInsertable.Insert()
can also be used statically outside of a class.
let data = [
FIELD_FIRSTNAME: firstName,
FIELD_LASTNAME: lastName,
FIELD_COUNTRY: country
]
ArtistModel.Insert(data: data) { (createdArtist: ArtistModel) in
// Handle completion
}
FIRDeletable
can be adopted by a FIRModel
that belongs to a database collection that can be written to. A class that adopts FIRDeletable
has the ability to call the function delete()
Any FIRModel
that has a location: String
pointing to a location in Firebase Storage can instead adopt FIRStorageDownloadable
to provide a seamless integration. In our case:
class ImageModel: FIRModel, FIRStorageDownloadable { }
Then from here,
let image: ImageModel = mural.images[0]
image.getData(withMaxSize: 1 * 1024 * 1024, completion: { (d: Data?, e: Error?) in
if let error = e
{
print("Woops: \(error)")
}
else if let data = d
{
self.imageView.image = UIImage(data: data)
}
})
Enjoy!