В настоящее время многие приложения предлагают своим пользователям функции чата и обмена сообщениями внутри приложения. Мессенджер внутри приложения может быть полезен для таких вещей, как чат поддержки в реальном времени или обмен сообщениями внутри приложения с другими пользователями приложения.

В этой статье мы собираемся изучить, как использовать Pusher Chatkit (который находится в стадии бета-тестирования на момент написания этой статьи) и SlackTextViewController для создания приложения чата.

💡 SlackTextViewController - подкласс UIViewController с расширяющимся представлением ввода текста и другими полезными функциями обмена сообщениями. Он предназначен для замены UITableViewController и UICollectionViewController.

Чтобы следовать этому руководству, необходимо базовое понимание Swift и Node.js.

Вот запись экрана нашего приложения в действии:

Требования

Чтобы следовать руководству, вам потребуются перечисленные ниже требования:

  • Xcode 7 или выше.
  • Знание построителя интерфейсов Swift и Xcode.
  • Кокоаподы установлены на вашу машину.
  • Node.js и NPM установлены на вашем компьютере.
  • Базовые знания JavaScript (Node.js и Express).
  • Приложение Pusher Chatkit. Создайте его здесь.

Предполагая, что у вас есть все требования, приступим.

Создание нашего приложения Chatkit

Перейдите на страницу Chatkit, создайте учетную запись и создайте приложение Chatkit из панели управления.

Следуйте инструкциям мастера «Приступая к работе» до конца, чтобы он помог вам создать новую учетную запись пользователя и новую комнату чата.

На этом же экране после завершения работы мастера «Приступить к работе» нажмите «Ключи», чтобы получить указатель экземпляра и ключ вашего приложения. Эти значения понадобятся вам для выполнения запросов к Chatkit API.

Это все! Теперь давайте создадим бэкэнд, который поможет нашему приложению взаимодействовать с Chatkit API.

Создание серверной части Node.js для Pusher Chatkit

Прежде чем мы создадим наше приложение для iOS, давайте создадим для него серверную часть Node.js. Приложение будет взаимодействовать с серверной частью, чтобы делать такие вещи, как получение токена, необходимого для выполнения запросов.

Откройте свой терминал и создайте там новый каталог, в котором будет размещаться веб-приложение. В этом веб-приложении мы определим некоторые маршруты, которые будут содержать логику для выполнения запросов к Chatkit API.

Выполните команду ниже, чтобы создать каталог, в котором будет находиться наше веб-приложение:

$ mkdir ChattrBackend

Создайте новый package.json файл в корне каталога и вставьте его содержимое ниже:

{
  "main": "index.js",
  "dependencies": {
    "body-parser": "^1.17.2",
    "express": "^4.15.3",
    "pusher-chatkit-server": "^0.5.0"
  }
}

Теперь откройте терминал и выполните команду ниже, чтобы начать установку зависимостей:

$ npm install

Когда установка будет завершена, создайте новый index.js файл и вставьте содержимое ниже:

// Pull in the libraries
const express    = require('express');
const bodyParser = require('body-parser');
const app        = express();
const Chatkit    = require('pusher-chatkit-server');
const chatkit    = new Chatkit.default(require('./config.js'))
// Express Middlewares
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// --------------------------------------------------------
// Creates a new user using the Chatkit API
// --------------------------------------------------------
app.post('/users', (req, res) => {
  let username = req.body.username;
  chatkit.createUser(username, username)
    .then(r => res.json({username}))
    .catch(e => res.json({error: e.error_description, type: e.error_type}))
})
// --------------------------------------------------------
// Generate a token and return it
// --------------------------------------------------------
app.post('/auth', (req, res) => {
  let resp = chatkit.authenticate({grant_type: req.body.grant_type}, req.query.user_id)
  res.json(resp)
});
// --------------------------------------------------------
// Index
// --------------------------------------------------------
app.get('/', (req, res) => {
  res.json("It works!");
});
// --------------------------------------------------------
// Handle 404 errors
// --------------------------------------------------------
app.use((req, res, next) => {
  let err = new Error('Not Found');
  err.status = 404;
  next(err);
});
// --------------------------------------------------------
// Serve application
// --------------------------------------------------------
app.listen(4000, function(){
  console.log('App listening on port 4000!')
});

