├── .gitignore ├── FIRDeletable.swift ├── FIRInsertable.swift ├── FIRModel.swift ├── FIRPropertyWritable.swift ├── FIRQueryable.swift ├── FIRStorageDownloadable.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | 67 | .DS_Store 68 | -------------------------------------------------------------------------------- /FIRDeletable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Firebase 3 | 4 | protocol FIRDeletable 5 | { 6 | static var COLLECTION_NAME: String { get } 7 | } 8 | 9 | extension FIRDeletable where Self: FIRModel 10 | { 11 | func delete(completion: ((Error?) -> Void)? = nil) 12 | { 13 | FIRDatabase.database() 14 | .reference(withPath: Self.COLLECTION_NAME) 15 | .child(self.key) 16 | .removeValue { (e: Error?, ref: FIRDatabaseReference) in 17 | 18 | completion?(e) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FIRInsertable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Firebase 3 | 4 | protocol FIRInsertable 5 | { 6 | static var COLLECTION_NAME: String { get } 7 | } 8 | 9 | extension FIRInsertable where Self: FIRModel 10 | { 11 | static func Insert(data: [String: Any], completion: ((Self) -> Void)? = nil) 12 | { 13 | let ref = FIRDatabase.database().reference().child(COLLECTION_NAME).childByAutoId() 14 | ref.updateChildValues(data) 15 | 16 | ref.observe(.value) { (snapshot: FIRDataSnapshot) in completion?(Self.init(snapshot: snapshot)) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FIRModel.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Firebase 4 | 5 | class FIRModel: CustomStringConvertible 6 | { 7 | private var _snapshot: FIRDataSnapshot 8 | var snapshot: FIRDataSnapshot { get { return self._snapshot } set { self._snapshot = newValue } } 9 | 10 | var key: String { return self._snapshot.key } 11 | 12 | var description: String { return self._snapshot.description } 13 | 14 | required init(snapshot: FIRDataSnapshot) 15 | { 16 | self._snapshot = snapshot 17 | } 18 | 19 | func get(_ path: String) -> T? 20 | { 21 | return self.snapshot.childSnapshot(forPath: path).value as? T 22 | } 23 | 24 | func get(_ path: String) -> T 25 | { 26 | return T(snapshot: self.snapshot.childSnapshot(forPath: path)) 27 | } 28 | 29 | func get(_ path: String) -> [T] 30 | { 31 | var items: [T] = [] 32 | 33 | self.snapshot.childSnapshot(forPath: path).children.forEach { (childSnapshot) in 34 | 35 | items.append(T(snapshot: childSnapshot as! FIRDataSnapshot)) 36 | } 37 | 38 | return items 39 | } 40 | 41 | func getLinkKeys(for path: String) -> [String] 42 | { 43 | return (self.snapshot.childSnapshot(forPath: path).value as? [String : Bool])?.map { (key, val) -> String in 44 | return key 45 | } ?? [] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /FIRPropertyWritable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Firebase 3 | 4 | protocol FIRPropertyWritable { } 5 | 6 | extension FIRPropertyWritable where Self: FIRModel 7 | { 8 | func set(value: Any?, for key: String) 9 | { 10 | self.snapshot.ref.child(key).setValue(value) 11 | } 12 | 13 | func add(key: String, forNode nodePath: String, completion: ((Error?) -> Void)? = nil) 14 | { 15 | self.snapshot.ref.child(nodePath).updateChildValues([key : true]) { (error: Error?, ref: FIRDatabaseReference) in 16 | 17 | completion?(error) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FIRQueryable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Firebase 3 | 4 | protocol FIRQueryable 5 | { 6 | static var COLLECTION_NAME: String { get } 7 | } 8 | 9 | extension FIRQueryable where Self: FIRModel 10 | { 11 | func getExternal(completion: @escaping () -> Void) 12 | { 13 | Self.GetCollectionRef().child(self.key).observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in 14 | 15 | self.snapshot = snapshot 16 | completion() 17 | } 18 | } 19 | 20 | static func All(completion: @escaping ([Self]) -> Void) 21 | { 22 | self.GetCollectionRef().observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in 23 | 24 | completion(self.GetModels(fromContainerSnapshot: snapshot)) 25 | } 26 | } 27 | 28 | static func From(key: String, completion: @escaping (Self) -> Void) 29 | { 30 | self.GetCollectionRef().child(key).observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in 31 | 32 | completion(Self(snapshot: snapshot)) 33 | } 34 | } 35 | 36 | static func Top(_ limit: UInt, completion: @escaping ([Self]) -> Void) 37 | { 38 | self.GetCollectionRef().queryLimited(toFirst: limit).observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in 39 | 40 | completion(self.GetModels(fromContainerSnapshot: snapshot)) 41 | } 42 | } 43 | 44 | static func Where(child path: String, equals value: Any?, limit: UInt = 1000, completion: @escaping ([Self]) -> Void) 45 | { 46 | self.GetCollectionRef() 47 | .queryOrdered(byChild: path) 48 | .queryEqual(toValue: value) 49 | .queryLimited(toFirst: limit) 50 | .observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in 51 | 52 | completion(self.GetModels(fromContainerSnapshot: snapshot)) 53 | } 54 | } 55 | 56 | 57 | static func TopWhere(child path: String, equals value: Any?, completion: @escaping (Self?) -> Void) 58 | { 59 | self.Where(child: path, equals: value, limit: 1) { (items: [Self]) in 60 | 61 | completion(items.count == 0 ? nil : items[0]) 62 | } 63 | } 64 | 65 | static func GetModels(fromContainerSnapshot snapshot: FIRDataSnapshot) -> [Self] 66 | { 67 | var models: [Self] = [] 68 | 69 | for obj in snapshot.children where obj is FIRDataSnapshot 70 | { 71 | models.append(Self.init(snapshot: obj as! FIRDataSnapshot)) 72 | } 73 | 74 | return models 75 | } 76 | 77 | static func GetCollectionRef() -> FIRDatabaseReference 78 | { 79 | return FIRDatabase.database().reference().child(COLLECTION_NAME) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FIRStorageDownloadable.swift: -------------------------------------------------------------------------------- 1 | import Firebase 2 | 3 | protocol FIRStorageDownloadable 4 | { 5 | var location: String? { get } 6 | } 7 | 8 | extension FIRStorageDownloadable where Self: FIRModel 9 | { 10 | var location: String? { return self.get("location") } 11 | 12 | func getDownloadURL(completion: @escaping (URL?, Error?) -> Void) 13 | { 14 | guard let ref = self.getStorageRef() else 15 | { 16 | completion(nil, NSError(domain: "No storage reference found", code: 0, userInfo: nil)) 17 | return 18 | } 19 | 20 | ref.downloadURL(completion: completion) 21 | } 22 | 23 | func getData(withMaxSize maxSize: Int64, completion: @escaping (Foundation.Data?, Error?) -> Void) 24 | { 25 | guard let ref = self.getStorageRef() else 26 | { 27 | completion(nil, NSError(domain: "No storage reference found", code: 0, userInfo: nil)) 28 | return 29 | } 30 | 31 | ref.data(withMaxSize: maxSize, completion: completion) 32 | } 33 | 34 | fileprivate func getStorageRef() -> FIRStorageReference? 35 | { 36 | guard let loc = location else 37 | { 38 | return nil 39 | } 40 | 41 | return FIRStorage.storage().reference(withPath: loc) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebase-db-wrapper-swift 2 | An easy-to-use object wrapper for Firebase's Realtime Database 3 | 4 | **Dependencies:** FirebaseDatabase, FirebaseStorage 5 | 6 | For demonstration purposes, we'll use the database structure defined below, comprised of murals & artists: 7 | ```json 8 | { 9 | "murals" : 10 | { 11 | "-KaQYfs3kbt4XgDY0ftb" : 12 | { 13 | "artists" : 14 | { 15 | "-KbJbPknFNECn07m1yzy" : true, 16 | "-KbJXK4aoXc6NZ6VwD7W" : true 17 | }, 18 | "description" : "A beautiful mural in Orange, CA", 19 | "images" : 20 | { 21 | "m1" : 22 | { 23 | "location" : "/murals/m1.jpg" 24 | }, 25 | "m2" : 26 | { 27 | "location" : "/murals/m2.jpg" 28 | } 29 | }, 30 | "name" : "A really great mural" 31 | } 32 | }, 33 | "artists" : 34 | { 35 | "-KbJXK4aoXc6NZ6VwD7W" : 36 | { 37 | "country" : "US", 38 | "firstName" : "Mary", 39 | "lastName" : "Smith" 40 | }, 41 | "-KbJbPknFNECn07m1yzy" : 42 | { 43 | "country" : "US", 44 | "firstName" : "Kerry", 45 | "lastName" : "Winston" 46 | } 47 | } 48 | ``` 49 | ## `FIRModel` Usage 50 | 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: 51 | 52 | ```swift 53 | class MuralModel: FIRModel 54 | { 55 | static var FIELD_NAME = "name" 56 | static var FIELD_DESCRIPTION = "description" 57 | static var FIELD_IMAGES = "images" 58 | static var FIELD_ARTISTS = "artists" 59 | 60 | var name: String? { return self.get(MuralModel.FIELD_NAME) } 61 | var desc: String? { return self.get(MuralModel.FIELD_DESCRIPTION) } 62 | 63 | var images: [ImageModel] { return self.get(MuralModel.FIELD_IMAGES) } 64 | var artists: [ArtistModel] { return self.get(MuralModel.FIELD_ARTISTS) } 65 | } 66 | ``` 67 | Artist: 68 | ```swift 69 | class ArtistModel: FIRModel 70 | { 71 | static var FIELD_FIRSTNAME = "firstName" 72 | static var FIELD_LASTNAME = "lastName" 73 | static var FIELD_COUNTRY = "country" 74 | 75 | var firstName: String? { return self.get(ArtistModel.FIELD_FIRSTNAME) } 76 | var lastName: String? { return self.get(ArtistModel.FIELD_LASTNAME) } 77 | var country: String? { return self.get(ArtistModel.FIELD_COUNTRY) } 78 | } 79 | ``` 80 | Image: 81 | ```swift 82 | class ImageModel: FIRModel 83 | { 84 | static var FIELD_LOCATION = "location" 85 | var location: String? { return self.get(ImageModel.FIELD_LOCATION) } 86 | } 87 | ``` 88 | 89 | `FIRModel` mirrors the functionality of Firebase's `FIRDataSnapshot`, and is therefore constructed using one: 90 | ```swift 91 | let mural = MuralModel(snapshot: muralSnapshot) 92 | ``` 93 | 94 | 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](https://firebase.google.com/docs/database/web/structure-data]), 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. 95 | 96 | ## `FIRQueryable` Usage 97 | `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`. 98 | 99 | ```swift 100 | class ArtistModel: FIRModel, FIRQueryable 101 | { 102 | static var COLLECTION_NAME = "artists" 103 | 104 | ... 105 | } 106 | ``` 107 | 108 | When `FIRQueryable` is adopted, you may use `getExternal(completion: () -> ())` to retrieve a partially populated model. See example below. 109 | 110 | ```swift 111 | let firstArtist = self.mural.artists[0] 112 | firstArtist.getExternal { 113 | self.artistLabel.text = firstArtist.firstName 114 | } 115 | ``` 116 | 117 | `FIRQueryable` additionally contains several static query-convenience functions. `FIRQueryable.Where()` is demonstrated below. 118 | 119 | ```swift 120 | MuralModel.Where(child: MuralModel.FIELD_NAME, equals: "Some value", limit: 1000) { (murals: [MuralModel]) in 121 | 122 | // Do something 123 | } 124 | ``` 125 | 126 | ## `FIRPropertyWritable` Usage 127 | 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. 128 | 129 | ```swift 130 | class MuralModel: FIRModel, FIRPropertyWritable 131 | { 132 | static var FIELD_NAME = "name" 133 | static var FIELD_DESCRIPTION = "description" 134 | static var FIELD_IMAGES = "images" 135 | static var FIELD_ARTISTS = "artists" 136 | 137 | var name: String? { 138 | get { return self.get(MuralModel.FIELD_NAME) } 139 | set { self.set(value: newValue, for: MuralModel.FIELD_NAME) } 140 | } 141 | var desc: String? { 142 | get { return self.get(MuralModel.FIELD_DESCRIPTION) } 143 | set { self.set(value: newValue, for: MuralModel.FIELD_DESCRIPTION) } 144 | } 145 | 146 | var images: [ImageModel] { return self.get(MuralModel.FIELD_IMAGES) } 147 | var artists: [ArtistModel] { return self.get(MuralModel.FIELD_ARTISTS) } 148 | } 149 | ``` 150 | 151 | ## `FIRInsertable` Usage 152 | `FIRInsertable` can be adopted by a `FIRModel` that belongs to a database collection that can be written to. 153 | ```swift 154 | class ArtistModel: FIRModel, FIRInsertable 155 | { 156 | static var COLLECTION_NAME = "artists" 157 | 158 | static var FIELD_FIRSTNAME = "firstName" 159 | static var FIELD_LASTNAME = "lastName" 160 | static var FIELD_COUNTRY = "country" 161 | 162 | var firstName: String? { return self.get(ArtistModel.FIELD_FIRSTNAME) } 163 | var lastName: String? { return self.get(ArtistModel.FIELD_LASTNAME) } 164 | var country: String? { return self.get(ArtistModel.FIELD_COUNTRY) } 165 | 166 | class func Create(firstName: String, lastName: String, country: String, completion: @escaping (ArtistModel) -> Void) 167 | { 168 | let data = [ 169 | FIELD_FIRSTNAME: firstName, 170 | FIELD_LASTNAME: lastName, 171 | FIELD_COUNTRY: country 172 | ] 173 | 174 | self.Insert(data: data, completion: completion) 175 | } 176 | } 177 | ``` 178 | `FIRInsertable.Insert()` can also be used statically outside of a class. 179 | ```swift 180 | let data = [ 181 | FIELD_FIRSTNAME: firstName, 182 | FIELD_LASTNAME: lastName, 183 | FIELD_COUNTRY: country 184 | ] 185 | 186 | ArtistModel.Insert(data: data) { (createdArtist: ArtistModel) in 187 | 188 | // Handle completion 189 | } 190 | ``` 191 | 192 | ## `FIRDeletable` Usage 193 | `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()` 194 | 195 | ## `FIRStorageDownloadable` Usage 196 | 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: 197 | ```swift 198 | class ImageModel: FIRModel, FIRStorageDownloadable { } 199 | ``` 200 | Then from here, 201 | ```swift 202 | let image: ImageModel = mural.images[0] 203 | image.getData(withMaxSize: 1 * 1024 * 1024, completion: { (d: Data?, e: Error?) in 204 | 205 | if let error = e 206 | { 207 | print("Woops: \(error)") 208 | } 209 | else if let data = d 210 | { 211 | self.imageView.image = UIImage(data: data) 212 | } 213 | }) 214 | ``` 215 | 216 | Enjoy! 217 | --------------------------------------------------------------------------------