Skip to content

Commit 39792ad

Browse files
committed
Fix the default image provider.
1 parent bf1ec48 commit 39792ad

File tree

4 files changed

+115
-117
lines changed

4 files changed

+115
-117
lines changed

Sources/MarkdownUI/Extensions/DefaultImageProvider.swift

Lines changed: 3 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ public struct DefaultImageProvider: ImageProvider {
1010
self.urlSession = urlSession
1111
}
1212

13-
public func makeImage(url: URL?) -> some SwiftUI.View {
14-
View(url: url, urlSession: self.urlSession).id(url)
13+
public func makeImage(url: URL?) -> some View {
14+
DefaultImageView(url: url, urlSession: self.urlSession)
15+
.id(url)
1516
}
1617
}
1718

@@ -23,118 +24,3 @@ extension ImageProvider where Self == DefaultImageProvider {
2324
.init()
2425
}
2526
}
26-
27-
// MARK: - View
28-
29-
extension DefaultImageProvider {
30-
private struct View: SwiftUI.View {
31-
@StateObject private var viewModel = ViewModel()
32-
33-
let url: URL?
34-
let urlSession: URLSession
35-
36-
var body: some SwiftUI.View {
37-
self.content.task {
38-
await self.viewModel.onAppear(url: self.url, urlSession: self.urlSession)
39-
}
40-
}
41-
42-
@ViewBuilder private var content: some SwiftUI.View {
43-
switch self.viewModel.state {
44-
case .notRequested, .loading, .failure:
45-
Color.clear
46-
.frame(width: 0, height: 0)
47-
case .success(let image, let size):
48-
ResizeToFit(idealSize: size) {
49-
image.resizable()
50-
}
51-
}
52-
}
53-
}
54-
}
55-
56-
// MARK: - ViewModel
57-
58-
extension DefaultImageProvider {
59-
private final class ViewModel: ObservableObject {
60-
enum State: Equatable {
61-
case notRequested
62-
case loading
63-
case success(Image, CGSize)
64-
case failure
65-
}
66-
67-
@Published private(set) var state: State = .notRequested
68-
69-
private let cache = NSCache<NSURL, PlatformImage>()
70-
71-
@MainActor func onAppear(url: URL?, urlSession: URLSession) async {
72-
guard case .notRequested = state else {
73-
return
74-
}
75-
76-
guard let url = url else {
77-
self.state = .failure
78-
return
79-
}
80-
81-
self.state = .loading
82-
83-
do {
84-
let image = try await self.image(with: url, urlSession: urlSession)
85-
self.state = .success(.init(platformImage: image), image.size)
86-
} catch {
87-
self.state = .failure
88-
}
89-
}
90-
91-
private func image(with url: URL, urlSession: URLSession) async throws -> PlatformImage {
92-
if let image = self.cache.object(forKey: url as NSURL) {
93-
return image
94-
}
95-
96-
let (data, response) = try await urlSession.data(from: url)
97-
98-
guard let statusCode = (response as? HTTPURLResponse)?.statusCode,
99-
200..<300 ~= statusCode
100-
else {
101-
throw URLError(.badServerResponse)
102-
}
103-
104-
guard let image = PlatformImage.decode(from: data) else {
105-
throw URLError(.cannotDecodeContentData)
106-
}
107-
108-
self.cache.setObject(image, forKey: url as NSURL)
109-
110-
return image
111-
}
112-
}
113-
}
114-
115-
// MARK: - PlatformImage
116-
117-
extension PlatformImage {
118-
fileprivate static func decode(from data: Data) -> PlatformImage? {
119-
#if os(iOS) || os(tvOS) || os(watchOS)
120-
guard let image = UIImage(data: data) else {
121-
return nil
122-
}
123-
return image
124-
#elseif os(macOS)
125-
guard let bitmapImageRep = NSBitmapImageRep(data: data) else {
126-
return nil
127-
}
128-
129-
let image = NSImage(
130-
size: NSSize(
131-
width: bitmapImageRep.pixelsWide,
132-
height: bitmapImageRep.pixelsHigh
133-
)
134-
)
135-
136-
image.addRepresentation(bitmapImageRep)
137-
return image
138-
#endif
139-
}
140-
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import SwiftUI
2+
3+
final class DefaultImageLoader {
4+
static let shared = DefaultImageLoader()
5+
6+
private let cache = NSCache<NSURL, PlatformImage>()
7+
8+
private init() {}
9+
10+
func image(with url: URL, urlSession: URLSession) async throws -> PlatformImage {
11+
if let image = self.cache.object(forKey: url as NSURL) {
12+
return image
13+
}
14+
15+
let (data, response) = try await urlSession.data(from: url)
16+
17+
guard let statusCode = (response as? HTTPURLResponse)?.statusCode,
18+
200..<300 ~= statusCode
19+
else {
20+
throw URLError(.badServerResponse)
21+
}
22+
23+
guard let image = PlatformImage.decode(from: data) else {
24+
throw URLError(.cannotDecodeContentData)
25+
}
26+
27+
self.cache.setObject(image, forKey: url as NSURL)
28+
29+
return image
30+
}
31+
}
32+
33+
extension PlatformImage {
34+
fileprivate static func decode(from data: Data) -> PlatformImage? {
35+
#if os(iOS) || os(tvOS) || os(watchOS)
36+
guard let image = UIImage(data: data) else {
37+
return nil
38+
}
39+
return image
40+
#elseif os(macOS)
41+
guard let bitmapImageRep = NSBitmapImageRep(data: data) else {
42+
return nil
43+
}
44+
45+
let image = NSImage(
46+
size: NSSize(
47+
width: bitmapImageRep.pixelsWide,
48+
height: bitmapImageRep.pixelsHigh
49+
)
50+
)
51+
52+
image.addRepresentation(bitmapImageRep)
53+
return image
54+
#endif
55+
}
56+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import SwiftUI
2+
3+
struct DefaultImageView: View {
4+
@StateObject private var viewModel = DefaultImageViewModel()
5+
6+
let url: URL?
7+
let urlSession: URLSession
8+
9+
var body: some View {
10+
switch self.viewModel.state {
11+
case .notRequested, .loading, .failure:
12+
Color.clear
13+
.frame(width: 0, height: 0)
14+
.task {
15+
await self.viewModel.task(url: self.url, urlSession: self.urlSession)
16+
}
17+
case .success(let image, let size):
18+
ResizeToFit(idealSize: size) {
19+
image.resizable()
20+
}
21+
}
22+
}
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import SwiftUI
2+
3+
final class DefaultImageViewModel: ObservableObject {
4+
enum State: Equatable {
5+
case notRequested
6+
case loading
7+
case success(Image, CGSize)
8+
case failure
9+
}
10+
11+
@Published private(set) var state: State = .notRequested
12+
private let imageLoader: DefaultImageLoader = .shared
13+
14+
@MainActor func task(url: URL?, urlSession: URLSession) async {
15+
guard case .notRequested = state else {
16+
return
17+
}
18+
19+
guard let url = url else {
20+
self.state = .failure
21+
return
22+
}
23+
24+
self.state = .loading
25+
26+
do {
27+
let image = try await self.imageLoader.image(with: url, urlSession: urlSession)
28+
self.state = .success(.init(platformImage: image), image.size)
29+
} catch {
30+
self.state = .failure
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)