From b9345d5d5e04ed3aee1f7d37dd39d71c70ca1d64 Mon Sep 17 00:00:00 2001 From: Guille Gonzalez Date: Sat, 9 Sep 2023 19:29:04 +0200 Subject: [PATCH] Adopt the NetworkImage package for image loading --- Package.resolved | 9 +++ Package.swift | 9 ++- .../Extensibility/AssetImageProvider.swift | 16 ++++++ .../Extensibility/DefaultImageProvider.swift | 21 ++++--- .../DefaultImageView/DefaultImageLoader.swift | 56 ------------------- .../DefaultImageView/DefaultImageView.swift | 23 -------- .../DefaultImageViewModel.swift | 33 ----------- .../DefaultInlineImageProvider.swift | 14 ++--- .../Extensibility/Image+PlatformImage.swift | 17 ------ Sources/MarkdownUI/Utility/Deprecations.swift | 16 ++++++ 10 files changed, 64 insertions(+), 150 deletions(-) delete mode 100644 Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageLoader.swift delete mode 100644 Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageView.swift delete mode 100644 Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageViewModel.swift delete mode 100644 Sources/MarkdownUI/Extensibility/Image+PlatformImage.swift diff --git a/Package.resolved b/Package.resolved index 23cdd0be..2d0cd944 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 53bc11dd..7a59d60e 100644 --- a/Package.swift +++ b/Package.swift @@ -17,13 +17,18 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.10.0") + .package( + url: "https://github.com/gonzalezreal/NetworkImage", from: "6.0.0"), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.10.0"), ], targets: [ .target(name: "cmark-gfm"), .target( name: "MarkdownUI", - dependencies: ["cmark-gfm"] + dependencies: [ + "cmark-gfm", + .product(name: "NetworkImage", package: "NetworkImage"), + ] ), .testTarget( name: "MarkdownUITests", diff --git a/Sources/MarkdownUI/Extensibility/AssetImageProvider.swift b/Sources/MarkdownUI/Extensibility/AssetImageProvider.swift index 8220b1fb..a88387c4 100644 --- a/Sources/MarkdownUI/Extensibility/AssetImageProvider.swift +++ b/Sources/MarkdownUI/Extensibility/AssetImageProvider.swift @@ -58,3 +58,19 @@ extension ImageProvider where Self == AssetImageProvider { .init() } } + +#if canImport(UIKit) + private typealias PlatformImage = UIImage +#elseif os(macOS) + private typealias PlatformImage = NSImage +#endif + +extension Image { + fileprivate init(platformImage: PlatformImage) { + #if canImport(UIKit) + self.init(uiImage: platformImage) + #elseif os(macOS) + self.init(nsImage: platformImage) + #endif + } +} diff --git a/Sources/MarkdownUI/Extensibility/DefaultImageProvider.swift b/Sources/MarkdownUI/Extensibility/DefaultImageProvider.swift index ffc3806d..cce0c086 100644 --- a/Sources/MarkdownUI/Extensibility/DefaultImageProvider.swift +++ b/Sources/MarkdownUI/Extensibility/DefaultImageProvider.swift @@ -1,17 +1,20 @@ +import NetworkImage import SwiftUI /// The default image provider, which loads images from the network. public struct DefaultImageProvider: ImageProvider { - private let urlSession: URLSession - - /// Creates a default image provider. - /// - Parameter urlSession: An `URLSession` instance to load images. - public init(urlSession: URLSession = .shared) { - self.urlSession = urlSession - } - public func makeImage(url: URL?) -> some View { - DefaultImageView(url: url, urlSession: self.urlSession) + NetworkImage(url: url) { state in + switch state { + case .empty, .failure: + Color.clear + .frame(width: 0, height: 0) + case .success(let image, let idealSize): + ResizeToFit(idealSize: idealSize) { + image.resizable() + } + } + } } } diff --git a/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageLoader.swift b/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageLoader.swift deleted file mode 100644 index 4d46a91d..00000000 --- a/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageLoader.swift +++ /dev/null @@ -1,56 +0,0 @@ -import SwiftUI - -final class DefaultImageLoader { - static let shared = DefaultImageLoader() - - private let cache = NSCache() - - private init() {} - - func image(with url: URL, urlSession: URLSession) async throws -> PlatformImage { - if let image = self.cache.object(forKey: url as NSURL) { - return image - } - - let (data, response) = try await urlSession.data(from: url) - - guard let statusCode = (response as? HTTPURLResponse)?.statusCode, - 200..<300 ~= statusCode - else { - throw URLError(.badServerResponse) - } - - guard let image = PlatformImage.decode(from: data) else { - throw URLError(.cannotDecodeContentData) - } - - self.cache.setObject(image, forKey: url as NSURL) - - return image - } -} - -extension PlatformImage { - fileprivate static func decode(from data: Data) -> PlatformImage? { - #if canImport(UIKit) - guard let image = UIImage(data: data) else { - return nil - } - return image - #elseif os(macOS) - guard let bitmapImageRep = NSBitmapImageRep(data: data) else { - return nil - } - - let image = NSImage( - size: NSSize( - width: bitmapImageRep.pixelsWide, - height: bitmapImageRep.pixelsHigh - ) - ) - - image.addRepresentation(bitmapImageRep) - return image - #endif - } -} diff --git a/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageView.swift b/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageView.swift deleted file mode 100644 index 118519f8..00000000 --- a/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageView.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -struct DefaultImageView: View { - @StateObject private var viewModel = DefaultImageViewModel() - - let url: URL? - let urlSession: URLSession - - var body: some View { - switch self.viewModel.state { - case .notRequested, .loading, .failure: - Color.clear - .frame(width: 0, height: 0) - .task(id: self.url) { - await self.viewModel.task(url: self.url, urlSession: self.urlSession) - } - case .success(let image, let size): - ResizeToFit(idealSize: size) { - image.resizable() - } - } - } -} diff --git a/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageViewModel.swift b/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageViewModel.swift deleted file mode 100644 index 472ad331..00000000 --- a/Sources/MarkdownUI/Extensibility/DefaultImageView/DefaultImageViewModel.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftUI - -final class DefaultImageViewModel: ObservableObject { - enum State: Equatable { - case notRequested - case loading - case success(Image, CGSize) - case failure - } - - @Published private(set) var state: State = .notRequested - private let imageLoader: DefaultImageLoader = .shared - - @MainActor func task(url: URL?, urlSession: URLSession) async { - guard case .notRequested = state else { - return - } - - guard let url = url else { - self.state = .failure - return - } - - self.state = .loading - - do { - let image = try await self.imageLoader.image(with: url, urlSession: urlSession) - self.state = .success(.init(platformImage: image), image.size) - } catch { - self.state = .failure - } - } -} diff --git a/Sources/MarkdownUI/Extensibility/DefaultInlineImageProvider.swift b/Sources/MarkdownUI/Extensibility/DefaultInlineImageProvider.swift index da15910c..a4a266d1 100644 --- a/Sources/MarkdownUI/Extensibility/DefaultInlineImageProvider.swift +++ b/Sources/MarkdownUI/Extensibility/DefaultInlineImageProvider.swift @@ -1,19 +1,13 @@ +import NetworkImage import SwiftUI /// The default inline image provider, which loads images from the network. public struct DefaultInlineImageProvider: InlineImageProvider { - private let urlSession: URLSession - - /// Creates a default inline image provider. - /// - Parameter urlSession: An `URLSession` instance to load images. - public init(urlSession: URLSession = .shared) { - self.urlSession = urlSession - } - public func image(with url: URL, label: String) async throws -> Image { try await Image( - platformImage: DefaultImageLoader.shared - .image(with: url, urlSession: self.urlSession) + DefaultNetworkImageLoader.shared.image(from: url), + scale: 1, + label: Text(label) ) } } diff --git a/Sources/MarkdownUI/Extensibility/Image+PlatformImage.swift b/Sources/MarkdownUI/Extensibility/Image+PlatformImage.swift deleted file mode 100644 index a6866c88..00000000 --- a/Sources/MarkdownUI/Extensibility/Image+PlatformImage.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI - -#if canImport(UIKit) - typealias PlatformImage = UIImage -#elseif os(macOS) - typealias PlatformImage = NSImage -#endif - -extension Image { - init(platformImage: PlatformImage) { - #if canImport(UIKit) - self.init(uiImage: platformImage) - #elseif os(macOS) - self.init(nsImage: platformImage) - #endif - } -} diff --git a/Sources/MarkdownUI/Utility/Deprecations.swift b/Sources/MarkdownUI/Utility/Deprecations.swift index a0229ca4..d05a8864 100644 --- a/Sources/MarkdownUI/Utility/Deprecations.swift +++ b/Sources/MarkdownUI/Utility/Deprecations.swift @@ -1,5 +1,21 @@ import SwiftUI +// MARK: - Deprecated after 2.1.0: + +extension DefaultImageProvider { + @available(*, deprecated, message: "Use the 'default' static property") + public init(urlSession: URLSession = .shared) { + self.init() + } +} + +extension DefaultInlineImageProvider { + @available(*, deprecated, message: "Use the 'default' static property") + public init(urlSession: URLSession = .shared) { + self.init() + } +} + // MARK: - Deprecated after 2.0.2: extension BlockStyle where Configuration == BlockConfiguration {