В наши дни текстовые редакторы становятся все более популярными, независимо от того, встроены ли они в форму комментариев на веб-сайте или используются в качестве блокнота. Есть много разных редакторов на выбор. В этом посте мы не только узнаем, как создать красивое мобильное приложение для текстового редактора в iOS, но и как сделать возможным совместную работу над заметкой в ​​реальном времени с помощью Pusher.

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

Приложение будет работать, инициируя событие при вводе текста. Это событие будет отправлено в Pusher, а затем получено устройством соавтора и обновлено автоматически.

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

  1. Cocoapods: для установки запустите gem install cocoapods на своем компьютере.
  2. Xcode
  3. Приложение Pusher: вы можете создать бесплатную учетную запись и приложение здесь
  4. Некоторое знание языка Swift
  5. Node.js

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

Начало работы с нашим приложением iOS в Xcode

Запустите Xcode и создайте новый проект. Я назову свой Collabo. Следуя указаниям мастера настройки и открыв рабочее пространство, закройте Xcode, а затем cd в корень вашего проекта и выполните команду pod init. Это должно сгенерировать для вас Podfile. Измените содержимое Podfile:

# Uncomment the next line to define a global platform for your project
    platform :ios, '9.0'
    target 'textcollabo' do
      # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
      use_frameworks!
      # Pods for anonchat
      pod 'Alamofire'
      pod 'PusherSwift'
    end

Теперь запустите команду pod install, чтобы менеджер пакетов Cocoapods смог получить необходимые зависимости. Когда это будет завершено, закройте Xcode (если он открыт), а затем откройте файл .xcworkspace, который находится в корне папки вашего проекта.

Разработка представлений для нашего приложения для iOS

Мы собираемся создать несколько представлений для нашего приложения для iOS. Это будет костяк, на который мы будем подключать всю логику. Используя раскадровку Xcode, сделайте ваши представления немного такими, как на скриншотах ниже.

Это файл LaunchScreen.storyboard. Я только что разработал что-то простое без каких-либо функций.

Следующая раскадровка, которую мы разработаем, - это Main.storyboard. Как видно из названия, он будет основным. Здесь у нас есть все важные представления, связанные с некоторой логикой.

Здесь у нас есть три взгляда.

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

Второй вид - это контроллер навигации. Он прикреплен к третьему виду - ViewController. Мы установили третье представление в качестве корневого контроллера для нашего контроллера навигации.

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

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

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

Создайте новый файл класса какао, назовите его TextEditorViewController и свяжите его с третьим представлением в Main.storyboard файле. TextViewController также должен принять UITextViewDelegate. Теперь вы можете ctrl+drag UITextView, а также ctrl+drag UILabel в файле Main.storyboard к классу TextEditorViewController.

Кроме того, вам следует импортировать библиотеки PusherSwift и AlamoFire в TextViewController. После того, как вы закончите, у вас должно быть что-то похожее на это:

import UIKit
    import PusherSwift
    import Alamofire
    class TextEditorViewController: UIViewController, UITextViewDelegate {
        @IBOutlet weak var textView: UITextView!
        @IBOutlet weak var charactersLabel: UILabel!
    }

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

import UIKit
    import PusherSwift
    import Alamofire
    class TextEditorViewController: UIViewController, UITextViewDelegate {
        static let API_ENDPOINT = "http://localhost:4000";
        @IBOutlet weak var textView: UITextView!
        @IBOutlet weak var charactersLabel: UILabel!
        var pusher : Pusher!
        var chillPill = true
        var placeHolderText = "Start typing..."
        var randomUuid : String = ""
    }

Теперь мы разделим логику на три части:

  1. Просмотр и события клавиатуры
  2. UITextViewDelegate методы
  3. Обработка событий Pusher.

События просмотра и клавиатуры

Откройте TextEditorViewController и обновите его следующими способами:

override func viewDidLoad() {
        super.viewDidLoad()

        // Notification trigger
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

        // Gesture recognizer
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedAwayFunction(_:))))

        // Set the controller as the textView delegate
        textView.delegate = self

        // Set the device ID
        randomUuid = UIDevice.current.identifierForVendor!.uuidString

        // Listen for changes from Pusher
        listenForChanges()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if self.textView.text == "" {
            self.textView.text = placeHolderText
            self.textView.textColor = UIColor.lightGray
        }
    }

    func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            if self.charactersLabel.frame.origin.y == 1.0 {
                self.charactersLabel.frame.origin.y -= keyboardSize.height
            }
        }
    }

    func keyboardWillHide(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            if self.view.frame.origin.y != 1.0 {
                self.charactersLabel.frame.origin.y += keyboardSize.height
            }
        }
    }

