备注: 本教程已由 Attila Hegedüs 更新适配 iOS 10 和 Swift 3,原教程由David East 创作。
原文:https://www.raywenderlich.com/139322/firebase-tutorial-getting-started-2
翻译:JoeyChang 转载请标明出处
Firebase 是一个移动后台服务,它可以帮助我们创建具有优秀特性的移动 apps。Firebase 提供以下三个主要服务: a realtime database, user authentication and hosting。通过集成 Firebase iOS SDK, 你几乎不用写一行代码就能创建出非常棒的应用。
Firebase 具有数据库实时性这样的独特性能。
你曾经使用过 pull-to-refresh 去拉新数据么?有了 Firebase,现在你可以忽略那种刷新数据方法了。
当 Firebase 数据库更新时,所有的连接者可以实时获取更新,这就意味着你的 app 可以不用用户交互就能获取数据库当前最新值。
本篇 Firebase 教程中,我们将通过创建一个名叫 Grocr 的具有协作性的grocery list app , 来学习Firebase 的一些基本原理。当我们添加一个项目到列表时,它将实时出现在用户的其它设备中,但是我们并不满足于此,我们还将调整 Grocr 让它可以离线工作,以致即使仅有一个 grocery 数据连接,列表也能保持同步。
通过本文,你将学习到以下技能:
- 保存数据到Firebase数据库
- 从 Firebase 实时同步数据
- 验证 users
- 在线监控 users
- 实现离线支持
开始,下载初始项目 Grocr-starter. 它使用 CocoaPods 管理 Firebase 。
在 Xcode 中打开 Grocr.xcworkspace,该项目包含三个view controllers:
LoginViewController.swift.
现在登录功能还是使用的硬编码 user credentials,稍后我们将优化它。GroceryListTableViewController.swift.
这个 controller 是 UITableViewController 子类,它通过 UIAlertController 添加 items 到本地数据库的 list 表格。OnlineUsersTableViewController.swift.
该 controller 使用 Firebase’s presence feature 展示所有当前在线 users。
此外,还有两个模型类 GroceryItem.swift 和 User.swift 。它们做为 app 的数据模型。
Build and run, 你将看到如下这样效果:
注: 当 build 工程时,我们将看到一些 ‘nullability’ 编译警告。它们来自Firebase,暂时我们先忽略它们,稍后解决。
我们可以点击 Login 进行登录,这将使用一个写死的 user 数据,现在该 app 还只能使用本地数据。接下来我们将调用 Firebase 数据使 app 生动起来。
创建 Firebase 账号
有两个重要步骤:
- 创建免费 Firebase 账号
- 获取你第一个 app 的 URL
我们可以访问 Getting Started page 进行注册。当我们使用我们谷歌账号共享登录进入 firebase, 我们将看到一个干净的 Firebase 控制台。不要担心费用问题,现在 Firebase 免费版本已经足够强大,够用了。
创建我们的第一个工程,点击 CREATE NEW PROJECT 。在弹出的对话框中输入项目名称以及你的首选 国家/地区:
点击 CREATE PROJECT, 我们就可以通过控制面板来管理我们的项目了。
这将作为所有 Firebase 服务的容器,我们用它存储数据和授权用户。
选择 Add Firebase to your iOS app 开始我们的项目。本项目的 bundle ID 是 rw.firebase.gettingstarted,所以添加此 id 到 iOS bundle ID 文本框。
点击 ADD APP ,将下载一个 GoogleService-Info.plist 文件。将该文件拖拽到 Xcode 中的 Grocr 项目。
点击 CONTINUE. 接下来一页描述怎样安装 Firebase SDK。
本项目已经替我们集成好了,所以点击 CONTINUE 继续。最后一页说明当 app 启动时怎样连接到 Firebase。
点击 FINISH ,查看新项目细节。
在 Xcode 打开 GroceryListTableViewController.swift ,添加如下代码,创建 Firebase 连接。
let ref = FIRDatabase.database().reference(withPath: "grocery-items")
这个 Firebase 连接使用已提供的 path。在 documentation 中,这些 Firebase 属性被称为 references ,它们指定 Firebase 的位置。
简言之,这些属性可以实现保存和同步数据到给定的位置。
我们发现,base URL 不是必须的,相反,它使用 grocery-items 的 child path。Firebase 数据库是 JSON NoSQL 数据库,所以数据都是保存为 JSON 格式。
JSON 是分等级的 key-value 数据结构 -- keys 指的是可以根据它获取其它对象格式的 values 值。JSON data 是一个简单的 key value 对儿树形结构。
在 Firebase 中,key 是一个 URL,value是形如 number, string, boolean , object 的随意的数据。
Structuring Data
无论客户端是什么数据格式,保存到 Firebase 的是 JSON 格式。下面是一个 JSON 示例:
// The root of the tree
{ // grocery-items
"grocery-items": {
// grocery-items/milk
"milk": {
// grocery-items/milk/name
"name": "Milk",
// grocery-items/milk/addedByUser
"addedByUser": "David"
},
"pizza": {
"name": "Pizza",
"addedByUser": "Alice"
},
}
}
在上面的 JSON 中,你可以看到每对儿数据都是以键值对儿形式出现的。我们可以继续遍历树并在更深的位置检索数据。
在上面的例子中,我们可以通过路径检索所有的 grocery item。
grocery-items
如果你想获取第一个 grocery item ,你可以通过以下路径获取:
grocery-items/milk
因为所有的 Firebase keys 对应paths,所以 key 的名字选择很重要。
Understanding Firebase References
一个基本的原则是,Firebase 引用指向 Firebase 中数据存储的位置。如果我们创建多引用,那么这些引用共享同一个连接。
看如下代码:
// 1
let rootRef = FIRDatabase.database().reference()
// 2
let childRef = FIRDatabase.database().reference(withPath: "grocery-items")
// 3
let itemsRef = rootRef.child("grocery-items")
// 4
let milkRef = itemsRef.child("milk")
// 5
print(rootRef.key) // prints: ""
print(childRef.key) // prints: "grocery-items"
print(itemsRef.key) // prints: "grocery-items"
print(milkRef.key) // prints: "milk"
下面我们解释下:
- 我们创建一个到 Firebase 数据库 root 引用。
- 使用一个 URL ,我们可以创建一个引用到 Firebase 数据库的子路径。
- 通过给 rootRef 传递子路径,我们可以使用 child(_:) 创建子引用,这个引用和上面的引用是一样意思。
- 使用 itemsRef ,我们可以创建到 milk 的子引用。
- 每个引用都有 key 属性。这个属性和 Firebase 数据库关键字的名字一样。
我们不需要在同一个项目中都添加这样的代码,这里只是出于展示目的进行列举。
Adding New Items to the List
在 GroceryListTableViewController.swift 的底部,找到 addButtonDidTouch(_:) 方法。
在这里我们要实现通过 UIAlertController 的方式添加一个新的 item 。
在 saveAction 方法内,现在仅仅保存数据到一个本地 array,因此 saveAction 不能同步不同客户端的数据,而且在下次启动 app 时,保存的数据将丢失。
没有人会使用不能记录或者同步他们 grocery 清单数据的 app ! 让我们完善 saveAction 方法:
let saveAction = UIAlertAction(title: "Save",
style: .default) { _ in
// 1
guard let textField = alert.textFields?.first,
let text = textField.text else { return }
// 2
let groceryItem = GroceryItem(name: text,
addedByUser: self.user.email,
completed: false)
// 3
let groceryItemRef = self.ref.child(text.lowercased())
// 4
groceryItemRef.setValue(groceryItem.toAnyObject())
}
注释如下:
从 alert controller 获取 text field 和它的内容。
使用当前用户数据创建一个新的 GroceryItem 。
使用 child(_:) 创建一个子引用,这个引用的 key 是 item 的小写名称,因此如果我们添加一个复制的 item (即使使用大写字母,或者使用混合字母),数据库只保存最后一个。
使用 setValue(_:) 保存数据到数据库。这个方法期望一个字典格式。GroceryItem 有个 toAnyObject() 方法,可以转换对象为字典格式。
在你可以连接数据库之前,我们还需要配置它。找到 AppDelegate.swift ,并在 application(_:didFinishLaunchingWithOptions:) 返回 true 之前添加如下代码:
FIRApp.configure()
默认情况,Firebase 数据库需要用户授权读写权限。在浏览器进入 Firebase 控制面板,选中左边的 Database 选项,设置 RULES 如下:
{
"rules": {
".read": true,
".write": true
}
}
修改后,选择 PUBLISH 按钮进行保存设置。
Build and run. 在 Firebase 控制面板,选择 DATA 标签,并将浏览器窗口紧挨模拟器。当我们在模拟器中添加 item ,我们将看到它会出现在控制面板。
现在,我们就有了一个可以实时添加数据到 Firebase 的活生生的 grocery list app!但是虽然 key 特性已经可以运行完好了,但是没有数据添加到table view。
那么我们怎样才能将数据从数据库同步到 table view 呢?
Retrieving Data
我们可以通过 observeEventType(_:withBlock:) 方法异步检索 Firebase 中的数据。
在 GroceryListTableViewController.swift 的 viewDidLoad() 下添加如下方法:
ref.observe(.value, with: { snapshot in
print(snapshot.value)
})
该方法有两个参数:FIRDataEventType 的一个实例以及一个闭包。
event type 确定我们要监听的事件,.value 监听诸如 add, removed, changed 这样的 Firebase 数据库重点数据改变。
当改变发生,数据库使用最新数据更新 app 显示。
app 在闭包方法中通过接受到的 FIRDataSnapshot 一个实例获知数据改变。snapshot,代表某个特定时间点的数据快照。我们可以通过 value 那个属性获取到 snapshot 的数据。
Build and run,我们将看到,在控制台会有 items 列表数据被打印出来。
Optional({
pizza = {
addedByUser = "hungry@person.food";
completed = 0;
name = Pizza;
};
})
Synchronizing Data to the Table View
注意打印日志--现在在 table view 中可以看到 grocery 列表了。
在 GroceryListTableViewController.swift, 替换之前的代码片段为如下代码:
// 1
ref.observe(.value, with: { snapshot in
// 2
var newItems: [GroceryItem] = []
// 3
for item in snapshot.children {
// 4
let groceryItem = GroceryItem(snapshot: item as! FIRDataSnapshot)
newItems.append(groceryItem)
}
// 5
self.items = newItems
self.tableView.reloadData()
})
以上代码的诸行解释:
添加一个监听器监听 grocery-items 改变了什么。
存储最近一次版本数据到闭包中本地的一个变量中。
监听者闭包返回最近数据的一个 snapshot,这个 snapshot 包含所有的 grocery items,而不是仅仅包含改变的 items。使 snapshot.children ,我们可以循环获取 grocery items 。
GroceryItem 结构有一个常用的实例化器,它使用 FIRDataSnapshot
来填充它的属性。snapshot 的值可以为任意类型,可以是 dictionary, array, number, or string。当创建好一个 GroceryItem 实例,它被添加到一个包含最近一次版本数据的数组中。将最新版本的数据赋值给 items,然后更新 table view,使它展示最新数据。
Build and run. 添加一个 pizza item 怎么样? 它将显示到 table view。
不用刷新,就可以及时获取到更新后的数据。
Removing Items From the Table View
table view 将同步我们所有的改变数据, 但是当我们想删除 pizza 时,现在还不能更新。
为了通知数据库删除数据,我们需要设置一个 Firebase reference,当用户轻扫时候删除 item。
定位到 tableView(_:commit:forRowAt:)。现在,该方法使用 index 移除 array 中的 grocery item。这可以实现功能,但我们还有更好的解决方法。替换为如下实现方式:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let groceryItem = items[indexPath.row]
groceryItem.ref?.removeValue()
}
}
Firebase 遵从单向数据流模型,因此 viewDidLoad() 的 listener 监听 grocery list 的最新数据。清除 item 触发数据改变。
index path 的 row 被用来获取相关的 grocery item。每个 GroceryItem 拥有一个名为 ref 的 Firebase reference property,调用 它的 removeValue() 将移除我们在 viewDidLoad() 定义的 listener。该listener有一个闭包,它使用最新的数据重新加载表视图。
Build and run. 轻扫 item ,点击删除,我们发现 app 和 Firebase 的数据都消失了。
Nice work! 我们 items 可以实时删除了。
Checking Off Items
现在我们知道了怎么添加、删除以及同步 items ,这很酷。但是当我们实际购物时候会怎样呢?我们会删除我们刚购买的物品么,或者当我们添加购物车时给物品打个标记是否更好?
在以前的纸质时代,人们过去常常把东西从购物清单上划掉,因为我们也将在我们的 app 用现代的方式模仿这个行为。
打开 GroceryListTableViewController.swift ,找到 toggleCellCheckbox(_:isCompleted:) 方法,该方法可以根据 item 是否完成来切换UITableViewCell 的必要视图属性。
当 table view 第一次加载后,刚方法在tableView (_:cellForRowAtIndexPath:) 中会被调用,以及当用户点击 cell 时也会被调用。
替换 tableView(_:didSelectRowAt:) 方法为如下:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 1
guard let cell = tableView.cellForRow(at: indexPath) else { return }
// 2
let groceryItem = items[indexPath.row]
// 3
let toggledCompletion = !groceryItem.completed
// 4
toggleCellCheckbox(cell, isCompleted: toggledCompletion)
// 5
groceryItem.ref?.updateChildValues([
"completed": toggledCompletion
])
}
以下为详细注解:
- 使用 cellForRow(at:) 确定用户点击的 cell。
- 根据 index path 的 row 获取对应的 GroceryItem。
- 改变 grocery item 的 completed 的状态。
- 调用 toggleCellCheckbox(_:isCompleted:) 更新 cell 的属性。
- 在 updateChildValues(:) 方法中,通过传递字典参数,更新Firebase。该方法与 setValue(:) 不同,因为它只应用更新,而setValue(_:) 具有破坏性,并在该引用中替换整个值。
Build and run. 点击一个 item,我们就可以看到该行被勾号标记并排序。
恭喜,我们已经完成了一个相当漂亮的 grocery list app 。
Sorting the Grocery List
如果把 ice cream 放在未排序的标记里面,有时我们可能会忘记它。现在让我们进行些优化。
如果可以把已选中的 items 自动移动到列表底部,我们的 app 将更加令人喜欢。这样,未被标记的 items 可以更容易被我们发现。
使用 Firebase queries, 我们可以根据任意属性对列表进行排序,在GroceryListTableViewController.swift, 更新 viewDidLoad() 方法:
ref.queryOrdered(byChild: "completed").observe(.value, with: { snapshot in
var newItems: [GroceryItem] = []
for item in snapshot.children {
let groceryItem = GroceryItem(snapshot: item as! FIRDataSnapshot)
newItems.append(groceryItem)
}
self.items = newItems
self.tableView.reloadData()
})
通过关键词 “ completed”,使用 Firebase 引用 queryOrdered(byChild:) 对数据进行排序。
由于列表需要完成顺序,所以 completed 键将传递给查询。然后,queryOrdered(byChild:)返回一个引用,通知服务器以有序的方式返回数据。
Build and run. 点击一行,使其置换为已完成状态,我们将看到,它神奇地自动移动到了最后一行。
哇! 我们现在真的让购物变得更容易了。跨多个用户同步数据,似乎应该足够简单,例如,与一个重要的其他用户或 housemate。这听起来像…身份验证!
Authenticating Users
Firebase 有一个 authentication service,它允许 apps 验证不同的提供者,我们可以使用 Google, Twitter, Facebook, Github, email & password, 匿名, 甚至 custom backends 这些方式。这里我们使用邮箱和密码方式进行身份认证,因为这种方式是最简单的。
进入 Firebase dashboard ,点击 Auth,激活邮箱密码认证。
选中 SIGN-IN METHOD 标签栏,再在 Sign-in providers 那一节选中Email/Password 行,切换 Enable 并点击 SAVE:
Firebase 存储账户信息到 keychain,因此最后一步,在项目中,切换到 target’s Capabilities 打开 Keychain Sharing 开关。
现在,我们已经可以使用邮箱和密码进行身份认证了。
Registering Users
在 LoginViewController.swift,找到 signUpDidTouch(_:) 方法,这里会弹出 UIAlertController 让用户注册账号,定位到 saveAction 方法,添加以下代码到方法块儿。
// 1
let emailField = alert.textFields![0]
let passwordField = alert.textFields![1]
// 2
FIRAuth.auth()!.createUser(withEmail: emailField.text!,
password: passwordField.text!) { user, error in
if error == nil {
// 3
FIRAuth.auth()!.signIn(withEmail: self.textFieldLoginEmail.text!,
password: self.textFieldLoginPassword.text!)
}
}
以上代码解释:
- 从弹框中获取邮箱和密码。
- 调用 Firebase 方法 createUser(withEmail:password:),传递邮箱和密码给它。
- 如果执行没有错误,用户账号即被创建。但是,我们还要再进行一下登录操作 signIn(withEmail:password:) ,同样需要传递邮箱和密码。
Build and run. 点击 Sign up ,键入邮箱和密码,点击保存。现在 view controller 还不能在登录成功后导航到其它地方。我们刷新 Firebase Login & Auth ,我们将看到新建的用户。
喔!我们的 app 现在可以让用户注册并进行登录了,不过我们先不要庆祝,我们还需要再做些优化,好使用户更好的使用它。
Logging Users In
Sign up 按钮可以注册和登录,然而 Login 现在还什么都做不了,因为我们还没有给它绑定验证。
到 LoginViewController.swift, 找到 loginDidTouch(_:) 方法,修改如下:
@IBAction func loginDidTouch(_ sender: AnyObject) {
FIRAuth.auth()!.signIn(withEmail: textFieldLoginEmail.text!,
password: textFieldLoginPassword.text!)
}
当用户点击 Login 时,这些代码将验证用户信息。
我们接下来需要在用户登录成功后导航到下一个页面。
Observing Authentication State
Firebase 有可以监控用户验证状态的观察者。这里是添加 segue 最好的地方。在 LoginViewController: 添加如下代码:
override func viewDidLoad() {
super.viewDidLoad()
// 1
FIRAuth.auth()!.addStateDidChangeListener() { auth, user in
// 2
if user != nil {
// 3
self.performSegue(withIdentifier: self.loginToList, sender: nil)
}
}
}
注释如下:
使用 addStateDidChangeListener(_:) 创建验证观察者。该 block 被传入两个参数:auth 和 user。
测试 user 的值,如果验证通过,返回用户信息,如果验证失败,返回 nil 。
验证成功,进行页面跳转。传输 sender 为 nil 。这看起来有些奇怪,但是稍后我们将在 GroceryListTableViewController.swift 进行设置。
Setting the User in the Grocery List
在 GroceryListTableViewController.swift 文件 viewDidLoad(): 方法底部添加如下代码:
FIRAuth.auth()!.addStateDidChangeListener { auth, user in
guard let user = user else { return }
self.user = User(authData: user)
}
这里我们添加了一个 Firebase auth object 的验证观察者,当用户成功登录时,依次分配用户属性。
Build and run. 如果用户已经登录,app 将跳过 LoginViewController 直接导航到 GroceryListTableViewController. 当用户添加 items ,他们的 email 将显示到 cell 的详情里面。
Success! app 现在已经有了基本的用户验证功能。
Monitoring Users’ Online Status
现在既然我们的 app 已经拥有了用户验证功能,那是时候添加监控哪个用户在线功能了。打开 GroceryListTableViewController.swift ,添加如下 property:
let usersRef = FIRDatabase.database().reference(withPath: "online")
这是一个指向存储在线用户列表的在线位置的Firebase引用。
下一步,在 viewDidLoad() 方法下添加如下代码到 addStateDidChangeListener(_:) 闭包的下面。
// 1
let currentUserRef = self.usersRef.child(self.user.uid)
// 2
currentUserRef.setValue(self.user.email)
// 3
currentUserRef.onDisconnectRemoveValue()
注释如下:
- 使用用户的 uid 创建一个 child 引用,当 Firebase 创建一个账号时,这个引用会被生成。
- 使用这个引用保存当前用户的 email.
- 当 Firebase 连接关闭的时候,例如用户退出 app , 调用 currentUserRef 的 onDisconnectRemoveValue(),删除位置引用的值。这可以完美监控离线用户。
Build and run. 当 view 加载时,当前用户的电子邮件,会被添加在当前在线位置的一个子节点。
Great! 现在当用户数量增加时,是时候改变 bar button item 的个数了。
Updating the Online User Count
仍然在 GroceryListTableViewController.swift 的 viewDidLoad() 方法下添加如下代码:
usersRef.observe(.value, with: { snapshot in
if snapshot.exists() {
self.userCountBarButtonItem?.title = snapshot.childrenCount.description
} else {
self.userCountBarButtonItem?.title = "0"
}
})
这创建一个观察者监控在线用户,当用户在线或者离线,userCountBarButtonItem 的 title 随之更新。
Displaying a List of Online Users
打开 OnlineUsersTableViewController.swift,在 class 的 property section 添加一个本地引用到 Firebase 的在线用户记录。
let usersRef = FIRDatabase.database().reference(withPath: "online")
然后,在viewDidLoad(), 替换代码
currentUsers.append("hungry@person.food")
为如下:
// 1
usersRef.observe(.childAdded, with: { snap in
// 2
guard let email = snap.value as? String else { return }
self.currentUsers.append(email)
// 3
let row = self.currentUsers.count - 1
// 4
let indexPath = IndexPath(row: row, section: 0)
// 5
self.tableView.insertRows(at: [indexPath], with: .top)
})
代码注释如下:
创建一个 children added 监听器,添加到被 usersRef 管理的位置。这与值侦听器不同,因为只有添加的 child 被传递到闭包。
从 snapshot 获取值,并赋值给本地变量 array。
因为 table view 的坐标从 0 开始计算,当前的 row 总是等于 array 的个数 -1。
使用当前 row index 创建一个 NSIndexPath.
使用动画从顶部添加一行到 table view.
这将只渲染添加的条目,而不是重新加载整个列表,而且还可以指定一个漂亮的动画。:]
由于用户可以脱机,table 需要对被删除的用户做出反应。在我们刚刚添加的代码下面添加以下内容:
usersRef.observe(.childRemoved, with: { snap in
guard let emailToFind = snap.value as? String else { return }
for (index, email) in self.currentUsers.enumerated() {
if email == emailToFind {
let indexPath = IndexPath(row: index, section: 0)
self.currentUsers.remove(at: index)
self.tableView.deleteRows(at: [indexPath], with: .fade)
}
}
})
这只是添加了一个观察者,它侦听被删除的 usersRef 引用的子元素。它在本地数组中搜索电子邮件的值,以找到相应的子条目,一旦找到,它就从表中删除相关的行。
Build and run.
在 Firebase 用户仪表板上点击 Online ,当前用户的电子邮件将出现在表格中。使用一些技巧,可以在网上添加一个用户,一旦你做了,它就会显示在列表中。在仪表板上单击删除按钮,用户就会从 table 中消失….
Booyah! 当用户被添加和删除的时候,table 随之更新了。
Enabling Offline
杂货店因不稳定的数据连接而臭名昭著。你会认为他们现在都有了Wi-Fi,但是没有!
不过没关系,我们只需设置数据库离线工作。打开 * AppDelegate*,在(_:didFinishLaunchingWithOptions:) 底部方法返回 true 之前,添加如下代码:
FIRDatabase.database().persistenceEnabled = true
是的,就是这样! 就像我们的应用能够离线运行一样。当 app 重启,一旦建立网络连接,离线更新也将作用于我们的 Firebase 数据库。Oooh-ahhhh !
Where To Go From Here?
我们可以在这里下载 Grocr-final完整项目。
注意:下载完后,我们仍需要添加自己的 GoogleService-Info.plist 和 设置允许Keychain sharing 。
在这个Firebase教程中,我们通过构建一个协作的购物清单 app 了解了Firebase的基础知识,我们已经实现了将数据保存到一个 Firebase 数据库、实时同步数据、认证用户、监视在线用户状态以及实现了离线支持。所有这些都是在没有写一行服务器代码的情况下完成的! :]
如果你对 Firebase 感兴趣,请查看文档 documentation,以及 Firebase 提供的示例。
如果您对这个Firebase教程、Firebase或示例应用有任何意见或问题,请加入下面的论坛讨论!
上海 虹桥V1
2017.09.06 19:02