URLImage is a SwiftUI view that displays an image downloaded from provided URL. URLImage manages downloading remote image and caching it locally, both in memory and on disk, for you.
- Features
- Installation
- Usage
- Advanced Customization
- Progress View
- Incremental Image Loading
- Local Cache
- Image Processing, Filters, and Resizing
- Examples
- URLImage
- Misc
- SwiftUI image view for remote images;
- Asynchronous image loading in the background with cancellation when view disappears;
- Local disk cache for downloaded images;
- Download progress indication;
- Incremental downloading with interlaced images support (interlaced PNG, interlaced GIF, and progressive JPEG);
- Fully customizable including placeholder, progress indication, and the image view;
- Image processing and Core Image filters;
- Control over download delay for better scroll performance in lists;
- Lower memory consumption when downloading image data directly to disk.
URLImage can be installed using Swift Package Manager or CocoaPods.
To install URLImage using Swift Package Manager look for https://github.com/dmytro-anokhin/url-image.git in Xcode (File/Swift Packages/Add Package Dependency...). See Adding Package Dependencies to Your App for details.
To install URLImage using CocoaPods add pod 'URLImage' to your Podfile.
URLImage can be initialized with URL:
URLImage(url)URLImage can also be initialized with URLRequest if you need to set additional HTTP headers:
URLImage(urlRequest)Note: the package expects GET request with URL
When using in lists delay can be provided to postpone loading and improve scrolling performance:
URLImage(url, delay: 0.25)The placeholder image can be changed:
URLImage(url, placeholder: Image(systemName: "circle"))Note: Image(systemName:) API is not available on macOS
0.7.0 release introduces some breaking changes:
- In 0.6.3 image was internal view. In 0.7.0 image is created by
contentclosure with a proxy object. This provides more flexibility in customization. - Placeholder closure is now also accepts an object that can be used to track download progress.
- Styling functions are now gone. Use
contentclosure to style the image. - Configuration object is now gone.
URLImage utilizes closures for customization. Downloaded image can be customized using (ImageProxy) -> Content closure. The closure parameter is a proxy that provides access to Image and UIImage or NSImage for iOS and macOS.
URLImage(url) { proxy in
proxy.image
.resizable() // Make image resizable
.aspectRatio(contentMode: .fill) // Fill the frame
.clipped() // Clip overlaping parts
}
.frame(width: 100.0, height: 100.0) // Set frame to 100x100.The placeholder can be customized with (DownloadProgressWrapper) -> Placeholder closure.
URLImage(url, placeholder: { _ in
Image(systemName: "circle") // Use different image for the placeholder
.resizable() // Make it resizable
.frame(width: 150.0, height: 150.0) // Set frame to 150x150
})URLImage(url, placeholder: { _ in
// Replace placeholder image with text
Text("Loading...")
})User ProgressView as a placeholder to display download progress.
Downloading image is a two step process:
- When
progressis 0 the download has not started yet. The best is to display continuously animated activity indicator. - When download is in progress you can show a progress indicator. Note: for smaller images the progress can go from 0 to 1 in one go. Than this step won't be called.
URLImage(url, placeholder: {
ProgressView($0) { progress in
ZStack {
if progress > 0.0 {
// The download has started. CircleProgressView displays the progress.
CircleProgressView(progress).stroke(lineWidth: 8.0)
}
else {
// The download has not yet started. CircleActivityView is animated activity indicator that suits this case.
CircleActivityView().stroke(lineWidth: 50.0)
}
}
}
.frame(width: 50.0, height: 50.0)
})CircleProgressView and CircleActivityView are two progress views included in the package to showcase the functionality.
URLImage supports incremental image loading. This way of loading image can create better user experience when using with interlaced PNG, GIF, or progressive JPEG format. Set incremental flag to enable it:
URLImage(url, incremental: true)Incremental download won't report progress but you can still use activity indicator to play animation when the first bytes has not been loaded yet.
Note: memory consumption in this mode is higher because the image data is stored in memory and written to disk only after the download completes.
URLImage stores downloaded image files in the Caches/ folder. The system may delete the Caches/ folder to free up disk space. However to provide better control this files have expiryDate set. Files with surpassed expiry date are deleted (lazily on attempt to read). By default files expire 7 days after download. Here are the ways to control this:
Provide expiryDate in the constructor:
URLImage(url, expireAfter: Date(timeIntervalSinceNow: 31_556_926.0)) // Expire after a yearChange default expiryDate:
URLImageService.shared.setDefaultExpiryTime(3600.0) // Expire after an hourCached images can be removed by URL:
URLImageService.shared.removeCachedImage(with: url)Because cached files are deleted lazily it is a good idea to clean caches time to time:
-
Call
URLImageService.shared.cleanFileCache()at some point on the app launch. This method will asynchronoously clean caches and won't block your launch sequence. -
Files cache can be reset by calling
URLImageService.shared.resetFileCache().
URLImage supports image processing and Core Image filters. The ImageProcessing encapsulates data and logic to process an image. URLImage initializer accepts an array of ImageProcessing objects.
URLImage(url, processors: [ /* Array of image processors */ ])Image processing is performed in-order on a background queue. URLImage limits maximum number of operations in order not to create thread explosion.
Note: currently image processing is supported for non-incremental downloads
There are two ways to implement custom image processor:
Implement ImageProcessing protocol. This is the most flexible and reusable approach.
protocol ImageProcessing {
func process(_ input: CGImage) -> CGImage
}Use ImageProcessorClosure and pass image processor as a closure.
URLImage(url, processors: [
ImageProcessorClosure { input in
// return result or input
}
]Core Image provides number of useful filters and URLImage has built-in support for it with CoreImageFilterProcessor processor.
// Apply sepia filter
URLImage(url, processors: [
CoreImageFilterProcessor(name: "CISepiaTone", parameters: [ kCIInputIntensityKey: 0.9 ])
])When applying multiple Core Image filters it is best to reuse CIContext object:
// Apply sepia and bloom filters
struct MyImageView : View {
let url: URL
let ciContext = CIContext()
var body: some View {
URLImage(url,
processors: [
CoreImageFilterProcessor(name: "CISepiaTone", parameters: [ kCIInputIntensityKey: 0.9 ], context: self.ciContext),
CoreImageFilterProcessor(name: "CIBloom", parameters: [ kCIInputIntensityKey: 1, kCIInputRadiusKey: 10.0 ], context: self.ciContext)
])
}
}Note: Core Image framework is not supported on watchOS
For best performance it is important to keep main thread free and graphic operations executed by GPU. You can read more in my post here: Rendering performance of iOS apps.
We want to follow this criteria:
- Image point size must be the same as the view frame;
- Image scale must be the same as the screen scale;
- Image color format must be natively supported.
URLImage provides convenient way to resize images preserving color space. Use Resize processor the view frame is know in advance.
URLImage(url,
processors: [ Resize(size: CGSize(width: 100.0, height: 100.0), scale: UIScreen.main.scale) ],
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
})
.frame(width: 100.0, height: 100.0)Use UIScreen scale on iOS and NSScreen backingScaleFactor on macOS.
import SwiftUI
import URLImage
struct DetailView : View {
let url: URL
var body: some View {
URLImage(url,
placeholder: {
ProgressView($0) { progress in
ZStack {
if progress > 0.0 {
CircleProgressView(progress).stroke(lineWidth: 8.0)
}
else {
CircleActivityView().stroke(lineWidth: 50.0)
}
}
}
.frame(width: 50.0, height: 50.0)
},
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 5))
.padding(.all, 40.0)
.shadow(radius: 10.0)
})
}
}import SwiftUI
import URLImage
struct ListView : View {
let urls: [URL]
var body: some View {
NavigationView {
List(urls, id: \.self) { url in
NavigationLink(destination: DetailView(url: url)) {
HStack {
URLImage(url,
delay: 0.25,
processors: [ Resize(size: CGSize(width: 100.0, height: 100.0), scale: UIScreen.main.scale) ],
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
})
.frame(width: 100.0, height: 100.0)
Text("\(url)")
}
}
}
.navigationBarTitle(Text("Images"))
}
}
}This example demonstrates using filters from this documentation Processing an Image Using Built-in Filters.
import SwiftUI
import URLImage
import CoreImage
struct DetailView : View {
let url: URL
let ciContext = CIContext()
var body: some View {
URLImage(url,
processors: [
// Core Image Sepia filter
CoreImageFilterProcessor(name: "CISepiaTone", parameters: [ kCIInputIntensityKey: 0.9 ], context: self.ciContext),
// Core Image Bloom filter
CoreImageFilterProcessor(name: "CIBloom", parameters: [ kCIInputIntensityKey: 1, kCIInputRadiusKey: 10.0 ], context: self.ciContext),
// Core Image Lanczos scale in a closure
ImageProcessorClosure { input in
let scaleFilter = CIFilter(name:"CILanczosScaleTransform")
let ciImage = CIImage(cgImage: input)
scaleFilter?.setValue(ciImage, forKey: kCIInputImageKey)
let aspectRatio = Double(input.width) / Double(input.height)
scaleFilter?.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)
scaleFilter?.setValue(0.5, forKey: kCIInputScaleKey)
guard let outputImage = scaleFilter?.outputImage else {
return input
}
var bounds = CGRect(x: 0, y: 0, width: input.width, height: input.height)
bounds.origin.x = bounds.width * -0.25
bounds.origin.y = bounds.height * -0.25
let resultImage = self.ciContext.createCGImage(outputImage, from: bounds, format: .RGBA8, colorSpace: input.colorSpace)
return resultImage ?? input
}
])
}
}URLImage allows you to configure its parameters using initializers:
init(_ url: URL, delay: TimeInterval, incremental: Bool, processors: [ImageProcessing]?, expiryDate: Date?)
init(_ urlRequest: URLRequest, delay: TimeInterval, incremental: Bool, processors: [ImageProcessing]?, expiryDate: Date?)url
URL of the remote image.
urlRequest
URLRequest for the remote image. The package expects GET request with URL.
fileIdentifier
String uniquely identifying image file. By default this is URL (when the value is not provided). Should be used when the same image can have multiple URLs.
delay
Delay before URLImage fetches the image from cache or starts to download it. This is useful to optimize scrolling when displaying URLImage in a List view. Default is 0.0.
incremental
Set to use incremental image downloading mode.
processors
Optional list of image processors to apply.
expiryDate
Date when image considered to be expired and needs to be redownloaded.
URLImage is a Swift Package and you can install it with Xcode 11:
- HTTPS
https://github.com/dmytro-anokhin/url-image.gitURL from github; - Open File/Swift Packages/Add Package Dependency... in Xcode 11;
- Paste the URL and follow steps.
Use GitHub issues to report a bug. Include this information when possible:
- Summary and/or background;
- OS and what device you are using;
- Version of URLImage library;
- What you expected would happen;
- What actually happens;
- Additional information:
- Screenshots or video demonstrating a bug;
- Crash log;
- Sample code, try isolating it so it compiles without dependancies;
- Test data: if you use public resource provide URLs of the images.
Use GitHub issues to request a feature.
Contributions are welcome. Please create a GitHub issue before submitting a pull request to plan and discuss implementation.
If you like the package please share it with your network. When you ship an app with URLImage I would love to know about it 🙌