在本教程中,我將介紹如何創建一個簡單的 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,如果你沒有用過它,不知道如何安裝它——請參考第一部分。
讓我們來添加一個新的 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
編譯項目,確認沒有任何錯誤發生。
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 錯誤。我們馬上就來解決這個問題。
為了偷懶,我們會使用一個新文件,用於保存 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")
保存文件,編譯項目,確保它能正常運行。
要實現登錄和注冊邏輯,我們需要增加一個 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)
}
}
}
這段代碼都做了些什麼?
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 等)。
新的特性馬上就可以用了。萬事俱備,只欠東風,我們只需要把所有東西連接起來就 OK 了。
在 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()
}
因為我們還沒實現 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()
}
}
這段代碼做了些什麼?
實現了前面定義的 LoginProtocol 協議(這樣,ViewController 會在用戶登錄或注冊新賬號之後得到通知) 幾個助手方法,檢查用戶是否登錄(通過判斷 SCUser.currentUser() 方法返回值是否為 nil),顯示或隱藏登錄界面,用於處理登出(只需要在當前用戶上調用 logout 方法)兩個方法。這裡會出現兩個錯誤。我們需要定義兩個方法:
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
}
}
這裡,我們添加了缺失的 setupSenderData() 方法,在這個方法中將 senderId 和 senderDisplayName 設置為 Syncano 中的用戶名。在Syncano 中,用戶名是唯一的,因此可以用作 senderId。
在 setup() 方法中,我們設置了 title,將 LoginViewController 的 delegate 設置為 self(我們已經為 self 實現了委托協議),然後調用 setupSenderData() 方法來設置發送者信息。
(我們刪除了在第一部分中添加測試消息的方法——我們已經用不著它了)
最後,我們還要實現 reloadAllMessages() 方法:
func reloadAllMessages() {
self.messages = []
self.reloadMessagesView()
self.downloadNewestMessagesFromSyncano()
}
要重新加載聊天消息,只需要將 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。
vcz1yc+ho8uru/ewtMWlo6y9q8v8tcTD+9fWuMSzySBMb2dvdXSho9TasLTFpcnP09K796Oszc/Su8z1z9+1vdfzsd8gT3V0bGluZSC0sL/atcQgVmlld0NvbnRyb2xsZXKjrNTata+z9rLLtaXW0NGh1PEgbG9naW5QcmVzc2VkOqOs1eLR+b7NvauwtMWlysK8/sGsvdO1vcHLyrXP1rao0uW1xLe9t6jJz6GjPC9wPg0KPGgzIGlkPQ=="添加登錄界面">添加登錄界面
拖一個 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 地址 [email protected]!
你已經完成了本教程的第三部分,你的 App 已經可以使用了。你可以將它分發給你的朋友們,根據是否由你發送還是由別人發送,消息的顯示是截然不同的。
這部分的代碼在這裡下載。
如果你有任何問題,請立即在下面的評論中留言,或者 tweet 我。