В методе viewDidLoad мы зарегистрировали функции клавиатуры, чтобы они реагировали на события клавиатуры. Мы также добавили распознаватели жестов, которые отключают клавиатуру, когда вы нажимаете за пределами UITextView. И мы устанавливаем делегата textView на сам контроллер. Наконец, мы вызвали функцию для прослушивания новых обновлений (мы создадим ее позже).

В методе viewWillAppear мы просто взломали UITextView, чтобы он имел текст-заполнитель, потому что по умолчанию UITextView не имеет этой функции. Интересно, почему, Apple…

В функциях keyboardWillShow и keyboardWillHide мы заставили метку количества символов подниматься вверх вместе с клавиатурой и опускаться вместе с ней соответственно. Это предотвратит закрытие ярлыка клавиатурой, когда она активна.

Методы UITextViewDelegate

Обновите TextEditorViewController следующим:

func textViewDidChange(_ textView: UITextView) {
        charactersLabel.text = String(format: "%i Characters", textView.text.characters.count)
        if textView.text.characters.count >= 2 {
            sendToPusher(text: textView.text)
        }
    }
    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        self.textView.textColor = UIColor.black
        if self.textView.text == placeHolderText {
            self.textView.text = ""
        }
        return true
    }
    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text == "" {
            self.textView.text = placeHolderText
            self.textView.textColor = UIColor.lightGray
        }
    }
    func tappedAwayFunction(_ sender: UITapGestureRecognizer) {
        textView.resignFirstResponder()
    }

Метод textViewDidChange просто обновляет метку количества символов, а также отправляет изменения в Pusher, используя наш внутренний API (который мы создадим через минуту).

textViewShouldBeginEditing получается из UITextViewDelegate и запускается, когда текстовое представление собирается редактироваться. Здесь мы в основном играем с заполнителем, так же, как и метод textViewDidEndEditing.

Наконец, в tappedAwayFunction мы определяем обратный вызов события для жеста, который мы зарегистрировали в предыдущем разделе. В этом методе мы в основном убираем клавиатуру.

Обработка событий Pusher

Обновите контроллер следующими способами:

func sendToPusher(text: String) {
        let params: Parameters = ["text": text, "from": randomUuid]
        Alamofire.request(TextEditorViewController.API_ENDPOINT + "/update_text", method: .post, parameters: params).validate().responseJSON { response in
            switch response.result {
            case .success:
                print("Succeeded")
            case .failure(let error):
                print(error)
            }
        }
    }
    func listenForChanges() {
        pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(
            host: .cluster("PUSHER_CLUSTER")
        ))
        let channel = pusher.subscribe("collabo")
        let _ = channel.bind(eventName: "text_update", callback: { (data: Any?) -> Void in
            if let data = data as? [String: AnyObject] {
                let fromDeviceId = data["deviceId"] as! String
                if fromDeviceId != self.randomUuid {
                    let text = data["text"] as! String
                    self.textView.text = text
                    self.charactersLabel.text = String(format: "%i Characters", text.characters.count)
                }
            }
        })
        pusher.connect()
    }

В методе sendToPusher мы отправляем полезные данные в наше внутреннее приложение, используя AlamoFire, которое, в свою очередь, отправляет их в Pusher.

Затем в методе listenForChanges мы отслеживаем изменения текста и, если они есть, применяем изменения к текстовому представлению.

💡 Не забудьте заменить ключ и кластер на фактическое значение, полученное с панели управления Pusher.

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

