[译] 使用 Swift 5 构建 iOS 移动端群聊 App

使用 Swift 5 构建 iOS 移动端群聊 App

不管是独立的群聊应用,嵌入式的客户服务组件,或者是约会应用里面的私人一对一聊天,各类特征和规模的移动端聊天无处不在。html

在本教程中,咱们将向你展现如何使用 Swift 5 构建一个 iOS 移动聊天应用程序,其可让任意数量的用户进行实时聊天。咱们还将向你展现如何存储消息历史记录,所以当用户离开以后回来时,他们的消息仍然在应用程序中。前端

为了实现上述的应用,咱们使用了 PubNub 的一些关键特性:发布/订阅(实时消息)和 存储 & 回放(消息存储)android

  • 发布是每一个客户端如何将本身的消息发送到全世界的方式,或者至少传递到本身想要发布的频道中。Pub/Sub 模式最简单的应用就是将你发送的每一条消息传递给订阅频道的任何人。发布须要一个 PubNub 链接的实例(我将在后面详细介绍),要发送的消息消息(类型为 String、NSNumber、Array 和 Dictionary),以及咱们要将消息发送到的频道。了解有关 Swift 发布的更多信息。
  • 订阅是 PubNub 即时通讯的另一个部分。为了订阅,咱们须要一个 PubNub 链接的实例和一个要订阅的频道。成功订阅后,咱们会收到消息,但若是咱们在消息到达的时候不进行处理,咱们仍然看不到这些消息。了解有关 Swift 订阅的更多信息
  • 事件处理或监听的更新在 PubNub 的生命周期中很是重要。Pub/Sub 虽然很是引人注目,但使用 PubNub 的关键是事件处理程序,它将数据流网络链接到咱们的控制台和应用程序。它们中的一个专门监听消息,而另外一个负责寻找其余任何内容,包括订阅变更和错误。
  • 存储和回放是这个伟大的功能集的另一个关键点。若是存储和消息检索已导入你的工程,那么存储和播放对你的应用程序来讲也是很好的补充。这个功能容许检索历史消息。应用程序消息的范围囊括了应用程序的整个生命周期。咱们将设置 PubNub 账户并获取 API 密钥,在 PubNub 管理控制台中设置存储的生存时间。了解有关 Swift 中存储和回放的更多信息

在看完这个教程以后,你会实现一个提供了聊天室服务的应用,而且这个应用能够是其余任何应用程序很好的基础或者补充。ios

完整的 Swift 5 iOS 聊天应用程序能够在这里找到git

构建

PubNub

若是你尚未 PubNub 帐户,能够在这里注册一个账户。登陆后,建立一个新的应用程序。单击它并建立一个新的密钥集或单击已有的演示版。 你如今应该看到发布和订阅密钥,咱们能够经过其使用 PubNub API。github

在 keys 下,咱们能够启用不一样的选项!让咱们在左下角附近启用存储和回放功能。咱们如今能够决定你但愿保留多长时间的消息。我选择了一天的保留时长并保存了更改。在保留设置下,还能够设置启用从 PubNub 历史记录中删除swift

Xcode 应用构建

打开 Xcode 并建立一个新项目,选择单视图应用程序,给他起一个名字,而后关闭项目。使用终端导航到项目文件夹,运行命令 gem install cocoapods 或运行命令 gem update cocoapods 来更新已有的安装。后端

在终端中输入 touch Podfile,为你的应用建立 Podfile,而后使用 open Podfile 打开文件。api

将下面的代码写入到文件中,确保将“application-target-name”替换为项目的名称。数组

source ‘https://github.com/CocoaPods/Specs.git'
# 若是出现编译问题,能够选择取消下面的注释而且填写完整
# project ‘<path to project relative to this Podfile>/<name of project without extension>’ # workspaceMyPubNubProjectuse_frameworks! # 用你的项目名称替换下一行中的引号里面的内容 targetapplication-target-namedo # 下面的配置只适用于 # 最小编译目标为 # iOS 8 的项目 platform :ios, ‘8.0’ # (or ‘9.0’ or ‘10.0’) podPubNub”, “~> 4” 复制代码

以后在终端中执行命令 pod install。这个命令会帮你在项目中安装 PubNub。安装完成以后,双击 .xcworkspace 文件能够打开项目工程。

设计应用程序

在咱们开始介绍全部逻辑以前,让咱们先设计并构建应用程序的视图。首先咱们从登陆视图开始。

