- 原文地址:How to Build an iOS Mobile Group Chat App with Swift 5
- 原文做者:Samba Diallo
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:LucaslEliane
- 校對者:江五渣,Endone
不管是獨立的羣聊應用,嵌入式的客戶服務組件,或者是約會應用裏面的私人一對一聊天,各類特徵和規模的移動端聊天無處不在。html
在本教程中,咱們將向你展現如何使用 Swift 5 構建一個 iOS 移動聊天應用程序,其可讓任意數量的用戶進行實時聊天。咱們還將向你展現如何存儲消息歷史記錄,所以當用戶離開以後回來時,他們的消息仍然在應用程序中。前端
爲了實現上述的應用,咱們使用了 PubNub 的一些關鍵特性:發佈/訂閱(實時消息)和 存儲 & 回放(消息存儲)。android
在看完這個教程以後,你會實現一個提供了聊天室服務的應用,而且這個應用能夠是其餘任何應用程序很好的基礎或者補充。ios
完整的 Swift 5 iOS 聊天應用程序能夠在這裏找到。git
若是你尚未 PubNub 帳戶,能夠在這裏註冊一個賬戶。登陸後,建立一個新的應用程序。單擊它並建立一個新的密鑰集或單擊已有的演示版。 你如今應該看到發佈和訂閱密鑰,咱們能夠經過其使用 PubNub API。github
在 keys 下,咱們能夠啓用不一樣的選項!讓咱們在左下角附近啓用存儲和回放功能。咱們如今能夠決定你但願保留多長時間的消息。我選擇了一天的保留時長並保存了更改。在保留設置下,還能夠設置啓用從 PubNub 歷史記錄中刪除。swift
打開 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>’ # workspace ‘MyPubNubProject’ use_frameworks! # 用你的項目名稱替換下一行中的引號裏面的內容 target ‘application-target-name’ do # 下面的配置只適用於 # 最小編譯目標爲 # iOS 8 的項目 platform :ios, ‘8.0’ # (or ‘9.0’ or ‘10.0’) pod 「PubNub」, 「~> 4」 複製代碼
以後在終端中執行命令 pod install
。這個命令會幫你在項目中安裝 PubNub。安裝完成以後,雙擊 .xcworkspace 文件能夠打開項目工程。
在咱們開始介紹全部邏輯以前,讓咱們先設計並構建應用程序的視圖。首先咱們從登陸視圖開始。
經過高亮點擊類聲明中的名字,將 ViewController.swift 重命名爲 ConnectVC.swift,而且進入 Editor -> Refactor -> Rename。
當用戶打開應用程序時,除了鏈接按鈕外,咱們但願他們有一個字段來輸入他們想要鏈接的用戶名和頻道。將這些添加到你的第一個視圖中。另外,我選擇了 Topically 做爲咱們應用程序的標題,你也能夠選擇一個更酷的標題。
而後,我經過 control + 拖動的方式,將個人 storyboard 上的項目拖動到個人 ConnectVC 文件,來爲個人用戶名和頻道的 TextFields 設置 outlet。對按鈕執行相同操做,但不要使用 outlet,而是建立 UIButton 的 action,以便在按下時執行操做。
接下來,讓咱們建立頻道聊天視圖。
建立一個新的 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」。
如今咱們已經完成了 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 中,咱們應該有兩個 outlet,一個 action,另外一個是咱們的 viewDidLoad 函數。最重要的是,在類定義下,咱們將開始爲類的其他部分定義一些咱們須要的變量和資源。
首先,讓咱們的類監聽 PubNub 事件並使其與咱們的 table 一塊兒工做。在文件頂部引入 PubNub,在咱們的類定義中的 UIViewController 寫入 PNObjectEventListener、UITableViewDataSource 和 UITableViewDelegate 以後。咱們的類如今應該顯示錯誤,單擊錯誤並添加建議的那些類,這樣進行引入比較便捷。
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)
複製代碼
讓咱們來運行一下這個應用程序!
咱們如今擁有了基本的聊天功能。用戶能夠實時發送和接收消息,而且歷史消息能夠被存儲一段時間。
PubNub 提供每月一百萬條免費的消息。這裏有 PubNub Swift SDK 文檔,以及其餘 75+ PubNub 客戶端 SDKs。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。