В приведенном выше коде у нас есть образец Express-приложения. Приложение имеет два основных маршрута. Маршрут /users создает нового пользователя с помощью Chatkit API. Созданный пользователь может затем запросить токен, используя маршрут /auth. Токены используются для проверки личности пользователя, отправляющего запрос к Chatkit API.

Наконец, давайте создадим config.js файл в том же корневом каталоге. Здесь мы определим ключи Chatkit. Вставьте содержимое ниже в файл:

module.exports = {
  instanceLocator: "PUSHER_CHATKIT_INSTANCE_LOCATOR",
  key: "PUSHER_CHATKIT_KEY",
}

Не забудьте заменить *PUSHER_CHATKIT_*` INSTANCE_LOCATOR and PUSHER_CHATKIT_KEY `на фактические значения для вашего приложения Chatkit. Вы можете найти значения в разделе «Ключи» на панели инструментов Chatkit.

На этом мы закончили создание приложения Node.js. Выполните команду ниже, чтобы запустить приложение Node.js:

$ node index.js

💡 Вы можете оставить окно терминала открытым и запустить другое окно терминала, чтобы сервер Node.js оставался работающим.

Создание нашего приложения для iOS

Запустите Xcode и создайте проект «Приложение для одного просмотра».

Установка наших пакетов Cocoapods

Когда вы создали приложение, закройте Xcode и запустите новое окно терминала. cd в корень каталога вашего мобильного приложения. Выполните команду ниже, чтобы инициализировать Cocoapods в проекте:

$ pod init

Это создаст новый Podfile. В этом файле мы можем определить наши зависимости Cocoapods. Откройте файл и вставьте следующее:

platform :ios, '10.0'
target 'Chattr' do
  use_frameworks!
  pod 'PusherChatkit', '~> 0.4.0'
  pod 'Alamofire', '~> 4.5.1'
  pod 'SlackTextViewController', git: 'https://github.com/slackhq/SlackTextViewController.git', branch: 'master'
end

Теперь запустите pod install, чтобы установить зависимости.

⚠️ SlackTextViewController имеет ошибку в iOS 11, когда текстовое представление не реагирует на щелчки. Хотя это было исправлено в версии 1.9.6, эта версия была недоступна для Cocoapods на момент написания этой статьи, поэтому нам пришлось вытащить мастер из подфайла.

Когда установка будет завершена, откройте новый .xcworkspace файл, созданный Cocoapods, в корне вашего проекта. Это запустит Xcode.

Настройка нашего приложения для iOS

В Xcode откройте файл AppDelegate.swift и замените содержимое файла следующим кодом:

import UIKit
struct AppConstants {
    static let ENDPOINT    = "http://localhost:4000"
    static let INSTANCE_LOCATOR = "PUSHER_CHATKIT_INSTANCE_LOCATOR"
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        window?.backgroundColor = UIColor.white
        return true
    }
}

В структуре AppConstants мы определили ENDPOINT и INSTANCE_LOCATOR. ENDPOINT - это URL-адрес удаленного веб-сервера, на котором находится ваше приложение Node.js. INSTANCE_LOCATOR содержит указатель экземпляров, предоставленный для вашего приложения Chatkit на панели управления Pusher Chatkit.

Теперь давайте сосредоточимся на создании раскадровки и других частей.

Создание раскадровки и контроллеров нашего приложения

Откройте файл Main.storyboard, и в нем мы создадим интерфейс приложения. В нашей раскадровке будет четыре сцены. Это будет выглядеть примерно так, как на скриншоте ниже:

В первой сцене View Controller давайте создадим LoginViewController и свяжем его со сценой View Controller в раскадровке. Создайте новый контроллер представления и вставьте приведенный ниже код:

import UIKit
import Alamofire
class LoginViewController: UIViewController {
    var username: String!
    @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var textField: UITextField!
}
extension LoginViewController {
    // MARK: Initialize
    override func viewDidLoad() {
        super.viewDidLoad()
        self.loginButton.isEnabled = false
        self.loginButton.addTarget(self, action: #selector(loginButtonPressed), for: .touchUpInside)
        self.textField.addTarget(self, action: #selector(typingUsername), for: .editingChanged)
    }
    // MARK: Navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) -> Void {
        if segue.identifier == "loginSegue" {
            let ctrl = segue.destination as! UINavigationController
            let actualCtrl = ctrl.viewControllers.first as! RoomListTableViewController
            actualCtrl.username = self.username
        }
    }
    // MARK: Helpers
    @objc func typingUsername(_ sender: UITextField) {
        self.loginButton.isEnabled = sender.text!.characters.count >= 3
    }
    @objc func loginButtonPressed(_ sender: Any) {
        let payload: Parameters = ["username": self.textField.text!]
        self.loginButton.isEnabled = false
        Alamofire.request(AppConstants.ENDPOINT + "/users", method: .post, parameters: payload).validate().responseJSON { (response) in
            switch response.result {
            case .success(_):
                self.username = self.textField.text!
                self.performSegue(withIdentifier: "loginSegue", sender: self)
            case .failure(let error):
                print(error)
            }
        }
    }
}

В приведенном выше коде мы определили два @IBOutlet, которые мы подключим к сцене View Controller в раскадровке. В методе prepare мы готовимся к переходу к RoomListTableViewController, устанавливая свойство username в этом классе. В обработчике loginButtonPressed мы запускаем запрос к приложению Node.js, которое мы создали ранее, чтобы создать нового пользователя.

Откройте раскадровку и свяжите первую сцену с классом LoginViewController. Добавьте UIButton и UITextField в сцену контроллера представления. Теперь подключите UITextField к свойству textField как точку отсчета. Также подключите UIButton к свойству loginButton в качестве опорной точки.

Затем добавьте в раскадровку контроллер навигации. Создайте ручной переход между контроллером представления и контроллером навигации и установите идентификатор этого перехода на loginSegue.

Затем создайте новый контроллер с именем RoomListTableViewController. В файле Main.storyboard установите этот новый класс в качестве настраиваемого класса для TableViewController, присоединенного к контроллеру навигации. Теперь в классе RoomListTableViewController замените содержимое следующим кодом:

import UIKit
import PusherChatkit
class RoomListTableViewController: UITableViewController {
    var username: String!
    var selectedRoom: PCRoom?
    var currentUser: PCCurrentUser!
    var availableRooms = [PCRoom]()
    var activityIndicator = UIActivityIndicatorView()
}

// MARK: - Initialize -
extension RoomListTableViewController: PCChatManagerDelegate {
    override func viewDidLoad() -> Void {
        super.viewDidLoad()
        self.setNavigationItemTitle()
        self.initActivityIndicator()
        self.initPusherChatkit()
    }
    private func setNavigationItemTitle() -> Void {
        self.navigationItem.title = "Rooms"
    }
    private func initActivityIndicator() -> Void {
        self.activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
        self.activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
        self.activityIndicator.center = self.view.center
        self.view.addSubview(self.activityIndicator)
        self.activityIndicator.startAnimating()
    }
    private func initPusherChatkit() -> Void {
        self.initPusherChatManager { [weak self] (currentUser) in
            guard let strongSelf = self else { return }
            strongSelf.currentUser = currentUser
            strongSelf.activityIndicator.stopAnimating()
            strongSelf.tableView.reloadData()
        }
    }
    private func initPusherChatManager(completion: @escaping (_ success: PCCurrentUser) -> Void) -> Void {        
        let chatManager = ChatManager(
            instanceId: AppConstants.INSTANCE_LOCATOR,
            tokenProvider: PCTokenProvider(url: AppConstants.ENDPOINT + "/auth", userId: username)
        )
        chatManager.connect(delegate: self) { (user, error) in
            guard error == nil else { return }
            guard let user = user else { return }
            // Get a list of all rooms. Attempt to join the room.
            user.getAllRooms { rooms, error in
                guard error == nil else { return }
                self.availableRooms = rooms!
                rooms!.forEach { room in
                    user.joinRoom(room) { room, error in
                        guard error == nil else { return }
                    }
                }
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
            DispatchQueue.main.async {
                completion(user)
            }
        }
    }
}

// MARK: - UITableViewController Overrides -
extension RoomListTableViewController {
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.availableRooms.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let roomTitle = self.availableRooms[indexPath.row].name
        let cell = tableView.dequeueReusableCell(withIdentifier: "RoomCell", for: indexPath)
        cell.textLabel?.text = "\(roomTitle)"
        return cell
    }
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.selectedRoom = self.availableRooms[indexPath.row]
        self.performSegue(withIdentifier: "segueToRoomViewController", sender: self)
    }
}

