johnxnguyen / Down

Blazing fast Markdown / CommonMark rendering in Swift, built upon cmark.
Other
2.23k stars 318 forks source link

[question] Custom element with custom view #304

Open paulocoutinhox opened 1 month ago

paulocoutinhox commented 1 month ago

Hello,

I need a lib for Swift markdown parser/display where when interpreting a link where the href is "ubook-app://product/123", a custom view is placed inside that displays a loading, then loads the product by the link id (ex: 123), and then show the product cover, title and two buttons (read/listen) within the view.

Can you tell me how to do this with this component?

Thanks.

paulocoutinhox commented 1 month ago

I solve the problem, but i still need click on buttons and it is not clickable, can anyone help me?

import UIKit
import Down

class MainViewController: UIViewController {

    private var textView: UITextView!
    private let exampleMarkdown = """
    Aqui está um link para um produto: [produto](ubook-app://product/123).
    Aqui está um link para um outro produto: [produto](ubook-app://product/456) que também é um produto bom.
    """

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupTextView()
        parseMarkdown()
    }

    private func setupTextView() {
        textView = UITextView()
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.isEditable = false
        textView.backgroundColor = .white
        textView.textColor = .black
        textView.isScrollEnabled = true
        textView.textContainerInset = .zero
        textView.textContainer.lineFragmentPadding = 0
        textView.delegate = self
        textView.isUserInteractionEnabled = true
        textView.isSelectable = false
        view.addSubview(textView)

        NSLayoutConstraint.activate([
            textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }

    private func parseMarkdown() {
        let down = Down(markdownString: exampleMarkdown)
        if let attributedString = try? down.toAttributedString() {
            let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
            let fullRange = NSRange(location: 0, length: attributedString.length)

            mutableAttributedString.enumerateAttributes(in: fullRange, options: []) { attributes, range, _ in
                if let link = attributes[.link] as? URL, link.scheme == "ubook-app", link.host == "product" {
                    let productId = link.lastPathComponent
                    let attachment = NSTextAttachment()
                    let productView = createProductView(productId: productId)
                    attachment.image = imageFromView(view: productView)

                    let attachmentString = NSAttributedString(attachment: attachment)
                    mutableAttributedString.replaceCharacters(in: range, with: attachmentString)

                    self.textView.attributedText = mutableAttributedString

                    // Carregar os dados do produto de forma assíncrona
                    self.loadProductData(productId: productId) { product in
                        DispatchQueue.main.async {
                            self.updateProductView(productView: productView, product: product)
                            attachment.image = self.imageFromView(view: productView)
                            self.textView.attributedText = mutableAttributedString // Atualizar o texto após carregar o produto
                            self.textView.layoutIfNeeded() // Garantir que a UI seja atualizada
                        }
                    }
                }
            }
        }
    }

    private func createProductView(productId: String) -> UIView {
        let productView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 80))
        productView.backgroundColor = .red
        productView.isUserInteractionEnabled = true

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(productViewTapped(_:)))
        productView.addGestureRecognizer(tapGesture)

        let loadingView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 80))
        loadingView.backgroundColor = .green
        loadingView.translatesAutoresizingMaskIntoConstraints = false
        loadingView.tag = 1
        productView.addSubview(loadingView)

        let activityIndicator = UIActivityIndicatorView(style: .large)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        activityIndicator.startAnimating()
        loadingView.addSubview(activityIndicator)

        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: loadingView.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: loadingView.centerYAnchor),
            loadingView.topAnchor.constraint(equalTo: productView.topAnchor),
            loadingView.leadingAnchor.constraint(equalTo: productView.leadingAnchor),
            loadingView.trailingAnchor.constraint(equalTo: productView.trailingAnchor),
            loadingView.bottomAnchor.constraint(equalTo: productView.bottomAnchor)
        ])

        let productContentView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 80))
        productContentView.backgroundColor = .blue
        productContentView.translatesAutoresizingMaskIntoConstraints = false
        productContentView.isHidden = true
        productContentView.tag = 2
        productView.addSubview(productContentView)

        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.tag = 3
        productContentView.addSubview(imageView)

        let titleLabel = UILabel()
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.textColor = .white
        titleLabel.tag = 4
        productContentView.addSubview(titleLabel)

        let listenButton = UIButton(type: .system)
        listenButton.setTitle("OUVIR", for: .normal)
        listenButton.backgroundColor = .white
        listenButton.setTitleColor(.blue, for: .normal)
        listenButton.translatesAutoresizingMaskIntoConstraints = false
        listenButton.tag = 5
        listenButton.addTarget(self, action: #selector(listenButtonTapped(_:)), for: .touchUpInside)
        productContentView.addSubview(listenButton)

        let viewButton = UIButton(type: .system)
        viewButton.setTitle("VER", for: .normal)
        viewButton.backgroundColor = .white
        viewButton.setTitleColor(.blue, for: .normal)
        viewButton.translatesAutoresizingMaskIntoConstraints = false
        viewButton.tag = 6
        viewButton.addTarget(self, action: #selector(viewButtonTapped(_:)), for: .touchUpInside)
        productContentView.addSubview(viewButton)

        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: loadingView.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: loadingView.centerYAnchor),

            loadingView.topAnchor.constraint(equalTo: productView.topAnchor),
            loadingView.leadingAnchor.constraint(equalTo: productView.leadingAnchor),
            loadingView.trailingAnchor.constraint(equalTo: productView.trailingAnchor),
            loadingView.bottomAnchor.constraint(equalTo: productView.bottomAnchor),

            imageView.topAnchor.constraint(equalTo: productContentView.topAnchor, constant: 10),
            imageView.leadingAnchor.constraint(equalTo: productContentView.leadingAnchor, constant: 10),
            imageView.widthAnchor.constraint(equalToConstant: 50),
            imageView.heightAnchor.constraint(equalToConstant: 60),

            titleLabel.topAnchor.constraint(equalTo: productContentView.topAnchor, constant: 10),
            titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
            titleLabel.trailingAnchor.constraint(equalTo: productContentView.trailingAnchor, constant: -10),

            listenButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
            listenButton.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
            listenButton.widthAnchor.constraint(equalToConstant: 80),

            viewButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
            viewButton.leadingAnchor.constraint(equalTo: listenButton.trailingAnchor, constant: 10),
            viewButton.trailingAnchor.constraint(equalTo: productContentView.trailingAnchor, constant: -10),

            productContentView.topAnchor.constraint(equalTo: productView.topAnchor),
            productContentView.leadingAnchor.constraint(equalTo: productView.leadingAnchor),
            productContentView.trailingAnchor.constraint(equalTo: productView.trailingAnchor),
            productContentView.bottomAnchor.constraint(equalTo: productView.bottomAnchor)
        ])

        return productView
    }

    @objc private func listenButtonTapped(_ sender: UIButton) {
        if let productView = sender.superview?.superview as? UIView,
           let titleLabel = productView.viewWithTag(4) as? UILabel {
            print("OUVIR botão clicado para o produto: \(titleLabel.text ?? "")")
        }
    }

    @objc private func viewButtonTapped(_ sender: UIButton) {
        if let productView = sender.superview?.superview as? UIView,
           let titleLabel = productView.viewWithTag(4) as? UILabel {
            print("VER botão clicado para o produto: \(titleLabel.text ?? "")")
        }
    }

    @objc private func productViewTapped(_ gesture: UITapGestureRecognizer) {
        if let productView = gesture.view,
           let titleLabel = productView.viewWithTag(4) as? UILabel {
            print("Produto clicado: \(titleLabel.text ?? "")")
        }
    }

    private func updateProductView(productView: UIView, product: Product) {
        if let loadingView = productView.viewWithTag(1),
           let productContentView = productView.viewWithTag(2),
           let imageView = productContentView.viewWithTag(3) as? UIImageView,
           let titleLabel = productContentView.viewWithTag(4) as? UILabel {
            print("Atualizando a view do produto")
            loadingView.isHidden = true
            productContentView.isHidden = false
            imageView.image = product.image
            titleLabel.text = "\(product.title) (\(product.id))"
            productView.setNeedsLayout()
            productView.layoutIfNeeded()
        } else {
            print("Não foi possível encontrar as views para atualizar")
        }
    }

    private func imageFromView(view: UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0)
        view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }

    private func loadProductData(productId: String, completion: @escaping (Product) -> Void) {
        // Simula carregamento assíncrono de dados
        DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
            let product = Product(id: productId, title: "Produto \(productId)", image: UIImage(systemName: "book")!)
            DispatchQueue.main.async {
                completion(product)
            }
        }
    }
}

struct Product {
    let id: String
    let title: String
    let image: UIImage
}

extension MainViewController: UITextViewDelegate {
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        return false
    }
}