用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 三 部分
- 原文链接 : Create an iOS Chat App using JSQMessagesViewController – Part 3
- 原文作者 : Mariusz Wisniewski
- 译者 : kmyhy
在本教程中,我将介绍如何创建一个简单的 iOS 聊天 App(用 swift 和 Syncano)。
在第一部分和第二部分,我们创建了一个新项目,用 JSQMessagesViewController 作为前端,用 Syncano 作为 App 的后端。
在第三部分,我们将增加用户登录认证——包括注册、登录和显示每条消息的发送者名称。
如果你错过了前面的部分,你可以在这里找到它们:
- Create an iOS Chat App using JSQMessagesViewController – Part 1
- Create an iOS Chat App using JSQMessagesViewController – Part 21
你可以从第一部分开始,也可以从这里下载第二部分的代码然后开始。
注意,这个项目使用了 CocoaPods,如果你没有用过它,不知道如何安装它——请参考第一部分。
导入 IQKeyboardManagerSwift
添加一个 CocoaPod
让我们来添加一个新的 CocoaPod —— 它叫做 IQKeyboardManagerSwift。因为我们会创建自己的登录/注册界面——这会用到 TextField 组件——我们不想将精力分散到和键盘的交互处理上,所以我们要用到这个库。
打开终端,进入我们的项目路径,例如:
cd ~/path/to/my/project/SyncanoChat
打开 Podfile:
open Podfile
加入这一句:
pod ‘JSQMessagesViewController’
修改后的 Podfile 最终变成这样:
# Uncomment this line to define a global platform for your project
# platform :ios, '8.0'
# Uncomment this line if you're using Swift
use_frameworks!
target 'SyncanoChat' do
pod 'syncano-ios'
pod 'JSQMessagesViewController'
pod 'IQKeyboardManagerSwift'
end
target 'SyncanoChatTests' do
end
target 'SyncanoChatUITests' do
end
保存文件,关闭文本编辑器,输入终端命令:
pod update
当命令执行完,打开 Workspace 文件:
open SyncanoChat.xcworkspace
编译项目,确认没有任何错误发生。
使用 IQKeyboardManagerSwift
Open AppDelegate.swift file and add two new imports - one for IQKeyboardManagerSwift and one for Syncano. Beginning of your file should contain these lines:
打开 AppDelegate.swift 导入两个库 —— 一个是 IQKeyboardManagerSwift,一个是 Syncano:
import syncano_ios
import IQKeyboardManagerSwift
在 application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool 方法中,启用 keyboard manager,创建一个 Syncano 共享实例:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
Syncano.sharedInstanceWithApiKey(syncanoApiKey, instanceName: syncanoInstanceName)
IQKeyboardManager.sharedManager().enable = true
IQKeyboardManager.sharedManager().shouldResignOnTouchOutside = true
return true
}
这时,Xcode 会报告 use of unresolved identifiers 错误。我们马上就来解决这个问题。
添加 settings 文件
为了偷懒,我们会使用一个新文件,用于保存 Syncano 的登录凭证。
新建一个 Swift 文件,命名为 Settings。
打开 Settings.swift,编辑内容为:
let syncanoApiKey = ""
let syncanoInstanceName = ""
let syncanoChannelName = "messages"
在这里输入你的 API key 和实例名 (也就是你写在 ViewController.swift 的那两个值).
打开 ViewController.swift 删除这两句:
```swift
let syncanoChannelName = "messages"
let syncano = Syncano.sharedInstanceWithApiKey("YOUR_API_KEY", instanceName: "YOUR_INSTANCE_NAME")
<div class="se-preview-section-delimiter"></div>
保存文件,编译项目,确保它能正常运行。
新建 LoginViewController
要实现登录和注册逻辑,我们需要增加一个 Controller。新建一个 Swift 文件,命名为 LoginViewController。
编辑其内容为:
import UIKit
import syncano_ios
let loginViewControllerIdentifier = "LoginViewController"
protocol LoginDelegate {
func didSignUp()
func didLogin()
}
class LoginViewController: UIViewController {
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
var delegate : LoginDelegate?
}
//MARK - UI
extension LoginViewController {
@IBAction func loginPressed(sender: UIButton) {
if self.areBothUsernameAndPasswordFilled() {
self.login(self.getUsername()!, password: self.getPassword()!, finished: { error in
if (error != nil) {
//handle error
print("Login, error: \(error)")
} else {
self.cleanTextFields()
self.delegate?.didLogin()
}
})
}
}
@IBAction func signUpPressed(sender: UIButton) {
if self.areBothUsernameAndPasswordFilled() {
self.signUp(self.getUsername()!, password: self.getPassword()!, finished: { error in
if (error != nil) {
//handle error
print("Sign Up, error: \(error)")
} else {
self.cleanTextFields()
self.delegate?.didSignUp()
}
})
}
}
func getUsername() -> String? {
return self.emailTextField.text
}
func getPassword() -> String? {
return self.passwordTextField.text
}
func areBothUsernameAndPasswordFilled() -> Bool {
if let username = self.emailTextField.text, password = self.passwordTextField.text {
if (username.characters.count > 0 && password.characters.count > 0) {
return true
}
}
return false
}
func cleanTextFields() {
self.emailTextField.text = nil
self.passwordTextField.text = nil
}
}
//MARK - Syncano
extension LoginViewController {
func login(username: String, password: String, finished: (NSError!) -> ()) {
SCUser.loginWithUsername(username, password: password) { error in
finished(error)
}
}
func signUp(username: String, password: String, finished: (NSError!) -> ()) {
SCUser.registerWithUsername(username, password: password) { error in
finished(error)
}
}
}
<div class="se-preview-section-delimiter"></div>
这段代码都做了些什么?
- LoginDelegate 协议:定义了两个方法。根据我们的例子,委托对象是 ViewController,所以一旦用户登录或者创建了新账号,就通过这两个方法让我们的主控制器知道。
- @IBAction func loginPressed(sender: UIButton) 和 @IBAction func signUpPressed(sender: UIButton) 方法:处理相应的按钮事件。在这两个方法里面,进行登录和注册操作,当操作成功后又通过协议方法通知委托对象。
- getUsername() -> String?、getPassword() -> String?、 areBothUsernameAndPasswordFilled() -> Bool 和 cleanTextFields() 方法:是4个助手方法,用于读取两个 TextField 的输入内容,清空它们,检查它们的内容是否为空。
-
使用 Syncano 的 SCUser 类进行用户登录和注册的两个方法:
//MARK - Syncano extension LoginViewController { func login(username: String, password: String, finished: (NSError!) -> ()) { SCUser.loginWithUsername(username, password: password) { error in finished(error) } } func signUp(username: String, password: String, finished: (NSError!) -> ()) { SCUser.registerWithUsername(username, password: password) { error in finished(error) } } }
如你所见,整个过程很简单——提供两个参数:用户名、密码,如果操作不成功,返回一个错误(例如 password was incorrect for login 或者 user already exists for signup 等)。
修改ViewController
新的特性马上就可以用了。万事俱备,只欠东风,我们只需要把所有东西连接起来就 OK 了。
使用 LoginViewController
在 ViewController 中,我们会根据用户登录状态来决定是否要显示 LoginViewController。首先需要一个 LoginViewController 引用。新增一个属性:
let loginViewController = UIStoryboard(name: “Main”, bundle: nil).instantiateViewControllerWithIdentifier(loginViewControllerIdentifier) as! LoginViewController
实现 viewDidAppear 方法——这个方法在视图显示到屏幕之后调用。在这个方法中检查用户是否已经登录,如果用户未登录,则显示登录界面:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.showLoginViewControllerIfNotLoggedIn()
}
<div class="se-preview-section-delimiter"></div>
因为我们还没实现 self.showLoginViewControllerIfNotLoggedIn() 方法,我们需要来实现它。增加一个新的扩展:
//MARK - Login Logic
extension ViewController : LoginDelegate {
func didSignUp() {
self.prepareAppForNewUser()
self.hideLoginViewController()
}
func didLogin() {
self.prepareAppForNewUser()
self.hideLoginViewController()
}
func prepareAppForNewUser() {
self.setupSenderData()
self.reloadAllMessages()
}
func isLoggedIn() -> Bool {
let isLoggedIn = (SCUser.currentUser() != nil)
return isLoggedIn
}
func logout() {
SCUser.currentUser()?.logout()
}
func showLoginViewController() {
self.presentViewController(self.loginViewController, animated: true) {
}
}
func hideLoginViewController() {
self.dismissViewControllerAnimated(true) {
}
}
func showLoginViewControllerIfNotLoggedIn() {
if (self.isLoggedIn() == false) {
self.showLoginViewController()
}
}
@IBAction func logoutPressed(sender: UIBarButtonItem) {
self.logout()
self.showLoginViewControllerIfNotLoggedIn()
}
}
<div class="se-preview-section-delimiter"></div>
这段代码做了些什么?
- 实现了前面定义的 LoginProtocol 协议(这样,ViewController 会在用户登录或注册新账号之后得到通知)
- 几个助手方法,检查用户是否登录(通过判断 SCUser.currentUser() 方法返回值是否为 nil),显示或隐藏登录界面,用于处理登出(只需要在当前用户上调用 logout 方法)两个方法。
修改 setup 扩展
这里会出现两个错误。我们需要定义两个方法:
- setupSenderData() 方法:根据用户登录信息修改发送者信息(替代早先我们使用的设备唯一标识
- reloadAllMessages() 方法:重新下载最新的聊天消息并显示给用户。
在 ViewController 中找到 setup 扩展(这个扩展的开头使用了 “// MARK - Setup” 进行注释),将它修改为:
//MARK - Setup
extension ViewController {
func setup() {
self.title = "Syncano ChatApp"
self.setupSenderData()
self.channel.delegate = self
self.channel.subscribeToChannel()
self.loginViewController.delegate = self
}
func setupSenderData() {
let sender = (SCUser.currentUser() != nil) ? SCUser.currentUser().username : ""
self.senderId = sender
self.senderDisplayName = sender
}
}
<div class="se-preview-section-delimiter"></div>
这里,我们添加了缺失的 setupSenderData() 方法,在这个方法中将 senderId 和 senderDisplayName 设置为 Syncano 中的用户名。在Syncano 中,用户名是唯一的,因此可以用作 senderId。
在 setup() 方法中,我们设置了 title,将 LoginViewController 的 delegate 设置为 self(我们已经为 self 实现了委托协议),然后调用 setupSenderData() 方法来设置发送者信息。
(我们删除了在第一部分中添加测试消息的方法——我们已经用不着它了)
最后,我们还要实现 reloadAllMessages() 方法:
func reloadAllMessages() {
self.messages = []
self.reloadMessagesView()
self.downloadNewestMessagesFromSyncano()
}
<div class="se-preview-section-delimiter"></div>
要重新加载聊天消息,只需要将 messages 数组清空,重新刷新消息视图,避免数据源和消息视图之间的不一致,然后从 Syncano 下载最新的消息。
最后就只剩下一块了。找到 ViewController 的数据源扩展,添加两个方法:
override func collectionView(collectionView: JSQMessagesCollectionView!, attributedTextForMessageBubbleTopLabelAtIndexPath indexPath: NSIndexPath!) -> NSAttributedString! {
let data = self.collectionView(self.collectionView, messageDataForItemAtIndexPath: indexPath)
if (self.senderDisplayName == data.senderDisplayName()) {
return nil
}
return NSAttributedString(string: data.senderDisplayName())
}
override func collectionView(collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
let data = self.collectionView(self.collectionView, messageDataForItemAtIndexPath: indexPath)
if (self.senderDisplayName == data.senderDisplayName()) {
return 0.0
}
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
第一个方法返回每条消息的发送者名称——用于表明这条消息是谁发的。而对于我们自己发出的消息,我们不需要显示这个名称——我们知道自己所发的消息,因为这些消息总是位于右边(其他人的消息位于左边),因此没有必要浪费空间了。
第二个方法定义了发送者名称 Label 的高度。如果我们要显示发送者名称(发送者不是自己的消息),我们需要返回一个默认高度(返回 JSQMessagesViewController 中的一个常量), 否则返回 0,Label 就不会显示。
修改故事板
在你运行 App 之前,还剩最后一件事:
- 修改故事板,添加一个 Login View Controller。
- 在 ViewController 上添加一个 logout 登出按钮,以便在运行时切换用户。
添加导航控制器
打开 Main.storyboard 文件,加入一个导航控制器。
拖一个 Navigation Controller 到故事板中。删除它自带的根 ViewController(通过 Delete 或 Backspace 键)。将初始控制器入口(蓝色箭头)拖到导航控制器上,在导航控制器上右键,拖一条线到 ViewController 上。在弹出菜单中,选择 root view controller。
添加登出按钮
拖一个 Bar Button Item 到 ViewController 的导航条上。双击按钮,将它的名字改成 Logout。在按钮上右击,拖一条线到左边 Outline 窗口的 ViewController,在弹出菜单中选择 loginPressed:,这样就将按钮事件连接到了实现定义的方法上。
添加登录界面
拖一个 View Controller 到故事板中。选中这个 View Controller,在 Identity 面板中将 Class 设置为 LoginViewController。将 Storyboard ID 也设置为同样值。
然后,拖 2 个 Text Fields 和 2 个 Button 到这个 View Controller,设置 TextField 的 Placeholder 属性和按钮的 Title 属性。
右键,从Login View Controller 拖一条线到两个 Text Field,将两个 TextField 连接到相应的出口。
右键,从两个按钮拖一条线到 Login View Controller,将两个按钮的 Action 连接到相应的方法。
添加约束
在故事板中,依次选择 LoginViewController 上的 UI 元素并设置它们的约束。这里,我让它们依次和最近的元素等间距对齐,并设置了固定高度(使用默认值)。
好了!你可以运行程序,它的所有功能都能使用了!
你可以运行多个 App 实例,登录多个账号并查看这个实时聊天 App 是否工作正常。
还有一个 Bug——如果你用同一个账号登录在两台设备上登录,如果在其中一台上发消息,则在另一台上不会立马显示这条消息。如果你能在 12 月 20 号前将解决办法发送给我们,我们会提供一个小礼物给你!
你可以将答案、思路或代码发送到 email 地址 love@syncano.com!
总结
你已经完成了本教程的第三部分,你的 App 已经可以使用了。你可以将它分发给你的朋友们,根据是否由你发送还是由别人发送,消息的显示是截然不同的。
这部分的代码在这里下载。
如果你有任何问题,请立即在下面的评论中留言,或者 tweet 我。