注意: 虽然 Proton 已经是一个功能强大且灵活的框架,但它仍处于开发的早期阶段。API 和公共接口仍在不断修订,并且在达到稳定版本 1.0.0 之前,每次版本更新都可能引入破坏性更改。
Proton 是一个简单的库,允许您扩展 textview 的行为,以添加您一直想要实现的丰富内容。它提供了简单的 API,允许您扩展 textView 以包含复杂内容,例如嵌套 textViews 或者任何其他 UIView。 简单来说 - 它就是您一直希望 UITextView 实现的功能。
Proton 的设计考虑了以下需求:
在其核心,Proton 由以下关键组件组成:
UITextView 的替代品,可以扩展以添加自定义视图,包括其他 EditorViews。TextProcessor 添加相应的行为,将标记文本转换为格式化的文本。EditorView)的容器。 Attachment 是一个功能强大的 NSTextAttachment,可以在其上应用自动约束,以各种配置(如匹配内容、宽度范围、固定宽度等)来调整其大小。 它还具有辅助函数来获取其在容器中的范围以及从容器中删除自身。EditorView 托管丰富内容的能力是通过使用 Attachment 实现的,Attachment 允许在 EditorView 中托管任何 UIView。 通过使用 TextProcessor 和 EditorCommand 来为编辑体验添加交互行为,可以进一步增强此功能。
让我们以一个 Panel 为例,看看如何在 EditorView 中创建它。 以下是 Panel 的主要要求:
Editor 之外还有自定义 UI。>> 字符插入到给定的编辑器中。backspace 键删除,类似于 Blockquote。首先需要创建一个表示 Panel 的视图。 创建此视图后,我们可以将其添加到附件并将其插入到 EditorView 中。
extension EditorContent.Name {
static let panel = EditorContent.Name("panel")
}
class PanelView: UIView, BlockContent, EditorContentView {
let container = UIView()
let editor: EditorView
let iconView = UIImageView()
var name: EditorContent.Name {
return .panel
}
override init(frame: CGRect) {
self.editor = EditorView(frame: frame)
super.init(frame: frame)
setup()
}
var textColor: UIColor {
get { editor.textColor }
set { editor.textColor = newValue }
}
override var backgroundColor: UIColor? {
get { container.backgroundColor }
set {
container.backgroundColor = newValue
editor.backgroundColor = newValue
}
}
private func setup() {
// setup view by creating required constraints
}
}
由于 Panel 内部包含一个 Editor,因此高度会根据在其中键入的内容自动更改。 要将高度限制为给定的最大值,可以使用绝对大小或自动布局约束。
使用 textColor 属性,可以更改默认字体颜色。
对于使用按钮将 Panel 添加到 Editor 的能力,我们可以使用 EditorCommand。 可以在给定的 EditorView 上或通过 CommandExecutor 执行 Command,CommandExecutor 会自动处理在焦点所在的 EditorView 上执行命令。 要将 EditorView 插入到另一个 EditorView 中,我们需要首先创建一个 Attachment,然后使用 Command 将其添加到所需的位置。
class PanelAttachment: Attachment {
var view: PanelView
init(frame: CGRect) {
view = PanelView(frame: frame)
super.init(view, size: .fullWidth)
view.delegate = self
view.boundsObserver = self
}
var attributedText: NSAttributedString {
get { view.attributedText }
set { view.attributedText = newValue }
}
}
class PanelCommand: EditorCommand {
func execute(on editor: EditorView) {
let selectedText = editor.selectedText
let attachment = PanelAttachment(frame: .zero)
attachment.selectBeforeDelete = true
editor.insertAttachment(in: editor.selectedRange, attachment: attachment)
let panel = attachment.view
panel.editor.maxHeight = 300
panel.editor.replaceCharacters(in: .zero, with: selectedText)
panel.editor.selectedRange = panel.editor.textEndRange
}
}
PanelCommand.execute 中的代码从 editor 读取 selectedText 并将其设置回 panel.editor。 这使得可以从主编辑器获取选定的文本,将其包装在面板中,然后将面板插入到主编辑器中以替换选定的文本。
要允许使用快捷方式文本输入而不是单击按钮来插入 Panel,您可以使用 TextProcessor。
class PanelTextProcessor: TextProcessing {
private let trigger = ">> "
var name: String {
return "PanelTextProcessor"
}
var priority: TextProcessingPriority {
return .medium
}
func process(editor: EditorView, range editedRange: NSRange, changeInLength delta: Int, processed: inout Bool) {
let line = editor.currentLine
guard line.text.string == trigger else {
return
}
let attachment = PanelAttachment(frame: .zero)
attachment.selectBeforeDelete = true
editor.insertAttachment(in: line.range, attachment: attachment)
}
对于像在空 Panel 上在索引 0 处点击退格键时删除 Panel 的要求,可以使用 EdtiorViewDelegate。
extension PanelAttachment: PanelViewDelegate {
func panel(_ panel: PanelView, didReceiveKey key: EditorKey, at range: NSRange, handled: inout Bool) {
if key == .backspace, range == .zero, panel.editor.attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
removeFromContainer()
handled = true
}
}
}
在上面的代码中,PanelViewDelegate 充当 PanelView 中 Editor 的 EditorViewDelegate 的传递。
在 ExamplesApp 中查看完整代码。
使用自定义 TextProcessor 在键入时更改文本
使用自定义 TextProcessor 在键入时添加属性
嵌套编辑器
来自现有文本的面板
将属性传递给附件中包含的编辑器
使用 Editor 中的自定义命令突出显示
查找文本并在编辑器中滚动
Proton 的 Editor 可以像标准 UIKit 组件一样与 SwiftUI 一起使用。 SwiftUI 支持按原样提供,并将在未来进行完善。
struct ProtonView: View {
@Binding var attributedText: NSAttributedString
@State var height: CGFloat = 0
var body: some View {
ProtonWrapperView(attributedText: $attributedText) { view in
let height = view.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize).height
self.height = height
}
.frame(height: height)
}
}
struct ProtonWrapperView: UIViewRepresentable {
@Binding var attributedText: NSAttributedString
let textDidChange: (EditorView) -> Void
func makeUIView(context: Context) -> EditorView {
let view = EditorView()
view.becomeFirstResponder()
view.attributedText = attributedText
view.isScrollEnabled = false
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.heightAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
DispatchQueue.main.async {
self.textDidChange(view)
}
EditorViewContext.shared.delegate = context.coordinator
return view
}
func updateUIView(_ view: EditorView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, EditorViewDelegate {
var parent: ProtonWrapperView
init(_ parent: ProtonWrapperView) {
self.parent = parent
}
func editor(_ editor: EditorView, didChangeTextAt range: NSRange) {
editor.isScrollEnabled = false
parent.attributedText = editor.attributedText
DispatchQueue.main.async {
self.parent.textDidChange(editor)
}
}
}
}
如果您有任何问题或功能请求,请随时在 github 中创建 issues。 虽然 Proton 是作为一个业余项目创建的,但我会尽力尽快回复您的问题。
Proton 在 Apache 2.0 许可证下发布。 有关详细信息,请参见 LICENSE。