Skip to content

Commit 0de1bbe

Browse files
committed
Update view when document changes
1 parent 09aa886 commit 0de1bbe

File tree

6 files changed

+215
-165
lines changed

6 files changed

+215
-165
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ let package = Package(
4141
url: "https://github.com/gonzalezreal/NetworkImage",
4242
from: "2.1.0"
4343
),
44+
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.1.2"),
4445
.package(
4546
name: "SnapshotTesting",
4647
url: "https://github.com/pointfreeco/swift-snapshot-testing",
@@ -66,6 +67,7 @@ let package = Package(
6667
"CommonMark",
6768
"AttributedText",
6869
"NetworkImage",
70+
.product(name: "CombineSchedulers", package: "combine-schedulers"),
6971
]
7072
),
7173
.testTarget(

Sources/MarkdownUI/Shared/ImageDownloader+TextAttachments.swift

Lines changed: 0 additions & 43 deletions
This file was deleted.

Sources/MarkdownUI/Shared/Markdown+Environment.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#if canImport(SwiftUI) && !os(watchOS) && !targetEnvironment(macCatalyst)
1+
#if canImport(SwiftUI) && !os(watchOS)
22

33
import SwiftUI
44

@@ -11,7 +11,7 @@
1111

1212
/// Sets the markdown style in this view and its children.
1313
@available(macOS 11.0, *)
14-
func markdownStyle(_ markdownStyle: @autoclosure @escaping () -> MarkdownStyle) -> some View {
14+
func markdownStyle(_ markdownStyle: MarkdownStyle) -> some View {
1515
environment(\.markdownStyle, markdownStyle)
1616
}
1717
}
@@ -24,7 +24,7 @@
2424
}
2525

2626
@available(macOS 11.0, *)
27-
var markdownStyle: () -> MarkdownStyle {
27+
var markdownStyle: MarkdownStyle {
2828
get { self[MarkdownStyleKey.self] }
2929
set { self[MarkdownStyleKey.self] = newValue }
3030
}
@@ -37,9 +37,7 @@
3737

3838
@available(macOS 11.0, iOS 13.0, tvOS 13.0, *)
3939
private struct MarkdownStyleKey: EnvironmentKey {
40-
static let defaultValue: () -> MarkdownStyle = {
41-
MarkdownStyle(font: .system(.body))
42-
}
40+
static let defaultValue = MarkdownStyle(font: .system(.body))
4341
}
4442

4543
#endif

Sources/MarkdownUI/Shared/Markdown.swift

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@
4444
@Environment(\.multilineTextAlignment) private var multilineTextAlignment: TextAlignment
4545
@Environment(\.sizeCategory) private var sizeCategory: ContentSizeCategory
4646
@Environment(\.markdownBaseURL) private var markdownBaseURL: URL?
47-
@Environment(\.markdownStyle) private var markdownStyle: () -> MarkdownStyle
48-
49-
@StateObject private var store = MarkdownStore()
47+
@Environment(\.markdownStyle) private var markdownStyle: MarkdownStyle
5048

5149
private let document: Document
5250

@@ -57,21 +55,78 @@
5755
}
5856