// MARK: - Navigation -
extension RoomListTableViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) -> Void {
        if segue.identifier == "segueToRoomViewController" {
            let roomViewController = segue.destination as! RoomViewController
            roomViewController.room = self.selectedRoom
            roomViewController.currentUser = self.currentUser
        }
    }
}

Этот класс предназначен для отображения всех доступных чатов, к которым пользователи могут подключаться и общаться в чате. Посмотрим, что делают некоторые части класса.

Первое расширение содержит инициализаторы. В методе viewDidLoad мы настраиваем заголовок контроллера, индикатор активности и Pusher Chatkit.

В initPusherChatManager мы инициализируем tokenProvider, который извлекает токен из нашей конечной точки Node.js. Затем мы создаем chatManager с помощью локатора экземпляров нашего приложения Chatkit и tokenProvider, и подключаемся к Chatkit.

Во втором расширении мы переопределяем некоторые методы контроллера табличного представления. Мы делаем это, чтобы отображать имена каналов в строках. В последнем методе второго расширения в строке 100 мы вызываем метод performSegue(withIdentifier: "segueToRoomViewController", sender: self), который будет перемещать страницу к новому контроллеру представления.

Последнее расширение имеет метод prepare. Это подготавливает View Controller, к которому мы переходим, прежде чем мы туда доберемся. Теперь давайте создадим контроллер представления и переход, необходимый для доступа к нему.

Для нашей последней сцены раскадровки создайте класс RoomViewController. В файле Main.storyboard перетащите последний контроллер представления на доску.

Установите для настраиваемого класса нового контроллера представления значение RoomViewController. Кроме того, создайте ручной переход от контроллера табличного представления к нему и назовите переход segueToRoomViewController:

Откройте класс RoomViewController и замените его содержимое следующим:

import UIKit
import PusherChatkit
import SlackTextViewController

class RoomViewController: SLKTextViewController, PCRoomDelegate {
    var room: PCRoom!
    var messages = [Message]()
    var currentUser: PCCurrentUser!
    override var tableView: UITableView {
        get { return super.tableView! }
    }
}