经过高亮点击类声明中的名字,将 ViewController.swift 重命名为 ConnectVC.swift,而且进入 Editor -> Refactor -> Rename。

当用户打开应用程序时,除了链接按钮外,咱们但愿他们有一个字段来输入他们想要链接的用户名和频道。将这些添加到你的第一个视图中。另外,我选择了 Topically 做为咱们应用程序的标题,你也能够选择一个更酷的标题。

而后,我经过 control + 拖动的方式,将个人 storyboard 上的项目拖动到个人 ConnectVC 文件,来为个人用户名和频道的 TextFields 设置 outlet。对按钮执行相同操做,但不要使用 outlet,而是建立 UIButton 的 action,以便在按下时执行操做。

使用 PubNub 的聊天应用程序的 Xcode Swift storyboard 截图

接下来,让咱们建立频道聊天视图。

建立一个新的 Cocoa Touch 类并将其命名为 ChannelVC。在 storyboard 中建立一个新的视图控制器,并将该类设置为 ChannelVC。选择该视图时,请转到屏幕顶部,而后单击 Editor -> Embed In -> Navigation Controller。另外一个视图如今应该在你的 storyboard 中。这是导航控制器,它容许用户在进入视图之间切换。

将一个 UIBarButtonItem 添加到 ChannelVC 导航栏上的左侧位置,这是咱们的“离开”按钮。按住 Control 键并将其拖到 ChannelVC.swift,并建立名为 leaveChannel,UIBarButtonItem 类型的 action。将 UITableView 拖到 ChannelVC 视图中。使其占据屏幕的大部分空间,但须要流出空间放置另外一个 TextField 和一个带有文本 Send 的按钮。建立它们。

在 ChannelVC.swift 中为 table 和 TextField 建立 outlet,并为发送按钮添加另外一个 action。

咱们的下一步不涉及咱们的 ChannelVC,而是在咱们的 table 内建立自定义单元格。一旦咱们获得 ChannelVC 设置的整体布局,咱们就必须在 tableView 中自定义单元格。建立一个新的 cocoa touch 类而且命名为 MessageCell,并将 UITableViewCell 拖到表视图中。将该 cell 类设置为新类,并将标识符更改成 MessageCell。

拖动任何东西来完成你想要的设计和须要的任何细节。咱们将用户名和消息标签放入 cell 中,完成以后,按住 Ctrl 键拖动便可为 MessageCell 类建立 outlet。确保设置样式约束,以便表格不会压缩你的内容。

有关使你的应用程序适用于全部屏幕尺寸的更多信息,请参阅 Apple 关于自动布局的文档或者查阅众多的在线指南。

如今咱们获得不少不错的 view 视图,但它们之间没法进行自由切换。单击 ConnectVC 上方有其名称的栏,而后单击黄色圆圈。按住 Control 键并将其拖动到导航控制器并选择 show 选项。选择导航控制器,单击右侧面板上的属性选项卡,其顶部显示“Storyboard Segue”。将标识符命名为“connectSegue”。当你单击 ConnectVC 上的链接按钮时,就能够执行这个 Segue 了。

咱们须要的下一个也是最后一个任务是将咱们从 ChannelVC 导航到 ConnectVC。选择 ChannelVC 的方式与 ConnectVC 相同,并将其拖到 ConnectVC。此次选择“Present Modally”并在属性检查器中将其命名为“leaveChannelSegue”。

PubBub 聊天程序的 Xode Storyboard

编码:ConnectVC

如今咱们已经完成了 storyboard,让咱们开始编码。咱们将从 ConnectVC 开始,它为咱们的 ChannelVC 提供用户名和频道,咱们将利用咱们全部的 PubNub 知识。首先,在咱们的链接操做中执行 segue。

@IBAction func connectToChannel(_ sender: UIButton) {
    self.performSegue(withIdentifier: "connectSegue", sender: self)
}
复制代码

这利用了咱们在上一节中制做的 connectSegue,它将咱们导航到了 ChannelVC 的导航控制器。咱们在这个视图控制器中惟一须要作的就是为上面的 segue 作准备。经过重写这个功能,咱们能够在视图之间发送信息。

注意:在本教程中,若是用户未提供用户名,我会自动为其分配用户名“A Naughty Moose”。若是他们没有提供频道,我会将他们发送到频道“General”。