5957
public var body: some View {
60-
AttributedText(store.attributedText)
61-
.onChange(of: sizeCategory) { _ in
62-
store.onStyleChange(markdownStyle())
63-
}
64-
.onAppear {
65-
store.onAppear(
66-
document: document,
67-
environment: MarkdownStore.Environment(
68-
layoutDirection: layoutDirection,
69-
multilineTextAlignment: multilineTextAlignment,
70-
baseURL: markdownBaseURL,
71-
style: markdownStyle()
72-
)
73-
)
74-
}
58+
PrimitiveMarkdown(
59+
document: document,
60+
baseURL: markdownBaseURL,
61+
writingDirection: NSWritingDirection(layoutDirection: layoutDirection),
62+
alignment: NSTextAlignment(
63+
layoutDirection: layoutDirection,
64+
multilineTextAlignment: multilineTextAlignment
65+
),
66+
style: markdownStyle,
67+
isSynchronous: false
68+
)
69+
}
70+
}
71+
72+
@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)
73+
private struct PrimitiveMarkdown: View {
74+
@ObservedObject private var renderer: MarkdownRenderer
75+
76+
init(
77+
document: Document,
78+
baseURL: URL?,
79+
writingDirection: NSWritingDirection,
80+
alignment: NSTextAlignment,
81+
style: MarkdownStyle,
82+
isSynchronous: Bool
83+
) {
84+
renderer = MarkdownRenderer(
85+
document: document,
86+
baseURL: baseURL,
87+
writingDirection: writingDirection,
88+
alignment: alignment,
89+
style: style,
90+
environment: isSynchronous ? .synchronous : .default
91+
)
92+
}
93+
94+
var body: some View {
95+
AttributedText(renderer.attributedString)
96+
}
97+
}
98+
99+
private extension NSWritingDirection {
100+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)
101+
init(layoutDirection: LayoutDirection) {
102+
switch layoutDirection {
103+
case .leftToRight:
104+
self = .leftToRight
105+
case .rightToLeft:
106+
self = .rightToLeft
107+
@unknown default:
108+
self = .natural
109+
}
110+
}
111+
}
112+
113+
private extension NSTextAlignment {
114+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)
115+
init(layoutDirection: LayoutDirection, multilineTextAlignment: TextAlignment) {
116+
switch (layoutDirection, multilineTextAlignment) {
117+
case (.leftToRight, .leading):
118+
self = .left
119+
case (.rightToLeft, .leading):
120+
self = .right
121+
case (_, .center):
122+
self = .center
123+
case (.leftToRight, .trailing):
124+
self = .right
125+
case (.rightToLeft, .trailing):
126+
self = .left
127+
default:
128+
self = .natural
129+
}
75130
}
76131
}
77132

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#if canImport(Combine) && !os(watchOS)
2+
3+
import Combine
4+
import CombineSchedulers
5+
import CommonMark
6+
import Foundation
7+
import NetworkImage
8+
9+
#if os(macOS)
10+
import AppKit
11+
#elseif canImport(UIKit)
12+
import UIKit
13+
#endif
14+
15+
@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)
16+
final class MarkdownRenderer: ObservableObject {
17+
struct Environment {
18+
static let `default` = Environment(
19+
textAttachments: textAttachments(for:baseURL:),
20+
scheduler: DispatchQueue.main.eraseToAnyScheduler()
21+
)
22+
23+
static let synchronous = Environment(
24+
textAttachments: syncTextAttachments(for:baseURL:),
25+
scheduler: DispatchQueue.immediateScheduler.eraseToAnyScheduler()
26+
)
27+
28+
let textAttachments: (Set<String>, URL?) -> AnyPublisher<[String: NSTextAttachment], Never>
29+
let scheduler: AnySchedulerOf<DispatchQueue>
30+
}
31+
32+
@Published private(set) var attributedString: NSAttributedString
33+
34+
init(
35+
document: Document,
36+
baseURL: URL?,
37+
writingDirection: NSWritingDirection,
38+
alignment: NSTextAlignment,
39+
style: MarkdownStyle,
40+
environment: Environment
41+
) {
42+
attributedString = NSAttributedString(
43+
document: document,
44+
writingDirection: writingDirection,
45+
alignment: alignment,
46+
style: style
47+
)
48+
49+
let urls = document.imageURLs
50+
51+
if !urls.isEmpty {
52+
environment.textAttachments(urls, baseURL)
53+
.map { attachments in
54+
NSAttributedString(
55+
document: document,
56+
attachments: attachments,
57+
writingDirection: writingDirection,
58+
alignment: alignment,
59+
style: style
60+
)
61+
}
62+
.receive(on: environment.scheduler)
63+
.assign(to: &$attributedString)
64+
}
65+
}
66+
}
67+
68+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)
69+
private func textAttachments(
70+
for urls: Set<String>,
71+
baseURL: URL?
72+
) -> AnyPublisher<[String: NSTextAttachment], Never> {
73+
let attachmentURLs = urls.compactMap {
74+
URL(string: $0, relativeTo: baseURL)
75+
}
76+
77+
guard !attachmentURLs.isEmpty else {
78+
return Just([:]).eraseToAnyPublisher()
79+
}
80+
81+
let textAttachmentPairs = attachmentURLs.map { url in
82+
ImageDownloader.shared.image(for: url).map { image -> (String, NSTextAttachment) in
83+
let attachment = ImageAttachment()
84+
attachment.image = image
85+
86+
return (url.relativeString, attachment)
87+
}
88+
}
89+
90+
return Publishers.MergeMany(textAttachmentPairs)
91+
.collect()
92+
.map { Dictionary($0, uniquingKeysWith: { _, last in last }) }
93+
.replaceError(with: [:])
94+
.eraseToAnyPublisher()
95+
}
96+
97+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)
98+
private func syncTextAttachments(
99+
for urls: Set<String>,
100+
baseURL: URL?
101+
) -> AnyPublisher<[String: NSTextAttachment], Never> {
102+
let attachmentURLs = urls.compactMap {
103+
URL(string: $0, relativeTo: baseURL)
104+
}
105+
106+
guard !attachmentURLs.isEmpty else {
107+
return Just([:]).eraseToAnyPublisher()
108+
}
109+
110+
var result: [String: NSTextAttachment] = [:]
111+
112+
for url in attachmentURLs {
113+
guard let data = try? Data(contentsOf: url),
114+
let image = image(from: data) else { continue }
115+
116+
let attachment = ImageAttachment()
117+
attachment.image = image
118+
119+
result[url.relativeString] = attachment
120+
}
121+
122+
return Just(result).eraseToAnyPublisher()
123+
}
124+
125+
#if os(macOS)
126+
private func image(from data: Data) -> NSImage? {
127+
NSImage(data: data)
128+
}
129+
130+
#elseif canImport(UIKit)
131+
private func image(from data: Data) -> UIImage? {
132+
UIImage(data: data, scale: UIScreen.main.scale)
133+
}
134+
#endif
135+
136+
#endif

0 commit comments

Comments
 (0)