// MARK: - Initialize -
extension RoomViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.subscribeToRoom()
        self.setNavigationItemTitle()
        self.configureSlackTableViewController()
    }
    private func subscribeToRoom() -> Void {
        self.currentUser.subscribeToRoom(room: self.room, roomDelegate: self)
    }
    private func setNavigationItemTitle() -> Void {
        self.navigationItem.title = self.room.name
    }
    private func configureSlackTableViewController() -> Void {
        self.bounces = true
        self.isInverted = true
        self.shakeToClearEnabled = true
        self.isKeyboardPanningEnabled = true
        self.textInputbar.maxCharCount = 256
        self.tableView.separatorStyle = .none
        self.textInputbar.counterStyle = .split
        self.textInputbar.counterPosition = .top
        self.textInputbar.autoHideRightButton = true
        self.textView.placeholder = "Enter Message...";
        self.shouldScrollToBottomAfterKeyboardShows = false
        self.textInputbar.editorTitle.textColor = UIColor.darkGray
        self.textInputbar.editorRightButton.tintColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1)
        self.tableView.register(MessageCell.classForCoder(), forCellReuseIdentifier: MessageCell.MessengerCellIdentifier)
        self.autoCompletionView.register(MessageCell.classForCoder(), forCellReuseIdentifier: MessageCell.AutoCompletionCellIdentifier)
    }
}

// MARK: - UITableViewController Overrides -
extension RoomViewController {
    override class func tableViewStyle(for decoder: NSCoder) -> UITableViewStyle {
        return .plain
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if tableView == self.tableView {
            return self.messages.count
        }
        return 0
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return self.messageCellForRowAtIndexPath(indexPath)
    }
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if tableView == self.tableView {
            let message = self.messages[(indexPath as NSIndexPath).row]
            if message.text.characters.count == 0 {
                return 0
            }
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineBreakMode = .byWordWrapping
            paragraphStyle.alignment = .left
            let pointSize = MessageCell.defaultFontSize()
            let attributes = [
                NSAttributedStringKey.font: UIFont.systemFont(ofSize: pointSize),
                NSAttributedStringKey.paragraphStyle: paragraphStyle
            ]
            var width = tableView.frame.width - MessageCell.kMessageTableViewCellAvatarHeight
            width -= 25.0
            let titleBounds = (message.username as NSString!).boundingRect(
                with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
                options: .usesLineFragmentOrigin,
                attributes: attributes,
                context: nil
            )
            let bodyBounds = (message.text as NSString!).boundingRect(
                with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
                options: .usesLineFragmentOrigin,
                attributes: attributes,
                context: nil
            )
            var height = titleBounds.height
            height += bodyBounds.height
            height += 40
            if height < MessageCell.kMessageTableViewCellMinimumHeight {
                height = MessageCell.kMessageTableViewCellMinimumHeight
            }
            return height
        }
        return MessageCell.kMessageTableViewCellMinimumHeight
    }
}

// MARK: - Overrides -
extension RoomViewController {
    override func keyForTextCaching() -> String? {
        return Bundle.main.bundleIdentifier
    }
    override func didPressRightButton(_ sender: Any!) {
        self.textView.refreshFirstResponder()
        self.sendMessage(textView.text)
        super.didPressRightButton(sender)
    }
}

// MARK: - Delegate Methods -
extension RoomViewController {
    public func newMessage(message: PCMessage) {
        let msg = self.PCMessageToMessage(message)
        let indexPath = IndexPath(row: 0, section: 0)
        let rowAnimation: UITableViewRowAnimation = self.isInverted ? .bottom : .top
        let scrollPosition: UITableViewScrollPosition = self.isInverted ? .bottom : .top
        DispatchQueue.main.async {
            self.tableView.beginUpdates()
            self.messages.insert(msg, at: 0)
            self.tableView.insertRows(at: [indexPath], with: rowAnimation)
            self.tableView.endUpdates()
            self.tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: true)
            self.tableView.reloadRows(at: [indexPath], with: .automatic)
            self.tableView.reloadData()
        }
    }
}

// MARK: - Helpers -
extension RoomViewController {
    private func PCMessageToMessage(_ message: PCMessage) -> Message {
        return Message(id: message.id, username: message.sender.displayName, text: message.text)
    }
    private func sendMessage(_ message: String) -> Void {
        guard let room = self.room else { return }
        self.currentUser?.addMessage(text: message, to: room, completionHandler: { (messsage, error) in
            guard error == nil else { return }
        })
    }
    private func messageCellForRowAtIndexPath(_ indexPath: IndexPath) -> MessageCell {
        let cell = self.tableView.dequeueReusableCell(withIdentifier: MessageCell.MessengerCellIdentifier) as! MessageCell
        let message = self.messages[(indexPath as NSIndexPath).row]
        cell.bodyLabel().text = message.text
        cell.titleLabel().text = message.username
        cell.usedForMessage = true
        cell.indexPath = indexPath
        cell.transform = self.tableView.transform
        return cell
    } 
}

