Skip to content

Commit

Permalink
Add URLRequest initializers
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzofiamingo committed Jan 24, 2022
1 parent db2ab0c commit 248bedf
Showing 1 changed file with 114 additions and 13 deletions.
127 changes: 114 additions & 13 deletions Sources/CachedAsyncImage/CachedAsyncImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public struct CachedAsyncImage<Content>: View where Content: View {

@State private var phase: AsyncImagePhase

private let url: URL?
private let urlRequest: URLRequest?

private let urlSession: URLSession

Expand All @@ -79,7 +79,7 @@ public struct CachedAsyncImage<Content>: View where Content: View {

public var body: some View {
content(phase)
.task(id: url, load)
.task(id: urlRequest, load)
}

/// Loads and displays an image from the specified URL.
Expand All @@ -105,7 +105,34 @@ public struct CachedAsyncImage<Content>: View where Content: View {
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
public init(url: URL?, urlCache: URLCache = .shared, scale: CGFloat = 1) where Content == Image {
self.init(url: url, urlCache: urlCache, scale: scale) { phase in
let urlRequest = url == nil ? nil : URLRequest(url: url!)
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale)
}

/// Loads and displays an image from the specified URL.
///
/// Until the image loads, SwiftUI displays a default placeholder. When
/// the load operation completes successfully, SwiftUI updates the
/// view to show the loaded image. If the operation fails, SwiftUI
/// continues to display the placeholder. The following example loads
/// and displays an icon from an example server:
///
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png"))
///
/// If you want to customize the placeholder or apply image-specific
/// modifiers --- like ``Image/resizable(capInsets:resizingMode:)`` ---
/// to the loaded image, use the ``init(url:scale:content:placeholder:)``
/// initializer instead.
///
/// - Parameters:
/// - urlRequest: The URL request of the image to display.
/// - urlCache: The URL cache for providing cached responses to requests within the session.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
public init(urlRequest: URLRequest?, urlCache: URLCache = .shared, scale: CGFloat = 1) where Content == Image {
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale) { phase in
#if os(macOS)
phase.image ?? Image(nsImage: .init())
#else
Expand Down Expand Up @@ -146,7 +173,43 @@ public struct CachedAsyncImage<Content>: View where Content: View {
/// - placeholder: A closure that returns the view to show until the
/// load operation completes successfully.
public init<I, P>(url: URL?, urlCache: URLCache = .shared, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
self.init(url: url, urlCache: urlCache, scale: scale) { phase in
let urlRequest = url == nil ? nil : URLRequest(url: url!)
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale, content: content, placeholder: placeholder)
}

/// Loads and displays a modifiable image from the specified URL using
/// a custom placeholder until the image loads.
///
/// Until the image loads, SwiftUI displays the placeholder view that
/// you specify. When the load operation completes successfully, SwiftUI
/// updates the view to show content that you specify, which you
/// create using the loaded image. For example, you can show a green
/// placeholder, followed by a tiled version of the loaded image:
///
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
/// image.resizable(resizingMode: .tile)
/// } placeholder: {
/// Color.green
/// }
///
/// If the load operation fails, SwiftUI continues to display the
/// placeholder. To be able to display a different view on a load error,
/// use the ``init(url:scale:transaction:content:)`` initializer instead.
///
/// - Parameters:
/// - urlRequest: The URL request of the image to display.
/// - urlCache: The URL cache for providing cached responses to requests within the session.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
/// - content: A closure that takes the loaded image as an input, and
/// returns the view to show. You can return the image directly, or
/// modify it as needed before returning it.
/// - placeholder: A closure that returns the view to show until the
/// load operation completes successfully.
public init<I, P>(urlRequest: URLRequest?, urlCache: URLCache = .shared, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale) { phase in
if let image = phase.image {
content(image)
} else {
Expand Down Expand Up @@ -191,17 +254,57 @@ public struct CachedAsyncImage<Content>: View where Content: View {
/// - content: A closure that takes the load phase as an input, and
/// returns the view to display for the specified phase.
public init(url: URL?, urlCache: URLCache = .shared, scale: CGFloat = 1, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
let urlRequest = url == nil ? nil : URLRequest(url: url!)
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale, transaction: transaction, content: content)
}

/// Loads and displays a modifiable image from the specified URL in phases.
///
/// If you set the asynchronous image's URL to `nil`, or after you set the
/// URL to a value but before the load operation completes, the phase is
/// ``AsyncImagePhase/empty``. After the operation completes, the phase
/// becomes either ``AsyncImagePhase/failure(_:)`` or
/// ``AsyncImagePhase/success(_:)``. In the first case, the phase's
/// ``AsyncImagePhase/error`` value indicates the reason for failure.
/// In the second case, the phase's ``AsyncImagePhase/image`` property
/// contains the loaded image. Use the phase to drive the output of the
/// `content` closure, which defines the view's appearance:
///
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
/// if let image = phase.image {
/// image // Displays the loaded image.
/// } else if phase.error != nil {
/// Color.red // Indicates an error.
/// } else {
/// Color.blue // Acts as a placeholder.
/// }
/// }
///
/// To add transitions when you change the URL, apply an identifier to the
/// ``CachedAsyncImage``.
///
/// - Parameters:
/// - urlRequest: The URL request of the image to display.
/// - urlCache: The URL cache for providing cached responses to requests within the session.
/// - scale: The scale to use for the image. The default is `1`. Set a
/// different value when loading images designed for higher resolution
/// displays. For example, set a value of `2` for an image that you
/// would name with the `@2x` suffix if stored in a file on disk.
/// - transaction: The transaction to use when the phase changes.
/// - content: A closure that takes the load phase as an input, and
/// returns the view to display for the specified phase.
public init(urlRequest: URLRequest?, urlCache: URLCache = .shared, scale: CGFloat = 1, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
let configuration = URLSessionConfiguration.default
configuration.urlCache = urlCache
configuration.requestCachePolicy = .returnCacheDataElseLoad
self.url = url
self.urlRequest = urlRequest
self.urlSession = URLSession(configuration: configuration)
self.scale = scale
self.transaction = transaction
self.content = content


if let image = cachedImage(from: url, cache: urlCache) {
if let image = cachedImage(from: urlRequest, cache: urlCache) {
self._phase = State(wrappedValue: .success(image))
} else {
self._phase = State(wrappedValue: .empty)
Expand All @@ -211,7 +314,7 @@ public struct CachedAsyncImage<Content>: View where Content: View {
@Sendable
private func load() async {
do {
if let image = try await remoteImage(from: url, session: urlSession) {
if let image = try await remoteImage(from: urlRequest, session: urlSession) {
withAnimation(transaction.animation) {
phase = .success(image)
}
Expand All @@ -238,17 +341,15 @@ private extension AsyncImage {
// MARK: - Helpers

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private func remoteImage(from url: URL?, session: URLSession) async throws -> Image? {
guard let url = url else { return nil }
let request = URLRequest(url: url)
private func remoteImage(from request: URLRequest?, session: URLSession) async throws -> Image? {
guard let request = request else { return nil }
let (data, _) = try await session.data(for: request)
return image(from: data)
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private func cachedImage(from url: URL?, cache: URLCache) -> Image? {
guard let url = url else { return nil }
let request = URLRequest(url: url)
private func cachedImage(from request: URLRequest?, cache: URLCache) -> Image? {
guard let request = request else { return nil }
guard let cachedResponse = cache.cachedResponse(for: request) else { return nil }
return image(from: cachedResponse.data)
}
Expand Down

0 comments on commit 248bedf

Please sign in to comment.