注意: 虽然 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。