为了访问咱们想要访问的视图,咱们须要得到导航控制器的实例,而后从那里获取咱们的 ChannelVC 视图。咱们检查文本字段是否为空,若是须要则替换值,而后使用咱们的用户名和频道在 ChannelVC 中设置两个咱们还没有建立的变量。

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    // 访问导航控制器和 ChannelVC 视图
    if let navigationController = segue.destination as? UINavigationController,
        let channelVC = navigationController.viewControllers.first as? ChannelVC{
        var username = ""
        var channel = ""

        // 下面的空字符串替换成一个你须要的默认值
        if(usernameTextField.text == "" ){
            username = "A Naughty Moose"
        }
        else{
            username = usernameTextField.text ?? "A Naughty Moose"
        }
        if(channelTextField.text == "" ){
            print("nothing in channel")
            channel = "General"
        }
        else{
            channel = channelTextField.text ?? "General"
        }

        // 设置 ChannelVC 的变量值
        channelVC.username = username
        channelVC.channelName = channel
    }
}
复制代码

编码:ChannelVC

在咱们的 ChannelVC 中,咱们应该有两个 outlet,一个 action,另外一个是咱们的 viewDidLoad 函数。最重要的是,在类定义下,咱们将开始为类的其他部分定义一些咱们须要的变量和资源。

首先,让咱们的类监听 PubNub 事件并使其与咱们的 table 一块儿工做。在文件顶部引入 PubNub,在咱们的类定义中的 UIViewController 写入 PNObjectEventListener、UITableViewDataSource 和 UITableViewDelegate 以后。咱们的类如今应该显示错误,单击错误并添加建议的那些类,这样进行引入比较便捷。

  • 在咱们的类定义下,让咱们定义一个结构,使得处理咱们的消息更容易一些。个人结构有三个字符串:消息,用户名和 UUID。稍后当咱们发布消息时,你能够发送不一样的信息并使用这些来更新结构。
  • 以后,建立一个 Message 数组并将其初始化为空,由于全部类变量都须要具备某种初始值。
  • 为咱们最先收到的消息建立一个 NSNumber 类型的标记,并设置标记的初始值为 -1。
  • 另外一个变量,用于跟踪咱们是否已开始加载更多消息。
  • 如今,对于这个视图控制器来讲,最重要的、用于发布和订阅的变量,是咱们将在此视图控制器中调用 PubNub 函数的对象。
  • 而后咱们获得用户在最后一步中输入的用户名和频道,并使用临时值进行初始化。
  • 以后应该是咱们的消息文本字段,咱们的 tableView,以及咱们的发送 action。
class ChannelVC: UIViewController,PNObjectEventListener, UITableViewDataSource, UITableViewDelegate {

    // 咱们的消息结构体,可让消息的处理更方便
    struct Message {
        var message: String
        var username: String
        var uuid: String
    }
    var messages: [Message] = []

    // 跟踪咱们加载的最先的一条消息
    var earliestMessageTime: NSNumber = -1

    // 来跟踪咱们是否已经加载了更多消息
    var loadingMore = false

    // 咱们使用 PubNub 对象来发布,订阅和获取咱们频道的内容
    var client: PubNub!

    // 临时值
    var channelName = "Channel Name"
    var username = "Username"

    //-- 应该已经存在在你的文件里面了
    // 消息入口
    @IBOutlet weak var messageTextField: UITextField!

    // 咱们用来自 messages 数组的信息填充了这个 View
    @IBOutlet weak var tableView: UITableView!

    //...

}
复制代码

咱们已经创建了一些能够在整个代码中使用的全局变量,接下来,让咱们设置 viewDidLoad 函数。在调用继承的 viewDidLoad 以后,将导航控制器顶部的标题更改成频道名称,并将 table view 的 delegate 数据源设置为 self。

self.navigationController?.navigationBar.topItem?.title = channelName

tableView.delegate = self
tableView.dataSource = self
复制代码

接下来,咱们配置并初始化咱们的 PubNub 对象。你能够在此处插入 PubNub 账户中的发布和订阅密钥。咱们将 stripMobilePayload 设置为 false,由于它已弃用,并为此链接提供惟一的 UUID,这使咱们在未来更容易开发更多功能。接着初始化它,将它设置为监听器,并订阅用户选择的频道。而后咱们调用将在下一步建立的方法 loadLastMessages。

// 设置咱们的 PubNub 对象!
let configuration = PNConfiguration(publishKey: "INSERT PUBLISH KEY", subscribeKey: "INSERT SUBSCRIBE KEY")
// 删除已经弃用的警告
configuration.stripMobilePayload = false
// 给每一个链接设置标志以供未来进行开发
configuration.uuid = UUID().uuidString
client = PubNub.clientWithConfiguration(configuration)
client.addListener(self)
client.subscribeToChannels([channelName],withPresence: true)

