[1]: Quick Start - GeekTree0101/VEditorKit GitHub Wiki

Example source code

Basic Example Step

STEP1: Make an Editor Rule



struct EditorRule: VEditorRule {
    
    enum XML: String, CaseIterable {
        
        case paragraph = "p"
        case bold = "b"
        case image = "img"
   }
    
    var defaultStyleXMLTag: String {
        return XML.paragraph.rawValue
    }
    
    var linkStyleXMLTag: String? {
        return nil
    }
    
    var allXML: [String] {
        return XML.allCases.map({ $0.rawValue })
    }
    
    func paragraphStyle(_ xmlTag: String, attributes: [String : String]) -> VEditorStyle? {
        guard let xml = XML.init(rawValue: xmlTag) else { return nil }
        
        switch xml {
        case .paragraph:
            return .init([.font(UIFont.systemFont(ofSize: 16)),
                          .minimumLineHeight(26.0),
                          .color(.black)])
        case .bold:
            return .init([.emphasis(.bold),
                          .font(UIFont.systemFont(ofSize: 16)),
                          .minimumLineHeight(26.0),
                          .color(.black)])
        default:
            return nil
        }
    }
    
    func build(_ xmlTag: String, attributes: [String : String]) -> VEditorMediaContent? {
        return nil
    }
    
    func parseAttributeToXML(_ xmlTag: String,
                             attributes: [NSAttributedString.Key : Any]) -> [String : String]? {
        return nil
    }
    
    func enableTypingXMLs(_ inActiveXML: String) -> [String]? {
        return nil
    }
    
    func disableTypingXMLs(_ activeXML: String) -> [String]? {
        return nil
    }
    
    func inactiveTypingXMLs(_ activeXML: String) -> [String]? {
        return nil
    }
    
    func activeTypingXMLs(_ inactiveXML: String) -> [String]? {
        return nil
    }
}

STEP2: Make a Media Content(VEditorMediaContent)

class VImageContent: VEditorMediaContent {
    
    var xmlTag: String
    
    var url: URL?
    var width: CGFloat
    var height: CGFloat
    
    var ratio: CGFloat {
        return height / width
    }
    
    required init(_ xmlTag: String, attributes: [String : String]) {
        self.xmlTag = xmlTag
        self.url = URL(string: attributes["src"] ?? "")
        self.width = CGFloat(Int(attributes["width"] ?? "") ?? 1)
        self.height = CGFloat(Int(attributes["height"] ?? "") ?? 1)
    }
    
    func parseAttributeToXML() -> [String : String] {
        return ["src": url?.absoluteString ?? "",
                "width": "\(Int(width))",
                "height": "\(Int(height))"]
    }
}

and define on build methods


    func build(_ xmlTag: String, attributes: [String : String]) -> VEditorMediaContent? {
        guard let xml = XML.init(rawValue: xmlTag) else { return nil }
        
        switch xml {
        case .image:
            return VImageContent(xmlTag, attributes: attributes)
        default:
            return nil
        }
    }

example image tag xml

<img src=\"https://imageurl.png\" width=\"500\" height=\"500\"/>

STEP3: Make an Editor ViewController with VEditorNode

class EditorNodeController: ASViewController<VEditorNode> {
    
    
    override init() {
        let rule = EditorRule()
        self.controlAreaNode = EditorControlAreaNode(rule: rule)
        super.init(node: .init(editorRule: rule, controlAreaNode: nil))
    }
  • STEP4: Make a TypingController
import Foundation
import AsyncDisplayKit
import VEditorKit

class EditorControlAreaNode: ASDisplayNode {
    
    struct Const {
        static let insets: UIEdgeInsets = .init(top: 10.0, left: 5.0, bottom: 5.0, right: 10.0)
        static let controlSize: CGSize = .init(width: 44.0, height: 44.0)
    }
    
    lazy var boldNode: VEditorTypingControlNode = {
        let node = VEditorTypingControlNode(EditorRule.XML.bold.rawValue,
                                            rule: rule)
        node.style.preferredSize = Const.controlSize
        node.setTitle("B", with: UIFont.systemFont(ofSize: 20.0), with: .black, for: .normal)
        node.setTitle("B", with: UIFont.systemFont(ofSize: 20.0), with: .lightGray, for: .disabled)
        node.setTitle("B", with: UIFont.systemFont(ofSize: 20.0, weight: .bold), with: .blue, for: .selected)
        return node
    }()
    
    lazy var seperateLineNode: ASDisplayNode = {
        let node = ASDisplayNode()
        node.backgroundColor = .lightGray
        return node
    }()
    
