Refactor release notes to use MarkdownUI library

- Create MarkdownContentView to encapsulate MarkdownUI rendering
- Replace custom AttributedString markdown parsing with MarkdownUI
- Simplify ReleaseNotesView by removing manual markdown styling
- Improve markdown rendering with proper support for lists, links, and formatting
- Make markdown rendering easily replaceable by keeping it in a dedicated view
This commit is contained in:
Ilyas Hallak 2025-10-30 21:48:28 +01:00
parent db3cbf41d4
commit 85bad35788
2 changed files with 41 additions and 49 deletions

View File

@ -0,0 +1,35 @@
import SwiftUI
import MarkdownUI
/// A custom view that renders Markdown content using the MarkdownUI library.
/// This view encapsulates the Markdown rendering logic, making it easy to swap
/// the underlying Markdown library if needed in the future.
struct MarkdownContentView: View {
let content: String
var body: some View {
Markdown(content)
.textSelection(.enabled)
}
}
#Preview {
ScrollView {
MarkdownContentView(content: """
# Heading 1
This is a paragraph with **bold** and *italic* text.
## Heading 2
- List item 1
- List item 2
- List item 3
### Heading 3
Another paragraph with [a link](https://example.com).
""")
.padding()
}
}

View File

@ -1,56 +1,14 @@
import SwiftUI
extension AttributedString {
init(styledMarkdown markdownString: String) throws {
var output = try AttributedString(
markdown: markdownString,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
failurePolicy: .returnPartiallyParsedIfPossible
),
baseURL: nil
)
for (intentBlock, intentRange) in output.runs[AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self].reversed() {
guard let intentBlock = intentBlock else { continue }
for intent in intentBlock.components {
switch intent.kind {
case .header(level: let level):
switch level {
case 1:
output[intentRange].font = .system(.title).bold()
case 2:
output[intentRange].font = .system(.title2).bold()
case 3:
output[intentRange].font = .system(.title3).bold()
default:
break
}
default:
break
}
}
if intentRange.lowerBound != output.startIndex {
output.characters.insert(contentsOf: "\n\n", at: intentRange.lowerBound)
}
}
self = output
}
}
struct ReleaseNotesView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if let attributedString = loadReleaseNotes() {
Text(attributedString)
.textSelection(.enabled)
VStack(alignment: .leading, spacing: 0) {
if let markdownContent = loadReleaseNotes() {
MarkdownContentView(content: markdownContent)
.padding()
} else {
Text("Unable to load release notes")
@ -71,13 +29,12 @@ struct ReleaseNotesView: View {
}
}
private func loadReleaseNotes() -> AttributedString? {
private func loadReleaseNotes() -> String? {
guard let url = Bundle.main.url(forResource: "RELEASE_NOTES", withExtension: "md"),
let markdownContent = try? String(contentsOf: url),
let attributedString = try? AttributedString(styledMarkdown: markdownContent) else {
let markdownContent = try? String(contentsOf: url) else {
return nil
}
return attributedString
return markdownContent
}
}