// 咱们加载最后的消息来填充 tableview
loadLastMessages()
复制代码

如今应该有一个 error,说咱们在 viewDidLoad 末尾调用的函数是未定义的,因此让咱们定义它!此功能用于在链接到通道时加载初始消息。它利用咱们即将建立的,名为 addHistory 的另外一个函数。

让咱们调用下一个函数,使用 nil 做为开始和结束,而后设置你想要接收的消息的数量,最多为 100。咱们在函数内部的最后一个操做是将咱们的 table view 向下滚动到表格底部的新消息。

// 当这个视图初始化加载来填充 tableview 的时候,将调用此函数
func loadLastMessages()
{
    addHistory(start: nil, end: nil, limit: 10)
    // 将 tableview 滚动到最新消息的底部
    if(!self.messages.isEmpty){
        let indexPath = IndexPath(row: self.messages.count-1, section: 0)
        self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
    }
}
复制代码

存储和回放历史消息

如今,咱们能够经过这个功能回顾咱们频道的历史记录。使用一些关键参数建立它,只有 limit 参数是必须的,而后调用函数就能够容许咱们查看频道历史记录了。

咱们使用具备许多重载版本的函数 historyForChannel。咱们可使用返回最后 100 条消息的简单消息或者接收开始和结束时间的消息,这两种方法都由 PNHistoryResultBlock 处理,这容许咱们访问查询结果和 error。

首先,让咱们检查结果是否为非空,若是是,咱们就能够开始访问消息了!一旦咱们知道咱们的消息至少包含某些内容,咱们就能够开始访问它们。咱们须要使用咱们在结果中收到的最先消息来更新咱们的 earlistMessage 开始时间。接下来将咱们返回的对象转换为咱们可使用的的对象,一个键值为 String 类型的数组。

咱们能够从这个新对象建立一个 Message 对象,将它们添加到一个临时数组中,而后将它插入到咱们的全局消息数组的开头而不是每次都去费力地直接访问这些对象。确保从新加载表格而后检查错误!

// 获取而且将频道的历史消息放入到 messages 数组中
func addHistory(start:NSNumber?,end:NSNumber?,limit:UInt){
    // PubNub 函数,它返回 X 消息的对象,而后发送第一个和最后一个消息的时间
    // limit 是接收的消息数量,最大值为 100,默认值为 100
    client.historyForChannel(channelName, start: start, end: end, limit:limit){ (result, status) in
        if(result != nil && status == nil){

            // 当咱们想要加载更多的时候,咱们会保存最先发送的消息的时间,以便获取以前的消息。
            self.earliestMessageTime = result!.data.start

            // 将咱们得到的 [Any] 包转换为 String 和 Any 的 dictionary
            let messageDict = result!.data.messages as! [[String:String]]

            // 从中建立新的消息而且将它们放在消息数组的末尾
            var newMessages :[Message] = []
            for m in messageDict{
                let message = Message(message: m["message"]! , username: m["username"]!, uuid: m["uuid"]! )
                newMessages.append(message)
            }
            self.messages.insert(contentsOf: newMessages, at: 0)
            // 使用新的消息从新加载 tableview,而且将 tableview 向下滚动到最新消息的底部
            self.tableView.reloadData()

            // 确保在加载完成以前,咱们没法尝试从新加载更多数据
            self.loadingMore = false
        }
        else if(status !=  nil){
            print(status!.category)

        }
        else{
            print("everything is nil whaaat")
        }
    }
}
复制代码

接下来,让咱们填写 tableView 所需的函数。第一个,numberOfRowsInSection 是一个简单的单行函数,返回数组中的消息数。第二个函数,咱们首先须要获取消息 cell 的实例,并将 cell 标签的文本设置为消息和消息数组索引的用户名。而后,直接返回 cell 就能够了!

// 须要 tableview 函数
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // 后面修改
    return messages.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell") as! MessageCell

    cell.messageLabel.text = messages[indexPath.row].message
    cell.usernameLabel.text = messages[indexPath.row].username


    return cell
}
复制代码

在 Swift 中使用和调试 PubNub 最重要的部分之一就是为事件和消息建立监听器。在这个应用程序中,咱们使用函数 didRecieveMessage,这个函数容许咱们访问进入到咱们频道的消息。此函数内部的逻辑就是咱们的 loadLastMessages 函数的精简版本。