    lazy var scrollNode: ASScrollNode = {
        let node = ASScrollNode()
        node.automaticallyManagesContentSize = true
        node.automaticallyManagesSubnodes = true
        node.scrollableDirections = [.left, .right]
        return node
    }()
    
    lazy var photoLoadControlNode: ASButtonNode = {
        let node = ASButtonNode()
        node.setImage(#imageLiteral(resourceName: "image.png"), for: .normal)
        node.style.preferredSize = Const.controlSize
        return node
    }()
    
    lazy var dismissNode: ASButtonNode = {
        let node = ASButtonNode()
        node.setImage(#imageLiteral(resourceName: "keyboard.png"), for: .normal)
        node.style.preferredSize = Const.controlSize
        return node
    }()
    
    var typingControlNodes: [VEditorTypingControlNode] {
        return [boldNode]
    }
    
    let rule: EditorRule
    
    init(rule: EditorRule) {
        self.rule = rule
        super.init()
        self.automaticallyManagesSubnodes = true
        self.backgroundColor = .white
        self.scrollNode.layoutSpecBlock = { [weak self] (_, _) -> ASLayoutSpec in
            return self?.controlButtonsGroupLayoutSpec() ?? ASLayoutSpec()
        }
    }
    
    override func didLoad() {
        super.didLoad()
        self.scrollNode.view.showsVerticalScrollIndicator = false
        self.scrollNode.view.showsHorizontalScrollIndicator = false
    }
    
    override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        scrollNode.style.flexShrink = 1.0
        scrollNode.style.flexGrow = 0.0
        dismissNode.style.flexShrink = 1.0
        dismissNode.style.flexGrow = 0.0
        let stackLayout = ASStackLayoutSpec(direction: .horizontal,
                                            spacing: 5.0,
                                            justifyContent: .spaceBetween,
                                            alignItems: .stretch,
                                            children: [scrollNode, dismissNode])
        let controlAreaLayout = ASInsetLayoutSpec(insets: Const.insets, child: stackLayout)
        seperateLineNode.style.height = .init(unit: .points, value: 0.5)
        return ASStackLayoutSpec(direction: .vertical,
                                 spacing: 0.0,
                                 justifyContent: .start,
                                 alignItems: .stretch,
                                 children: [seperateLineNode,
                                            controlAreaLayout])
    }
    
    private func controlButtonsGroupLayoutSpec() -> ASLayoutSpec {
        return ASStackLayoutSpec(direction: .horizontal,
                                 spacing: 10.0,
                                 justifyContent: .start,
                                 alignItems: .start,
                                 children: [photoLoadControlNode] + typingControlNodes)
    }
}

and define on viewController


class EditorNodeController: ASViewController<VEditorNode> {
    
    let controlAreaNode: EditorControlAreaNode
    
    init(isEditMode: Bool = true, xmlString: String? = nil) {
        let rule = EditorRule()
        self.controlAreaNode = EditorControlAreaNode(rule: rule)
        super.init(node: .init(editorRule: rule, controlAreaNode: controlAreaNode))
    }

STEP5: Inherit VEditorNodeDelegate


class EditorNodeController: ASViewController<VEditorNode> {
    
    let controlAreaNode: EditorControlAreaNode
    
    init(isEditMode: Bool = true, xmlString: String? = nil) {
        let rule = EditorRule()
        self.controlAreaNode = EditorControlAreaNode(rule: rule)
        super.init(node: .init(editorRule: rule, controlAreaNode: controlAreaNode))
        self.node.delegate = self <---- @HERE
    }

extension EditorNodeController: VEditorNodeDelegate {
    
    func getRegisterTypingControls() -> [VEditorTypingControlNode]? {
        return controlAreaNode.typingControlNodes
    }
    
    func dismissKeyboardNode() -> ASControlNode? {
        return controlAreaNode.dismissNode
    }
    
    func placeholderCellNode(_ content: VEditorPlaceholderContent, indexPath: IndexPath) -> VEditorMediaPlaceholderNode? {
        return nil
    }
    
    func contentCellNode(_ content: VEditorContent, indexPath: IndexPath) -> ASCellNode? {
        switch content {
        case let text as NSAttributedString:
            return VEditorTextCellNode(isEdit: true,
                                       placeholderText: nil,
                                       attributedText: text,
                                       rule: self.node.editorRule,
                                       regexDelegate: self,
                                       automaticallyGenerateLinkPreview: true)
                .setContentInsets(.zero)
        case let imageNode as VImageContent:
            return VEditorImageNode(isEdit: true)
                .setContentInsets(.zero)
                .setTextInsertionHeight(16.0)
                .setURL(imageNode.url)
                .setMediaRatio(imageNode.ratio)
                .setPlaceholderColor(.lightGray)
                .setBackgroundColor(.lightGray)
        default:
            return nil
        }
    }
}