swift 富文本编辑
by Neo Ighodaro
由新Ighodaro
如何使用Swift构建协作式文本编辑器 (How to build a collaborative text editor using Swift)
Text editors are increasingly popular these days, whether they’re embedded in a website comment form or used as a notepad. There are many different editors to choose from. In this post, we are not only going to learn how to build a beautiful text editor mobile app in iOS, but also how to make it possible to collaborate on a note in realtime using Pusher.
如今,无论文本编辑器是嵌入网站评论表单还是用作记事本,文本编辑器都越来越受欢迎。 有许多不同的编辑器可供选择。 在本文中,我们不仅将学习如何在iOS中构建漂亮的文本编辑器移动应用程序,还将学习如何使用Pusher实时协作处理笔记。
Please note, however, that to keep the application simple, the article will not cover concurrent edits. Therefore, only one person can edit at the same time while others watch.
但是请注意,为使应用程序简单,本文将不涉及并发编辑。 因此,只有一个人可以同时编辑,而其他人则可以观看。
The application will work by triggering an event when some text is entered. This event will be sent to Pusher and then picked up by the collaborator’s device and updated automatically.
输入某些文本后,该应用程序将通过触发事件来工作。 该事件将发送到Pusher,然后由协作者的设备接收并自动更新。
To follow along in this tutorial, you will need the following:
要继续学习本教程,您将需要以下内容:
Cocoapods: to install, run
gem install cocoapods
on your machineCocoapods:在计算机上安装,运行
gem install cocoapods
Xcode
Xcode
A Pusher application: you can create a free account and application here
Pusher应用程序:您可以在此处创建免费帐户和应用程序
Some knowledge of the Swift language
对Swift语言的一些了解
Node.js
Node.js
Lastly, a basic understanding of Swift and Node.js is needed to follow this tutorial.
最后,需要对Swift和Node.js有基本的了解才能遵循本教程。
Xcode我们的iOS应用程序入门 (Getting started with our iOS application in Xcode)
Launch Xcode and create a new project. I’ll call mine Collabo. After following the set up wizard, and with the workspace open, close Xcode and then cd
to the root of your project and run the command pod init
. This should generate a Podfile
for you. Change the contents of the Podfile
:
启动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
Now run the command pod install
so the Cocoapods package manager can pull in the necessary dependencies. When this is complete, close Xcode (if open) and then open the .xcworkspace
file that is in the root of your project folder.
现在运行命令pod install
以便Cocoapods软件包管理器可以提取必要的依赖项。 完成此操作后,关闭Xcode(如果已打开),然后打开项目文件夹根目录中的.xcworkspace
文件。
设计我们的iOS应用程序的视图 (Designing the views for our iOS application)
We are going to create some views for our iOS application. These will be the backbone where we will hook all the logic into. Using the Xcode story board, make your views look a little like the screenshots below.
我们将为我们的iOS应用程序创建一些视图。 这些将是我们将所有逻辑连接到的骨干。 使用Xcode故事板,使您的视图看起来像下面的屏幕截图。
This is the LaunchScreen.storyboard file. I’ve just designed something simple with no functionality at all.
这是LaunchScreen.storyboard文件。 我刚刚设计了一些简单的东西,根本没有任何功能。
The next storyboard we will design is the Main.storyboard. As the name implies, it will be the main one. This is where we have all the important views that are attached to some logic.
我们将设计的下一个故事板是Main.storyboard。 顾名思义,它将是主要的。 在这里,我们拥有所有附加在某些逻辑上的重要视图。
Here we have three views.
在这里,我们有三种看法。
The first view is designed to look exactly like the launch screen, with the exception of a button that we have linked to open up the second view.
第一个视图的设计看起来与启动屏幕完全一样,只是我们已链接以打开第二个视图的按钮除外。
The second view is the Navigation controller. It is attached to a third view which is a ViewController
. We have set the third view as the root controller to our Navigation Controller.
第二个视图是导航控制器。 它附加到第三个视图,即ViewController
。 我们将第三个视图设置为导航控制器的根控制器。
In the third view, we have a UITextView
that is editable which is placed in the view. There’s also a label that is supposed to be a character counter. This is the place where we will increment the characters as the user is typing text into the text view.
在第三个视图中,我们有一个可编辑的UITextView
,它位于视图中。 还有一个标签应该是字符计数器。 这是我们在用户在文本视图中键入文本时增加字符的地方。
编码iOS协作文本编辑器应用程序 (Coding the iOS collaborative text editor application)
Now that we have successfully created the views required for the application to load, the next thing we will do is start coding the logic for the application.
现在我们已经成功创建了应用程序加载所需的视图,接下来我们要做的是开始为应用程序逻辑编码。
Create a new cocoa class file and name it TextEditorViewController
and link it to the third view in the Main.storyboard
file. The TextViewController
should also adopt the UITextViewDelegate
. Now, you can ctrl+drag
the UITextView
and also ctrl+drag
the UILabel
in the Main.storyboard
file to the TextEditorViewController
class.
创建一个新的可可类文件,并将其命名为TextEditorViewController
并将其链接到Main.storyboard
文件中的第三个视图。 TextViewController
还应该采用UITextViewDelegate
。 现在,您可以ctrl+drag
UITextView
,也可以ctrl+drag
Main.storyboard
文件中的UILabel
到TextEditorViewController
类。
Also, you should import the PusherSwift
and AlamoFire
libraries to the TextViewController
. You should have something close to this after you are done:
另外,您应该将PusherSwift
和AlamoFire
库导入TextViewController
。 完成后,您应该对此有一些了解:
import UIKit import PusherSwift import Alamofire
class TextEditorViewController: UIViewController, UITextViewDelegate { @IBOutlet weak var textView: UITextView! @IBOutlet weak var charactersLabel: UILabel! }
Now we need to add some properties that we will be needing sometime later in the controller.
现在,我们需要添加一些稍后将需要在控制器中使用的属性。
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 = "" }
Now we will break up the logic into three parts:
现在,我们将逻辑分为三个部分:
- View and Keyboard events 查看和键盘事件
- UITextViewDelegate methods UITextViewDelegate方法
- Handling Pusher events. 处理Pusher事件。
查看和键盘事件 (View and Keyboard events)
Open the TextEditorViewController
and update it with the methods below:
打开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 } } }
In the viewDidLoad
method, we registered the keyboard functions so they will respond to keyboard events. We also added gesture recognizers that will dismiss the keyboard when you tap outside the UITextView
. And we set the textView
delegate to the controller itself. Finally, we called a function to listen for new updates (we will create this later).
在viewDidLoad
方法中,我们注册了键盘功能,因此它们将响应键盘事件。 我们还添加了手势识别器,当您在UITextView
外部点击时,这些手势识别器将关闭键盘。 然后我们将textView
委托设置为控制器本身。 最后,我们调用了一个函数来侦听新的更新(我们将在以后创建)。
In the viewWillAppear
method, we simply hacked the UITextView
into having a placeholder text, because, by default, the UITextView
does not have that feature. Wonder why, Apple…
在viewWillAppear
方法中,我们只是将UITextView
为具有占位符文本,因为默认情况下, UITextView
不具有该功能。 不知道为什么,苹果…
In the keyboardWillShow
and keyboardWillHide
functions, we made the character count label rise up with the keyboard and descend with it, respectively. This will prevent the Keyboard from covering the label when it is active.
在keyboardWillShow
和keyboardWillHide
函数中,我们使字符计数标签分别随键盘上升和下降。 这样可以防止键盘在激活时覆盖标签。
UITextViewDelegate方法 (UITextViewDelegate methods)
Update the TextEditorViewController
with the following:
使用以下命令更新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() }
The textViewDidChange
method simply updates the character count label and also sends the changes to Pusher using our backend API (which we will create in a minute).
textViewDidChange
方法仅更新字符计数标签,并使用我们的后端API(将在几分钟内创建)将更改发送到Pusher。
The textViewShouldBeginEditing
is gotten from the UITextViewDelegate
and it is triggered when the text view is about to be edited. In here, we basically play around with the placeholder, same as the textViewDidEndEditing
method.
textViewShouldBeginEditing
是从UITextViewDelegate
获得的,当要编辑文本视图时将触发它。 在这里,我们基本上使用占位符,与textViewDidEndEditing
方法相同。
Finally, in the tappedAwayFunction
we define the event callback for the gesture we registered in the previous section. In the method, we basically dismiss the keyboard.
最后,在tappedAwayFunction
中,为上一节中注册的手势定义事件回调。 在该方法中,我们基本上关闭了键盘。
处理推杆事件 (Handling Pusher events)
Update the controller with the following methods:
使用以下方法更新控制器:
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() }
In the sendToPusher
method, we send the payload to our backend application using AlamoFire
, which will, in turn, send it to Pusher.
在sendToPusher
方法中,我们使用AlamoFire
将有效负载发送到后端应用程序,然后将其发送到Pusher。
In the listenForChanges
method, we then listen for changes to the text and, if there are any, we apply the changes to the text view.
然后,在listenForChanges
方法中,我们侦听文本更改,如果有更改,则将更改应用于文本视图。
? Remember to replace the key and cluster with the actual value you have gotten from your Pusher dashboard.
? 请记住用您从Pusher仪表板获得的实际值替换密钥和群集。
If you have followed the tutorial closely, then your TextEditorViewController
should look something like this:
如果您已经仔细阅读了本教程,那么您的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() } }
Great! Now we need to make the backend of the application.
大! 现在我们需要制作应用程序的后端。
构建后端节点应用程序 (Building the backend Node application)
Now that we are done with the Swift part, we can focus on creating the Node.js backend for the application. We are going to be using Express so that we can quickly get something running.
现在我们完成了Swift部分,我们可以集中精力为应用程序创建Node.js后端。 我们将使用Express,以便我们可以快速运行某些东西。
Create a directory for the web application and then create some new files.
为Web应用程序创建目录,然后创建一些新文件。
The index.js file:
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!'); });
In the JS file above, we are using Express to create a simple application. In the /update_text
route, we simply receive the payload and pass it on to Pusher. Nothing complicated there.
在上面的JS文件中,我们使用Express创建一个简单的应用程序。 在/update_text
路由中,我们仅接收有效负载并将其传递给Pusher。 那里没什么复杂的。
Create a package.json file also:
还创建一个package.json文件:
{ "main": "index.js", "dependencies": { "body-parser": "^1.17.2", "express": "^4.15.3", "path": "^0.12.7", "pusher": "^1.5.1" } }
The package.json file is where we define all the NPM dependencies.
在package.json文件中,我们定义了所有NPM依赖项。
The last file to create is a config.js file. This is where we will define the configuration values for our Pusher application:
最后创建的文件是config.js文件。 在此处,我们将为Pusher应用程序定义配置值:
module.exports = { appId: 'PUSHER_ID', key: 'PUSHER_KEY', secret: 'PUSHER_SECRET', cluster: 'PUSHER_CLUSTER', encrypted: true };
? Remember to replace the key and cluster with the actual value you have gotten from your Pusher dashboard.
? 请记住用您从Pusher仪表板获得的实际值替换密钥和群集。
Now run npm install
on the directory and then node index.js
once the npm installation is complete. You should see an App listening on port 4000! message.
现在,在目录上运行npm install
,然后在npm安装完成后运行node index.js
。 您应该看到一个监听端口4000的应用程序! 信息。
测试应用程序 (Testing the application)
Once you have your local node web server running, you will need to make some changes so your application can talk to the local web server. In the info.plist
file, make the following changes:
一旦运行了本地节点Web服务器,就需要进行一些更改,以便您的应用程序可以与本地Web服务器通信。 在info.plist
文件中,进行以下更改:
With this change, you can build and run your application and it will talk directly with your local web application.
进行此更改后,您可以构建和运行您的应用程序,它将直接与本地Web应用程序通信。
结论 (Conclusion)
In this article, we have covered how to build a realtime collaborative text editor on iOS using Pusher. Hopefully, you have learned a thing or two from following the tutorial. For practice, you can expand the statuses to support more instances.
在本文中,我们介绍了如何使用Pusher在iOS上构建实时协作文本编辑器。 希望您从本教程中学到了一两件事。 作为练习,您可以扩展状态以支持更多实例。
This post was first published to Pusher.
该帖子最初发布给Pusher 。
翻译自: https://www.freecodecamp.org/news/how-to-build-a-collaborative-text-editor-using-swift-df7402c82510/
swift 富文本编辑