检查进来的消息是否与咱们订阅的频道匹配,以防咱们订阅到其余频道的内容。获取咱们给出的消息,并将其转换为键值类型为 String 的数组。使用该 dictionary 建立消息并将其绑定到消息数组的末尾。

再次从新加载数据,而后向下滚动到新消息。能够根据你想要的实现更改这个操做。我在控制台中打印消息以便进行调试。

func client(_ client: PubNub, didReceiveMessage message: PNMessageResult) {
    // 每当咱们收到一条新的消息时,咱们都会将它添加到咱们消息数组的末尾
    // 从新加载 table,以便消息展现在底部

    if(channelName == message.data.channel)
    {
        let m = message.data.message as! [String:String]
        self.messages.append(Message(message: m["message"]!, username: m["username"]!, uuid: m["uuid"]!))
        tableView.reloadData()


        let indexPath = IndexPath(row: messages.count-1, section: 0)
        tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)

    }

    print("Received message in Channel:",message.data.message!)
}
复制代码

如今咱们能够在第一次打开频道时加载一些历史消息,当发送新消息时,它们会显示在底部。

若是有比咱们最初加载的消息更多的消息怎么办?在这个新函数 scrollViewDidScroll 中,当咱们从顶部向下拉时,咱们从 historyForChannel 中提取了另一些消息。这个函数也能够修改,以便当用户没有到达页面顶部时,它能够进行预加载,来帮助实现一个无限滚动的效果。

咱们有一个名为 loadingMore 的全局变量,咱们在开始时检查是否已经加载了更多消息,而后检查用户是否滚动超过某个阈值来开始加载更多消息。值得庆幸的是,使用 PubNub 很是快,因此几乎能够当即加载。一旦真的有更多历史消息,咱们将 loadingMore 设置为 true 并开始调用咱们的 addHistory 函数,将 earliestMessageTime 做为开始,将 nil 做为结束,能够根据你的需求来设置 limit,尽管返回消息的最大值是 100。

// 这个方法容许用户经过从顶部向下拖动来查询更多消息
func scrollViewDidScroll(_ scrollView: UIScrollView){
    //If we are not loading more messages already
    if(!loadingMore){

        // 当从消息顶部向下拖动超过 -40 的时候
        if(scrollView.contentOffset.y < -40 ) {
            loadingMore = true
            addHistory(start: earliestMessageTime, end: nil, limit: 10)
        }
    }

}
复制代码

咱们如今须要在单击“发送”按钮时发布消息。为此,让咱们建立一个函数来发送 messageTextField 中的消息。首先,咱们检查 messageTextField 是否为空,若是是,则进行处理,而后创一个 dictionary 用于包含你要发送的消息信息,以后在 PubNub 对象上使用简单发布功能。

这个函数接收多种类型的变量和对象做为消息和频道名称发送。你也能够在回调中包含一个处理程序,以根据状态代码执行某些操做。以后,调用咱们刚刚在 sendMessage 操做中建立的函数。

func publishMessage() {
    if(messageTextField.text != "" || messageTextField.text != nil){
        let messageString: String = messageTextField.text!
        let messageObject : [String:Any] =
            [
                "message" : messageString,
                "username" : username,
                "uuid": client.uuid()
        ]

        client.publish(messageObject, toChannel: channelName) { (status) in
            print(status.data.information)
        }
        messageTextField.text = ""
    }

}

// 单击发送按钮的时候,将会发送消息
@IBAction func sendMessage(_ sender: UIButton) {
    publishMessage()
}
复制代码

为了使咱们的应用程序彻底正常工做,咱们须要可以离开频道并返回 ConnectVC。咱们已经有了这个功能,咱们只须要填写它。取消订阅客户订阅的全部频道,而后执行咱们最初建立的“leaveChannelSegue”。

client.unsubscribeFromAll()
self.performSegue(withIdentifier: "leaveChannelSegue", sender: self)
复制代码

运行 App

让咱们来运行一下这个应用程序!

PubNub Swift 聊天应用程序

咱们如今拥有了基本的聊天功能。用户能够实时发送和接收消息,而且历史消息能够被存储一段时间。

这个项目完整的 Github 仓库在这里

PubNub 提供每月一百万条免费的消息。这里有 PubNub Swift SDK 文档,以及其余 75+ PubNub 客户端 SDKs。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索