Since Firebase first released an iOS SDK, CocoaPods has been the preferred way to add Firebase to your iOS projects. CocoaPods has served the iOS community well and has made adding libraries and frameworks as easy as adding their package name to your project's Podfile file and running pod install.
Podfile
pod install
In recent years, Swift Package Manager has grown in popularity thanks to being officially supported by Apple, and its tight integration with Xcode. The 2020 iOS Developer Community Survey shows that more than 41% of app developers use Swift Package Manager in their business apps. For hobby/personal apps, that number is even higher at > 56% of developers using Swift Package Manager in their projects.
Adding support for Swift Package Manager has been one of the most requested features in our issue tracker:
Firebase shipped initial (beta) support for Swift Package Manager in August 2020 with Firebase 6.31.0 for a partial set of Firebase products.
Today, we are excited to announce that as of Firebase 8.6.0 for iOS, Firebase fully supports Swift Package Manager. This means you can now add Firebase to your iOS project without leaving Xcode. Gone are the days of having to maintain a working Ruby installation, or having to remember the correct command line arguments just to be able to add a Swift library to your iOS project.
Adding Firebase to your iOS project is easier than ever before with Xcode 12.5:
Adding support for Swift Package Manager was a multi-year project that not only required us to refactor our directory structure and build options to conform to SwiftPM's requirements - we also worked closely with the SwiftPM community to resolve integration issues with binary libraries, resource support, and - last but not least - unit testing support.
Performance Monitoring was the last Firebase product that was missing support for Swift Package Manager, and just a few days ago, the Performance Monitoring team completed their work to get ready for SwiftPM. This involved substantial investment in migrating from Protobuf to nanopb, a refactoring that reduced the size of the SDK by more than 30%.
We know that many of you have been looking forward to being able to migrate to a SwiftPM-only project setup, and we're excited to be able to say that using Swift Package Manager is now the preferred way to add Firebase to your iOS project.
We're working on updating the documentation, setup flows, and quickstart apps to reflect this change.
Come visit us on our GitHub discussion board and issue tracker, and let us know what you think, or if you have any questions about migrating from CocoaPods to Swift Package Manager.
Firebase has an impressive feature list for getting your apps up and running quickly. I love that I can go from idea to working app in a matter of hours! But for developers who haven't had much experience with asynchronous programming, it's easy to get bogged down in the details. Heck, I've been working with Firebase for two years and I still find myself pausing to think about how I want to handle asynchronous calls. What will my view look like while waiting to populate data? How should I set up my data class so that it can alert the view controller about updates? After lots of practice and exploring options, I feel like I finally have an answer that works for me.
Now I'm not going to get into the what and why of asynchronous programming with Firebase because Doug Stevenson already addressed this in this great blog post. Instead, I want to get into how to get a function that uses an async callback working the way you want it to in Swift.
Take this example function I have below from this photo app I've created. It queries Cloud Firestore for 10 documents in a collection called "cats". Once the Cat data is downloaded, I want to use it to populate a UITableView with information about the cat photos.
// ViewController.swift var dbRef = Firestore.firestore().collection("cats") var cats = [Cat]() func downloadCats() { // order the posts by timestamp let query = dbRef.order(by: "timestamp", descending: true).limit(to: 10) query.getDocuments { snapshot, error in print(error ?? "No error.") // iterate through the documents and create Cat objects guard let snapshot = snapshot else { return } for doc in snapshot.documents { let cat = Cat(snapshot: doc) self.cats.append(cat) } } print(cats) // 1 what will this print? tableView.reloadData() }
What will be printed at "1"? Since the call to getDocuments is asynchronous, the closure will be run when the download of data is completed sometime in the future. Commands that I write following the closure will be run immediately -- they do not wait for the work in the closure to be complete. So chances are, in the scenario above, that print statement is going to print an empty array. The UITableView will reload data immediately, so any Cat objects that were supposed to populate the view will not appear. That's not the behavior we want. So what can we do?
getDocuments
print
.
UITableView
Cat
One simple option is to move the functionality I want into the closure, like so:
// ViewController.swift func downloadCats() { let query = dbRef.order(by: "timestamp", descending: true).limit(to: 10) query.getDocuments { snapshot, error in print(error ?? "No error.") for doc in snapshot!.documents { let cat = Cat(snapshot: doc) self.cats.append(cat) } print(cats) // 1 what will this print now? tableView.reloadData() } }
What is printed at "1" now? We should now see the full array of Cat objects, and when tableView.reloadData() is called, the UITableView can populate with the data of those Cats, assuming you've set up your UITableViewDataSource for it. This is a viable method for handling data in the closure, but it means you'll have to include this functionality in your UIViewController. As you add more functionality, such as different kinds of queries, Auth, Cloud Storage, and callable Cloud Functions, you'll quickly have a Massive View Controller on your hands.
tableView.reloadData()
UITableViewDataSource
UIViewController
I like to keep this functionality separate from my view controller whenever possible. I can do this by wrapping getDocuments() in a function with a completion handler. If I include a completion handler in this function, I can call completion() when the array of Cats is populated.
getDocuments()
completion()
// CatManager.swift var dbRef = Firestore.firestore().collection("cats") var cats = [Cat]() // function includes an completion handler, marked with @escaping func downloadCats(completion: @escaping () -> Void) { let query = dbRef.order(by: "timestamp", descending: true).limit(to: 10) query.getDocuments { snapshot, error in print(error ?? "No error.") self.cats = [] guard let snapshot = snapshot else { completion() return } for doc in snapshot.documents { let cat = Cat(snapshot: doc) self.cats.append(cat) } completion() } } }
Then inside the view controller, I can call my function like this:
// ViewController.Swift func initializeCats() { activityIndicator.startAnimating() CatManager.sharedInstance.downloadCats() { // Data for UITableView is populated from the CatManager singleton self.tableView.reloadData() self.activityIndicator.stopAnimating() } }
Now I'm controlling the view from the view controller, reloading the tableView and indicating when the activityIndicator should run. The data is being handled in the CatManager class. This does, however, require a singleton, so while it may prevent a massive view controller from developing, singletons come with their own host of issues.
tableView
activityIndicator
CatManager
Instead of passing void in the completion, I can pass some useful values: an array of Cats and an optional error.
error
// CatDownloader.swift // Use instead of CatManager func downloadCats(completion: @escaping ([Cat], Error) -> Void) { var catArray = [Cat]() let query = dbRef.order(by: "timestamp", descending: true).limit(to: 10) query.getDocuments { snapshot, error in if let error = error { print(error) completion(catArray, error) return } for doc in snapshot!.documents { let cat = Cat(snapshot: doc) catArray.append(cat) } completion(catArray, nil) } } }
With this function, since I pass the error in the closure, the view controller can be made aware of an error and display it to the user. Here I made a function called alert() that displays a UIAlertController. I can also create an instance of CatDownloader in the class because I don't need to depend on a single CatManger to keep track of the Cat array.
alert()
CatDownloader
CatManger
// ViewController.swift // create an instance of CatDownloader let catDownloader = CatDownloader() //… func initializeCats() { activityIndicator.startAnimating() catDownloader.downloadCats() { catArray, error in self.activityIndicator.stopAnimating() if let error = error { self.alert(title: "Error", message: error.localizedDescription) return } self.cats = catArray self.tableView.reloadData() } } func alert(title: String, message: String) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(action) navigationController?.present(alertController, animated: true, completion: nil) }
You may want to display a custom error message to the user instead of the error description, but you can see how the closure made it easy to get the info I needed to the right place in my app. You can use this same configuration for other Firebase features that incorporate closures, including the Realtime Database, Authentication, and ML Kit.
And there you have it! Now go forth and write your own functions with closures to help you update your views. What other examples would you like to see? Do you have any cool solutions you've found that you'd like to share? Reach out to me on Twitter at @ThatJenPerson to tell me all about it!
Ever spend an hour wondering why Remote Config wasn't working, only to realize that you forgot to call activateFetched? Or didn't read a Dynamic Link because you forgot to implement the application:continue:restorationHandler method? Well, now there's a tool to help stop those mistakes before they happen!
activateFetched
application:continue:restorationHandler
SwiftLint is a great open source tool that makes it easier for you to follow Swift style and conventions. It also helps with identifying possible errors early by highlighting problematic usage. You can run SwiftLint on your Xcode project to see all the style guide exceptions on the lines where they occur, and fix them quickly. I found it was a great help when I migrated my code from Objective-c to Swift.
In the spirit of making SwiftLint even more useful for Firebase developers, we've added some experimental new Firebase rules into SwiftLint. These rules will display warnings on common mistakes that might lead to errors when using the Firebase SDK.
Currently we are hosting the rules on our fork in a firebase_rules branch. Our pre-release binary holds the Firebase rules. You simply download the .pkg file and double click to install. You can also build the binary from the source.
Since the rules are opt-in, you'll need to add a .swiftlint.yml file in the same folder as your Swift source files, containing the following text:
``` opt_in_rules: - firebase_config_activate - firebase_config_defaults - firebase_config_fetch - firebase_core - firebase_dynamiclinks_customschemeURL - firebase_dynamiclinks_schemeURL - firebase_dynamiclinks_universallink - firebase_invites ```
Then, just run SwiftLint on your project like normal.
If you're interested in how we put the rules together, you can read our post with all the development details.
We'd love for you to give it a try and send us feedback on Twitter with #FirebaseLinter. You can also ask questions on StackOverflow using the firebase and swiftlint tags together.
firebase
swiftlint