Приведенный выше класс расширяет SlackTableViewController, что автоматически дает ему доступ к некоторым интересным функциям этого контроллера. В приведенном выше коде мы разбили класс на 5 расширений. Давайте возьмем каждое расширение и немного объясним, что в них происходит.

В первом расширении мы подписываемся на комнату, устанавливаем имя комнаты в качестве заголовка страницы и настраиваем SlackTableViewController. В методе configureSlackTableViewController мы просто настраиваем части нашего SlackTableViewController.

Во втором расширении мы переопределяем методы контроллера табличного представления. Мы также устанавливаем количество разделов, количество строк и устанавливаем Message, который будет отображаться в каждой строке. И, наконец, мы также вычисляем динамическую высоту каждой ячейки в зависимости от символов в сообщении.

В третьем расширении у нас есть функция didPressRightButton, которая вызывается каждый раз, когда пользователь нажимает кнопку «Отправить» для отправки сообщения.

В четвертом расширении у нас есть функции, доступные из PCRoomDelegate. В функции newMessage мы отправляем сообщение в Chatkit, а затем перезагружаем таблицу для отображения вновь добавленных данных.

В пятом и последнем расширении мы определяем функции, которые должны быть помощниками. Метод PCMessageToMessage преобразует PCMessage в нашу собственную Message структуру (мы определим это позже). Метод sendMessage отправляет сообщение в Chatkit API. Наконец, у нас есть метод messageCellForRowAtIndexPath. Этот метод просто прикрепляет сообщение к определенной строке с помощью indexPath.

Теперь создайте новый класс с именем MessageCell. Это будет класс View, в котором мы будем настраивать все, как будет выглядеть отдельная ячейка чата. В файле замените содержимое следующим:

import UIKit
import SlackTextViewController

struct Message {
    var id: Int!
    var username: String!
    var text: String!
}

