В наши дни текстовые редакторы становятся все более популярными, независимо от того, встроены ли они в форму комментариев на веб-сайте или используются в качестве блокнота. Есть много разных редакторов на выбор. В этом посте мы не только узнаем, как создать красивое мобильное приложение для текстового редактора в iOS, но и как сделать возможным совместную работу над заметкой в реальном времени с помощью Pusher.
Однако обратите внимание, что для упрощения приложения статья не будет охватывать одновременное редактирование. Таким образом, только один человек может редактировать одновременно, пока другие смотрят.
Приложение будет работать, инициируя событие при вводе текста. Это событие будет отправлено в Pusher, а затем получено устройством соавтора и обновлено автоматически.
Чтобы следовать этому руководству, вам понадобится следующее:
- Cocoapods: для установки запустите
gem install cocoapods
на своем компьютере. - Xcode
- Приложение Pusher: вы можете создать бесплатную учетную запись и приложение здесь
- Некоторое знание языка Swift
- 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 = "" }
Теперь мы разделим логику на три части:
- Просмотр и события клавиатуры
- UITextViewDelegate методы
- Обработка событий 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.