import UIKit
    import PusherSwift
    import Alamofire
    class TextEditorViewController: UIViewController, UITextViewDelegate {
        static let API_ENDPOINT = "http://localhost:4000";
        @IBOutlet weak var textView: UITextView!
        @IBOutlet weak var charactersLabel: UILabel!
        var pusher : Pusher!
        var chillPill = true
        var placeHolderText = "Start typing..."
        var randomUuid : String = ""
        override func viewDidLoad() {
            super.viewDidLoad()
            // Notification trigger
            NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
            // Gesture recognizer
            view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedAwayFunction(_:))))
            // Set the controller as the textView delegate
            textView.delegate = self
            // Set the device ID
            randomUuid = UIDevice.current.identifierForVendor!.uuidString
            // Listen for changes from Pusher
            listenForChanges()
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            if self.textView.text == "" {
                self.textView.text = placeHolderText
                self.textView.textColor = UIColor.lightGray
            }
        }
        func keyboardWillShow(notification: NSNotification) {
            if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
                if self.charactersLabel.frame.origin.y == 1.0 {
                    self.charactersLabel.frame.origin.y -= keyboardSize.height
                }
            }
        }
        func keyboardWillHide(notification: NSNotification) {
            if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
                if self.view.frame.origin.y != 1.0 {
                    self.charactersLabel.frame.origin.y += keyboardSize.height
                }
            }
        }
        func textViewDidChange(_ textView: UITextView) {
            charactersLabel.text = String(format: "%i Characters", textView.text.characters.count)
            if textView.text.characters.count >= 2 {
                sendToPusher(text: textView.text)
            }
        }
        func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
            self.textView.textColor = UIColor.black
            if self.textView.text == placeHolderText {
                self.textView.text = ""
            }
            return true
        }
        func textViewDidEndEditing(_ textView: UITextView) {
            if textView.text == "" {
                self.textView.text = placeHolderText
                self.textView.textColor = UIColor.lightGray
            }
        }

        func tappedAwayFunction(_ sender: UITapGestureRecognizer) {
            textView.resignFirstResponder()
        }
        func sendToPusher(text: String) {
            let params: Parameters = ["text": text, "from": randomUuid]
            Alamofire.request(TextEditorViewController.API_ENDPOINT + "/update_text", method: .post, parameters: params).validate().responseJSON { response in
                switch response.result {
                case .success:
                    print("Succeeded")
                case .failure(let error):
                    print(error)
                }
            }
        }
        func listenForChanges() {
            pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(
                host: .cluster("PUSHER_CLUSTER")
            ))
            let channel = pusher.subscribe("collabo")
            let _ = channel.bind(eventName: "text_update", callback: { (data: Any?) -> Void in
                if let data = data as? [String: AnyObject] {
                    let fromDeviceId = data["deviceId"] as! String
                    if fromDeviceId != self.randomUuid {
                        let text = data["text"] as! String
                        self.textView.text = text
                        self.charactersLabel.text = String(format: "%i Characters", text.characters.count)
                    }
                }
            })
            pusher.connect()
        }
    }

Большой! Теперь нам нужно сделать бэкэнд приложения.

Создание приложения backend Node

Теперь, когда мы закончили с частью Swift, мы можем сосредоточиться на создании серверной части Node.js для приложения. Мы собираемся использовать Express, чтобы быстро запустить что-нибудь.

Создайте каталог для веб-приложения, а затем создайте несколько новых файлов.

Файл index.js:

let path = require('path');
    let Pusher = require('pusher');
    let express = require('express');
    let bodyParser = require('body-parser');
    let app = express();
    let pusher = new Pusher(require('./config.js'));
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
    app.post('/update_text', function(req, res){
      var payload = {text: req.body.text, deviceId: req.body.from}
      pusher.trigger('collabo', 'text_update', payload)
      res.json({success: 200})
    });
    app.use(function(req, res, next) {
        var err = new Error('Not Found');
        err.status = 404;
        next(err);
    });
    module.exports = app;
    app.listen(4000, function(){
      console.log('App listening on port 4000!');
    });

В приведенном выше файле JS мы используем Express для создания простого приложения. В маршруте /update_text мы просто получаем полезную нагрузку и передаем ее Pusher. Ничего сложного нет.

Также создайте файл package.json:

{
      "main": "index.js",
      "dependencies": {
        "body-parser": "^1.17.2",
        "express": "^4.15.3",
        "path": "^0.12.7",
        "pusher": "^1.5.1"
      }
    }

В файле package.json мы определяем все зависимости NPM.

Последний создаваемый файл - это файл config.js. Здесь мы определим значения конфигурации для нашего приложения Pusher:

module.exports = {
      appId: 'PUSHER_ID',
      key: 'PUSHER_KEY',
      secret: 'PUSHER_SECRET',
      cluster: 'PUSHER_CLUSTER',
      encrypted: true
    };

💡 Не забудьте заменить ключ и кластер на фактическое значение, полученное с панели управления Pusher.

Теперь запустите npm install в каталоге, а затем node index.js после завершения установки npm. Вы должны увидеть сообщение Приложение прослушивает порт 4000!.

Тестирование приложения

После того, как у вас будет запущен локальный веб-сервер узла, вам нужно будет внести некоторые изменения, чтобы ваше приложение могло взаимодействовать с локальным веб-сервером. В файле info.plist внесите следующие изменения:

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

Заключение

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

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