Welcome to the Friendly Chat codelab. In this codelab, you'll learn how to use the Firebase platform to create iOS applications. You will implement a chat client and monitor its performance using Firebase.
This codelab is also available in Objective-C.
Clone the GitHub repository from the command line.
$ git clone https://github.com/firebase/friendlychat-ios
To build the starter app:
ios-starter/swift-starter
directory from your sample code download pod install
You should see the Friendly Chat home screen appear after a few seconds. The UI should appear. However, at this point you cannot sign in, send or receive messages. The app will abort with an exception until you complete the next step.
From Firebase console select Add Project.
Call the project FriendlyChat
, then click on Create Project.
com.google.firebase.codelab.FriendlyChatSwift
".123456
".On the second screen click Download GoogleService-Info.plist to download a configuration file that contains all the necessary Firebase metadata for your app. Copy that file to your application and add it to the FriendlyChatSwift target.
You can now click the "x" in the upper right corner of the popup to close it -- skipping steps 3 and 4 -- as you will perform those steps here.
Start by making sure the Firebase
module is imported.
import Firebase
Use the "configure" method in FirebaseApp inside the application:didFinishLaunchingWithOptions function to configure underlying Firebase services from your .plist file.
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
GIDSignIn.sharedInstance().delegate = self
return true
}
We will now add a rule to require authentication before reading or writing any messages. To do this we add the following rules to our messages data object. From within the Database section of Firebase console select the RULES tab. Then update the rules so they look like this:
{
"rules": {
"messages": {
".read": "auth != null",
".write": "auth != null"
}
}
}
For more information on how this works (including documentation on the "auth" variable) see the Firebase security documentation.
Before your application can access the Firebase Authentication APIs on behalf of your users, you will have to enable it
If you get errors later in this codelab with the message "CONFIGURATION_NOT_FOUND", come back to this step and double check your work.
Confirm Firebase Auth dependencies exist in the Podfile
file.
pod 'Firebase/Auth'
You'll need to add a custom URL scheme to your XCode project.
After Firebase is configured, we can use the clientID to set up the Google Sign In inside the "didFinishLaunchingWithOptions:" method.
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
GIDSignIn.sharedInstance().delegate = self
return true
}
Once the result of the Google Sign-In was successful, use the account to authenticate with Firebase.
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
if let error = error {
print("Error \(error)")
return
}
guard let authentication = user.authentication else { return }
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
accessToken: authentication.accessToken)
Auth.auth().signIn(with: credential) { (user, error) in
if let error = error {
print("Error \(error)")
return
}
}
}
Automatically sign in the user. Then add a listener to Firebase Auth, to let the user into the app, after successful sign in. And remove the listener on deinit.
override func viewDidLoad() {
super.viewDidLoad()
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().signInSilently()
handle = Auth.auth().addStateDidChangeListener() { (auth, user) in
if user != nil {
MeasurementHelper.sendLoginEvent()
self.performSegue(withIdentifier: Constants.Segues.SignInToFp, sender: nil)
}
}
}
deinit {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
Add the Sign out method
@IBAction func signOut(_ sender: UIButton) {
let firebaseAuth = Auth.auth()
do {
try firebaseAuth.signOut()
dismiss(animated: true, completion: nil)
} catch let signOutError as NSError {
print ("Error signing out: \(signOutError.localizedDescription)")
}
}
In your project in Firebase console select the Database item on the left navigation bar. In the overflow menu of the Database select Import JSON. Browse to the initial_messages.json
file in the friendlychat directory, select it then click the Import button. This will replace any data currently in your database. You could also edit the database directly, using the green + and red x to add and remove items.
After importing your database should look like this:
In the dependencies block of the Podfile
file, confirm that Firebase/Database
is included.
pod 'Firebase/Database'
Add code that synchronizes newly added messages to the app UI.
The code you add in this section will:
DataSnapshot
so new messages will be shown. Modify your FCViewController's "deinit", "configureDatabase", and "tableView:cellForRow indexPath:" methods; replace with the code defined below:
deinit {
if let refHandle = _refHandle {
self.ref.child("messages").removeObserver(withHandle: _refHandle)
}
}
func configureDatabase() {
ref = Database.database().reference()
// Listen for new messages in the Firebase database
_refHandle = self.ref.child("messages").observe(.childAdded, with: { [weak self] (snapshot) -> Void in
guard let strongSelf = self else { return }
strongSelf.messages.append(snapshot)
strongSelf.clientTable.insertRows(at: [IndexPath(row: strongSelf.messages.count-1, section: 0)], with: .automatic)
})
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue cell
let cell = self.clientTable.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
// Unpack message from Firebase DataSnapshot
let messageSnapshot = self.messages[indexPath.row]
guard let message = messageSnapshot.value as? [String: String] else { return cell }
let name = message[Constants.MessageFields.name] ?? ""
let text = message[Constants.MessageFields.text] ?? ""
cell.textLabel?.text = name + ": " + text
cell.imageView?.image = UIImage(named: "ic_account_circle")
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage(data: data)
}
return cell
}
Push values to the database. When you use the push method to add data to Firebase Realtime Database, an automatic ID will be added. These auto generated IDs are sequential, which ensures that new messages will be added in the correct order.
Modify your FCViewController's "sendMessage:" method; replace with the code defined below:
func sendMessage(withData data: [String: String]) {
var mdata = data
mdata[Constants.MessageFields.name] = Auth.auth().currentUser?.displayName
if let photoURL = Auth.auth().currentUser?.photoURL {
mdata[Constants.MessageFields.photoURL] = photoURL.absoluteString
}
// Push data to Firebase Database
self.ref.child("messages").childByAutoId().setValue(mdata)
}
In the dependencies block of the Podfile
, confirm Firebase/Storage
is included.
pod 'Firebase/Storage'
Go to Firebase console and confirm that Storage is activated with "gs://PROJECTID.apppot.com" domain
If you are seeing the activation window instead, click "GET STARTED" to activate it with default rules.
func configureStorage() {
storageRef = Storage.storage().reference()
}
Add code that downloads images from Firebase Storage.
Modify your FCViewController's "tableView: cellForRowAt indexPath:" method; replace with the code defined below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue cell
let cell = self.clientTable .dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
// Unpack message from Firebase DataSnapshot
let messageSnapshot: DataSnapshot! = self.messages[indexPath.row]
guard let message = messageSnapshot.value as? [String:String] else { return cell }
let name = message[Constants.MessageFields.name] ?? ""
if let imageURL = message[Constants.MessageFields.imageURL] {
if imageURL.hasPrefix("gs://") {
Storage.storage().reference(forURL: imageURL).getData(maxSize: INT64_MAX) {(data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
DispatchQueue.main.async {
cell.imageView?.image = UIImage.init(data: data!)
cell.setNeedsLayout()
}
}
} else if let URL = URL(string: imageURL), let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage.init(data: data)
}
cell.textLabel?.text = "sent by: \(name)"
} else {
let text = message[Constants.MessageFields.text] ?? ""
cell.textLabel?.text = name + ": " + text
cell.imageView?.image = UIImage(named: "ic_account_circle")
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.imageView?.image = UIImage(data: data)
}
}
return cell
}
Upload an image from the user, then sync this image's storage URL to database so this image is sent inside the message.
Modify your FCViewController's "imagePickerController: didFinishPickingMediaWithInfo:" method; replace with the code defined below:
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion:nil)
guard let uid = Auth.auth().currentUser?.uid else { return }
// if it's a photo from the library, not an image from the camera
if #available(iOS 8.0, *), let referenceURL = info[UIImagePickerControllerReferenceURL] as? URL {
let assets = PHAsset.fetchAssets(withALAssetURLs: [referenceURL], options: nil)
let asset = assets.firstObject
asset?.requestContentEditingInput(with: nil, completionHandler: { [weak self] (contentEditingInput, info) in
let imageFile = contentEditingInput?.fullSizeImageURL
let filePath = "\(uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\((referenceURL as AnyObject).lastPathComponent!)"
guard let strongSelf = self else { return }
strongSelf.storageRef.child(filePath)
.putFile(from: imageFile!, metadata: nil) { (metadata, error) in
if let error = error {
let nsError = error as NSError
print("Error uploading: \(nsError.localizedDescription)")
return
}
strongSelf.sendMessage(withData: [Constants.MessageFields.imageURL: strongSelf.storageRef.child((metadata?.path)!).description])
}
})
} else {
guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { return }
let imageData = UIImageJPEGRepresentation(image, 0.8)
let imagePath = "\(uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
self.storageRef.child(imagePath)
.putData(imageData!, metadata: metadata) { [weak self] (metadata, error) in
if let error = error {
print("Error uploading: \(error)")
return
}
guard let strongSelf = self else { return }
strongSelf.sendMessage(withData: [Constants.MessageFields.imageURL: strongSelf.storageRef.child((metadata?.path)!).description])
}
}
}
Firebase Remote Config allows you to remotely configure your active clients. FriendlyChat messages are restricted to a maximum length. While this length can be defined directly in the client, defining this maximum length with Firebase Remote Config allows an update to the maximum length to be applied to active clients.
In Firebase console, select the "Remote Config" panel and click "Add your first parameter". Set the parameter key to friendly_msg_length and the parameter value to 10. Select Publish Changes to apply the updates.
Confirm the pod 'Firebase/RemoteConfig'
dependency exists in your Podfile
file.
func configureRemoteConfig() {
remoteConfig = RemoteConfig.remoteConfig()
// Create Remote Config Setting to enable developer mode.
// Fetching configs from the server is normally limited to 5 requests per hour.
// Enabling developer mode allows many more requests to be made per hour, so developers
// can test different config values during development.
let remoteConfigSettings = RemoteConfigSettings(developerModeEnabled: true)
remoteConfig.configSettings = remoteConfigSettings!
}
Create a fetch request for config and add a completion handler to pick up and use the config parameters.
func fetchConfig() {
var expirationDuration: TimeInterval = 3600
// If in developer mode cacheExpiration is set to 0 so each fetch will retrieve values from
// the server.
if self.remoteConfig.configSettings.isDeveloperModeEnabled {
expirationDuration = 0
}
// cacheExpirationSeconds is set to cacheExpiration here, indicating that any previously
// fetched and cached config would be considered expired because it would have been fetched
// more than cacheExpiration seconds ago. Thus the next fetch would go to the server unless
// throttling is in progress. The default expiration duration is 43200 (12 hours).
remoteConfig.fetch(withExpirationDuration: expirationDuration) { [weak self] (status, error) in
if status == .success {
print("Config fetched!")
guard let strongSelf = self else { return }
strongSelf.remoteConfig.activateFetched()
let friendlyMsgLength = strongSelf.remoteConfig["friendly_msg_length"]
if friendlyMsgLength.source != .static {
strongSelf.msglength = friendlyMsgLength.numberValue!
print("Friendly msg length config: \(strongSelf.msglength)")
}
} else {
print("Config not fetched")
if let error = error {
print("Error \(error)")
}
}
}
}
Firebase App Invites provide a simple way for your users to share your application with their friends through Email or SMS.
Confirm the pod 'Firebase/Invites'
dependency exists in your Podfile
:
pod 'Firebase/Invites'
Present invite dialog when invite button is clicked:
@IBAction func inviteTapped(_ sender: AnyObject) {
if let invite = Invites.inviteDialog() {
invite.setInviteDelegate(self)
// NOTE: You must have the App Store ID set in your developer console project
// in order for invitations to successfully be sent.
// A message hint for the dialog. Note this manifests differently depending on the
// received invitation type. For example, in an email invite this appears as the subject.
invite.setMessage("Try this out!\n -\(Auth.auth().currentUser?.displayName ?? "")")
// Title for the dialog, this is what the user sees before sending the invites.
invite.setTitle("FriendlyChat")
invite.setDeepLink("app_url")
invite.setCallToActionText("Install!")
invite.setCustomImage("https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png")
invite.open()
}
}
Add the inviteFinished withInvitations: handler to print out the result.
func inviteFinished(withInvitations invitationIds: [Any], error: Error?) {
if let error = error {
print("Failed: \(error.localizedDescription)")
} else {
print("Invitations sent")
}
}
You now know how to enable invites. Congrats!
Firebase Analytics provides a way for you to understand the way users move through your application, where they succeed and where they get stuck and turn back. It can also be used to understand the most used parts of your application.
Add measurement helper methods.
static func sendLoginEvent() {
Analytics.logEvent(AnalyticsEventLogin, parameters: nil)
}
static func sendLogoutEvent() {
Analytics.logEvent("logout", parameters: nil)
}
static func sendMessageEvent() {
Analytics.logEvent("message", parameters: nil)
}
If you want to view this activity in your Firebase console, select Product ... Scheme... Edit Scheme in Xcode. In the Arguments Passed on Launch section, click the + to add a new argument and add add `-FIRAnalyticsDebugEnabled` as the new argument.
AdMob gives you a way to easily monetize the application, you simply add the AdView placeholder and Google handles the ad delivery for you.
Confirm the pod 'Firebase/AdMob'
dependency exists in your Podfile
file.
pod 'Firebase/AdMob'
func loadAd() {
self.banner.adUnitID = kBannerAdUnitID
self.banner.rootViewController = self
self.banner.load(GADRequest())
}
Firebase Crashlytics allows your application to report when crashes occur and log the events leading up to the crash.
Confirm that pod 'Fabric', '~> 1.7.2' and pod 'Crashlytics', '~> 3.9.3' dependencies exist in your Podfile file.
"${PODS_ROOT}/Fabric/run"
@IBAction func didPressCrash(_ sender: AnyObject) {
print("Crash button pressed!")
Crashlytics.sharedInstance().crash()
}
Firebase Test Lab lets you test your app on various types of iOS devices across multiple SDK levels and locales. The best part is that all this testing happens automatically in the cloud without you needing to maintain a collection of test devices.
Go to http://g.co/firebase/testlabsignup and fill the form with your email and ProjectId
There are three things you need to do to build iOS tests in a Test Lab-compatible format:
Xcode places compiled iOS artifacts, including any tests you build, in a Derived Data directory. It is possible to keep the default location for that directory, if you'd like, but it's often helpful to choose a more easily-accessible place for the files, especially if you're going to be running tests with Test Lab often:
Test Lab runs instrumentation tests using the XCTest framework. To run your app's XCTests on Test Lab devices, build it for testing on a Generic iOS Device:
Finally, package your test for upload to Test Lab by compressing the test files you built into a .zip file:
Once you have an XCTest .zip file, you're ready to start testing:
When the test starts, you're automatically redirected to the test results page. Tests can take a number of minutes to run, depending on the number of different configurations you have selected and the test timeout duration set for your tests. After your tests have run, you can review test results. See Analyzing Firebase Test Lab Results to learn more about how to interpret the test results.
You have used Firebase to easily build a real-time chat application.
You can use Firebase Cloud Messaging (FCM) to send notifications to users of your app. In this section we will configure the application to receive reengagement notifications which you can send from Firebase console.
The Firebase/Messaging dependency provides the ability to send and receive FCM messages. Confirm the pod 'Firebase/Messaging'
dependency exists in your Podfile
file.
pod 'Firebase/Messaging'
First, let's add a method to display the notifications we're receiving so we'll be able to see them clearly on the UI. Add the following to your AppDelegate:
func showAlert(withUserInfo userInfo: [AnyHashable : Any]) {
let apsKey = "aps"
let gcmMessage = "alert"
let gcmLabel = "google.c.a.c_l"
if let aps = userInfo[apsKey] as? NSDictionary {
if let message = aps[gcmMessage] as? String {
DispatchQueue.main.async {
let alert = UIAlertController(title: userInfo[gcmLabel] as? String ?? "",
message: message, preferredStyle: .alert)
let dismissAction = UIAlertAction(title: "Dismiss", style: .destructive, handler: nil)
alert.addAction(dismissAction)
self.window?.rootViewController?.presentedViewController?.present(alert, animated: true, completion: nil)
}
}
}
}
Implement UNUserNotificationCenterDelegate after the implementation of "AppDelegate" class (append to the end of the file) to receive display notification via APNS for devices running iOS 10 and above.
import UserNotifications
@available(iOS 10, *)
extension AppDelegate : UNUserNotificationCenterDelegate {
// Receive displayed notifications for iOS 10 devices.
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
showAlert(withUserInfo: userInfo)
// Change this to your preferred presentation option
completionHandler([])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
showAlert(withUserInfo: userInfo)
completionHandler()
}
}
Register your app for remote notifications inside the "application:didFinishLaunchingWithOptions:" function
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
GIDSignIn.sharedInstance().delegate = self
// Register for remote notifications. This shows a permission dialog on first run, to
// show the dialog at a more appropriate time move this registration accordingly.
if #available(iOS 10.0, *) {
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions) {_,_ in }
// For iOS 10 display notification (sent via APNS)
UNUserNotificationCenter.current().delegate = self
} else {
let settings: UIUserNotificationSettings =
UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
application.registerUserNotificationSettings(settings)
}
application.registerForRemoteNotifications()
return true
}
At this point, you'll receive notifications when the app is in the background. Add notification handlers to handle the incoming notification when the app is in the foreground and show an alert with the notification message.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
showAlert(withUserInfo: userInfo)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
showAlert(withUserInfo: userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
That's it! Your app is ready to receive messages through FCM.
Hooray! You can re-engage your users easily with FCM. See the documentation for more on FCM.