class MessageCell: UITableViewCell {
    static let kMessageTableViewCellMinimumHeight: CGFloat = 50.0;
    static let kMessageTableViewCellAvatarHeight: CGFloat = 30.0;
    static let MessengerCellIdentifier: String = "MessengerCell";
    static let AutoCompletionCellIdentifier: String = "AutoCompletionCell";
    var _titleLabel: UILabel?
    var _bodyLabel: UILabel?
    var _thumbnailView: UIImageView?
    var indexPath: IndexPath?
    var usedForMessage: Bool?
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.selectionStyle = .none
        self.backgroundColor = UIColor.white
        configureSubviews()
    }
    func configureSubviews() {
        contentView.addSubview(thumbnailView())
        contentView.addSubview(titleLabel())
        contentView.addSubview(bodyLabel())
        let views: [String:Any] = [
            "thumbnailView": thumbnailView(),
            "titleLabel": titleLabel(),
            "bodyLabel": bodyLabel()
        ]
        let metrics = [
            "tumbSize": MessageCell.kMessageTableViewCellAvatarHeight,
            "padding": 15,
            "right": 10,
            "left": 5
        ]
        contentView.addConstraints(NSLayoutConstraint.constraints(
            withVisualFormat: "H:|-left-[thumbnailView(tumbSize)]-right-[titleLabel(>=0)]-right-|",
            options: [],
            metrics: metrics,
            views: views
        ))
        contentView.addConstraints(NSLayoutConstraint.constraints(
            withVisualFormat: "H:|-left-[thumbnailView(tumbSize)]-right-[bodyLabel(>=0)]-right-|",
            options: [],
            metrics: metrics,
            views: views
        ))
        contentView.addConstraints(NSLayoutConstraint.constraints(
            withVisualFormat: "V:|-right-[thumbnailView(tumbSize)]-(>=0)-|",
            options: [],
            metrics: metrics,
            views: views
        ))
        if (reuseIdentifier == MessageCell.MessengerCellIdentifier) {
            contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "V:|-right-[titleLabel(20)]-left-[bodyLabel(>=0@999)]-left-|",
                options: [],
                metrics: metrics,
                views: views
            ))
        }
        else {
            contentView.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "V:|[titleLabel]|",
                options: [],
                metrics: metrics,
                views: views
            ))
        }
    }
    // MARK: Getters
    override func prepareForReuse() {
        super.prepareForReuse()
        selectionStyle = .none
        let pointSize: CGFloat = MessageCell.defaultFontSize()
        titleLabel().font = UIFont.boldSystemFont(ofSize: pointSize)
        bodyLabel().font = UIFont.systemFont(ofSize: pointSize)
        titleLabel().text = ""
        bodyLabel().text = ""
    }
    func titleLabel() -> UILabel {
        if _titleLabel == nil {
            _titleLabel = UILabel()
            _titleLabel?.translatesAutoresizingMaskIntoConstraints = false
            _titleLabel?.backgroundColor = UIColor.clear
            _titleLabel?.isUserInteractionEnabled = false
            _titleLabel?.numberOfLines = 0
            _titleLabel?.textColor = UIColor.gray
            _titleLabel?.font = UIFont.boldSystemFont(ofSize: MessageCell.defaultFontSize())
        }
        return _titleLabel!
    }
    func bodyLabel() -> UILabel {
        if _bodyLabel == nil {
            _bodyLabel = UILabel()
            _bodyLabel?.translatesAutoresizingMaskIntoConstraints = false
            _bodyLabel?.backgroundColor = UIColor.clear
            _bodyLabel?.isUserInteractionEnabled = false
            _bodyLabel?.numberOfLines = 0
            _bodyLabel?.textColor = UIColor.darkGray
            _bodyLabel?.font = UIFont.systemFont(ofSize: MessageCell.defaultFontSize())
        }
        return _bodyLabel!
    }
    func thumbnailView() -> UIImageView {
        if _thumbnailView == nil {
            _thumbnailView = UIImageView()
            _thumbnailView?.translatesAutoresizingMaskIntoConstraints = false
            _thumbnailView?.isUserInteractionEnabled = false
            _thumbnailView?.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            _thumbnailView?.layer.cornerRadius = MessageCell.kMessageTableViewCellAvatarHeight / 2.0
            _thumbnailView?.layer.masksToBounds = true
        }
        return _thumbnailView!
    }
    class func defaultFontSize() -> CGFloat {
        var pointSize: CGFloat = 16.0
        let contentSizeCategory: String = String(describing: UIApplication.shared.preferredContentSizeCategory)
        pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory)
        return pointSize
    }
}

В приведенном выше коде мы создаем класс, расширяющий UITableViewCell. Этот класс будет использоваться SlackTextViewController как класс для каждой строки сообщения. Он был зарегистрирован в RoomsViewController при настройке SlackTextViewController.

Тестирование нашего приложения Pusher Chatkit

Чтобы ваше приложение iOS подключалось к локальному серверу Node.js, вам необходимо внести некоторые изменения. В файле info.plist добавьте ключи, как показано ниже:

С этим изменением вы можете создавать и запускать свое приложение, и оно будет напрямую взаимодействовать с вашим локальным веб-приложением. Теперь вы можете запустить свое приложение.

Заключение

В этом руководстве мы смогли создать простое приложение для чата, используя SlackTextViewController и мощь Pusher Chatkit SDK. Надеюсь, вы кое-что узнали о том, как интегрировать Pusher Chatkit в существующие технологии и как он может улучшить обмен сообщениями в вашем приложении.

Этот пост впервые был опубликован в Pusher.