From 0073769bd09731c2e637b90e19fc6c71ced19fda Mon Sep 17 00:00:00 2001 From: Angelika Serwa Date: Mon, 29 Jun 2026 19:27:19 +0200 Subject: [PATCH] feat(ios): add EnrichedMarkdownText SwiftUI view Add a TextKit 2 UITextView host and EnrichedMarkdownText view that wires parsing, rendering, and theme config into a SwiftUI-native API. Co-authored-by: Cursor --- .../EnrichedMarkdown/EnrichedMarkdown.swift | 4 +- .../Views/EnrichedMarkdownText.swift | 19 +++++++ .../Views/MarkdownTextViewRepresentable.swift | 57 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/EnrichedMarkdownText.swift create mode 100644 packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/MarkdownTextViewRepresentable.swift diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/EnrichedMarkdown.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/EnrichedMarkdown.swift index ea721eea1..2c2a18a5a 100644 --- a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/EnrichedMarkdown.swift +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/EnrichedMarkdown.swift @@ -1 +1,3 @@ -public enum EnrichedMarkdown {} +public enum EnrichedMarkdown { + // Namespace for the EnrichedMarkdown library. +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/EnrichedMarkdownText.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/EnrichedMarkdownText.swift new file mode 100644 index 000000000..225c917b2 --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/EnrichedMarkdownText.swift @@ -0,0 +1,19 @@ +import SwiftUI +import UIKit + +public struct EnrichedMarkdownText: View { + private let markdown: String + + @Environment(\.markdownStyleConfig) private var styleConfig + + public init(_ markdown: String) { + self.markdown = markdown + } + + public var body: some View { + MarkdownTextViewRepresentable( + attributedText: MarkdownRenderer.render(markdown, config: styleConfig) + ) + .fixedSize(horizontal: false, vertical: true) + } +} diff --git a/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/MarkdownTextViewRepresentable.swift b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/MarkdownTextViewRepresentable.swift new file mode 100644 index 000000000..9ded9fbbe --- /dev/null +++ b/packages/ios-enriched-markdown/Sources/EnrichedMarkdown/Views/MarkdownTextViewRepresentable.swift @@ -0,0 +1,57 @@ +import SwiftUI +import UIKit + +struct MarkdownTextViewRepresentable: UIViewRepresentable { + let attributedText: NSAttributedString + + func makeUIView(context: Context) -> MarkdownTextView { + MarkdownTextView() + } + + func updateUIView(_ textView: MarkdownTextView, context: Context) { + textView.setMarkdownAttributedText(attributedText) + } + + @available(iOS 16.0, *) + func sizeThatFits(_ proposal: ProposedViewSize, uiView: MarkdownTextView, context: Context) -> CGSize? { + let width = proposal.width ?? UIScreen.main.bounds.width + let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return CGSize(width: width, height: size.height) + } +} + +final class MarkdownTextView: UITextView { + override var intrinsicContentSize: CGSize { + let width = bounds.width > 0 ? bounds.width : UIView.noIntrinsicMetric + guard width != UIView.noIntrinsicMetric else { + return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) + } + let size = sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } + + init() { + super.init(frame: .zero, textContainer: nil) + configure() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + isEditable = false + isScrollEnabled = false + backgroundColor = .clear + textContainerInset = .zero + textContainer.lineFragmentPadding = 0 + setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + + func setMarkdownAttributedText(_ attributedText: NSAttributedString) { + guard !(self.attributedText?.isEqual(to: attributedText) ?? false) else { return } + self.attributedText = attributedText + invalidateIntrinsicContentSize() + } +}