用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 三 部分

用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 3 部分
  • 原文链接 : 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 文件,加入一个导航控制器。
用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 三 部分

拖一个 Navigation Controller 到故事板中。删除它自带的根 ViewController(通过 Delete 或 Backspace 键)。将初始控制器入口(蓝色箭头)拖到导航控制器上,在导航控制器上右键,拖一条线到 ViewController 上。在弹出菜单中,选择 root view controller。

添加登出按钮

用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 三 部分

拖一个 Bar Button Item 到 ViewController 的导航条上。双击按钮,将它的名字改成 Logout。在按钮上右击,拖一条线到左边 Outline 窗口的 ViewController,在弹出菜单中选择 loginPressed:,这样就将按钮事件连接到了实现定义的方法上。

添加登录界面

用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 三 部分

拖一个 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 连接到相应的方法。

添加约束

用 JSQMessagesViewController 创建一个 iOS 聊天 App - 第 三 部分

在故事板中,依次选择 LoginViewController 上的 UI 元素并设置它们的约束。这里,我让它们依次和最近的元素等间距对齐,并设置了固定高度(使用默认值)。

好了!你可以运行程序,它的所有功能都能使用了!

你可以运行多个 App 实例,登录多个账号并查看这个实时聊天 App 是否工作正常。

还有一个 Bug——如果你用同一个账号登录在两台设备上登录,如果在其中一台上发消息,则在另一台上不会立马显示这条消息。如果你能在 12 月 20 号前将解决办法发送给我们,我们会提供一个小礼物给你!

你可以将答案、思路或代码发送到 email 地址 love@syncano.com!

总结

你已经完成了本教程的第三部分,你的 App 已经可以使用了。你可以将它分发给你的朋友们,根据是否由你发送还是由别人发送,消息的显示是截然不同的。

这部分的代码在这里下载。

如果你有任何问题,请立即在下面的评论中留言,或者 tweet 我。