HWIFileDownload
HWIFileDownload simplifies file download with NSURLSession
on iOS. Parallel file download can be controlled individually with all possible actions: start, cancel, pause, resume. Download progress is reported natively with NSProgress
for every single file and in total.
Features
Based on NSURLSession
HWIFileDownload offers system background operation even when the app is not running. Downloads can be started individually, cancelled, paused and resumed. All possible states are supported: not started, waiting for download, started (downloading), completed, paused, cancelled, interrupted, error. When resuming cancelled downloads, previously downloaded data is reused. NSProgress
is used for progress reporting and cancel/pause/resume event propagation.
HWIFileDownload is backwards compatible down to iOS 6 (where NSURLConnection
is used instead of NSURLSession
).
Installation
You can add HWIFileDownload to your project manually or with CocoaPods.
Manual installation
HWIFileDownload consists of these files:
- HWIBackgroundSessionCompletionHandlerBlock.h
- HWIFileDownloadDelegate.h
- HWIFileDownloader.h
- HWIFileDownloader.m
- HWIFileDownloadItem.h
- HWIFileDownloadItem.m
- HWIFileDownloadProgress.h
- HWIFileDownloadProgress.m
All files need to be added to your app project.
Installation with CocoaPods
To integrate HWIFileDownload into your Xcode project with CocoaPods, specify it in your Podfile
:
pod 'HWIFileDownload'
Then run
$ pod install
Using HWIFileDownload
To use HWIFileDownload after integration, import the header file HWIFileDownloader.h
in the Objective-C class files where you want to use it:
#import "HWIFileDownloader.h"
For use with Swift you need to add the imports to your Bridging-Header file:
#import "HWIBackgroundSessionCompletionHandlerBlock.h"
#import "HWIFileDownloadDelegate.h"
#import "HWIFileDownloader.h"
#import "HWIFileDownloadItem.h"
#import "HWIFileDownloadProgress.h"
Implementation
HWIFileDownload uses a download identifier for starting a download, retrieving progress information, and for handling download completion. The download identifier is a string that must be unique for each individual file download.
To start a download, the app client calls the method startDownloadWithIdentifier:fromRemoteURL:
of the HWIFileDownloader
.
Download Store as Delegate
The app client must maintain a custom download store to manage the downloads and the persistent store. The app download store needs to implement the protocol HWIFileDownloadDelegate
to be called on important download events.
The delegate is called on download completion. Additional mandatory calls control the visibility of the network activity indicator. Optionally the delegate can be called on download progress change for each download item. To control the local name and location of the downloaded file, the delegate can implement the method localFileURLForIdentifier:remoteURL:
.
Objective-C:
@protocol HWIFileDownloadDelegate
- (void)downloadDidCompleteWithIdentifier:(nonnull NSString *)identifier
localFileURL:(nonnull NSURL *)localFileURL;
- (void)downloadFailedWithIdentifier:(nonnull NSString *)identifier
error:(nonnull NSError *)error
httpStatusCode:(NSInteger)httpStatusCode
errorMessagesStack:(nullable NSArray<NSString *> *)errorMessagesStack
resumeData:(nullable NSData *)resumeData;
- (void)incrementNetworkActivityIndicatorActivityCount;
- (void)decrementNetworkActivityIndicatorActivityCount;
@optional
- (void)downloadProgressChangedForIdentifier:(nonnull NSString *)identifier;
- (void)downloadPausedWithIdentifier:(nonnull NSString *)identifier
resumeData:(nullable NSData *)resumeData;
- (void)resumeDownloadWithIdentifier:(nonnull NSString *)identifier;
- (nullable NSURL *)localFileURLForIdentifier:(nonnull NSString *)identifier
remoteURL:(nonnull NSURL *)remoteURL;
- (BOOL)downloadAtLocalFileURL:(nonnull NSURL *)localFileURL isValidForDownloadIdentifier:(nonnull NSString *)downloadIdentifier;
- (BOOL)httpStatusCode:(NSInteger)httpStatusCode isValidForDownloadIdentifier:(nonnull NSString *)downloadIdentifier;
- (void)customizeBackgroundSessionConfiguration:(nonnull NSURLSessionConfiguration *)backgroundSessionConfiguration;
- (nullable NSURLRequest *)urlRequestForRemoteURL:(nonnull NSURL *)remoteURL;
- (void)onAuthenticationChallenge:(nonnull NSURLAuthenticationChallenge *)challenge
downloadIdentifier:(nonnull NSString *)downloadIdentifier
completionHandler:(void (^ _Nonnull)(NSURLCredential * _Nullable credential, NSURLSessionAuthChallengeDisposition disposition))completionHandler;
- (nullable NSProgress *)rootProgress;
@end
Swift sample class implementing the protocol:
class DownloadStore: NSObject, HWIFileDownloadDelegate {
// HWIFileDownloadDelegate (mandatory)
@objc public func downloadDidComplete(withIdentifier identifier: String, localFileURL: URL) {
print("yes")
}
@objc public func downloadFailed(withIdentifier identifier: String, error: Error, httpStatusCode: Int, errorMessagesStack: [String]?, resumeData: Data?) {
print("no")
}
@objc public func incrementNetworkActivityIndicatorActivityCount() {
//
}
@objc public func decrementNetworkActivityIndicatorActivityCount() {
//
}
// HWIFileDownloadDelegate (optional)
/*
@objc public func downloadProgressChanged(forIdentifier identifier: String) {
//
}
@objc public func downloadPaused(withIdentifier identifier: String, resumeData: Data?) {
//
}
@objc public func resumeDownload(withIdentifier identifier: String) {
//
}
@objc public func localFileURL(forIdentifier identifier: String, remoteURL: URL) -> URL? {
return nil
}
@objc public func download(atLocalFileURL localFileURL: URL, isValidForDownloadIdentifier downloadIdentifier: String) -> Bool {
return true
}
@objc public func httpStatusCode(_ httpStatusCode: Int, isValidForDownloadIdentifier downloadIdentifier: String) -> Bool {
return true
}
@objc public func customizeBackgroundSessionConfiguration(_ backgroundSessionConfiguration: URLSessionConfiguration) {
//
}
@objc public func urlRequest(forRemoteURL remoteURL: URL) -> URLRequest? {
return nil
}
@objc public func onAuthenticationChallenge(_ challenge: URLAuthenticationChallenge, downloadIdentifier: String, completionHandler: @escaping (URLCredential?, URLSession.AuthChallengeDisposition) -> Void) {
//
}
@objc public func rootProgress() -> Progress? {
return nil
}
*/
Downloader
The app needs to hold an instance of the HWIFileDownloader
that manages the download process. HWIFileDownloader
provides methods for starting, querying and controlling individual download processes.
- (nonnull instancetype)initWithDelegate:(nonnull NSObject<HWIFileDownloadDelegate>*)delegate;
- (void)startDownloadWithIdentifier:(nonnull NSString *)identifier
fromRemoteURL:(nonnull NSURL *)remoteURL;
- (void)startDownloadWithIdentifier:(nonnull NSString *)identifier
usingResumeData:(nonnull NSData *)resumeData;
- (BOOL)isDownloadingIdentifier:(nonnull NSString *)identifier;
- (BOOL)isWaitingForDownloadOfIdentifier:(nonnull NSString *)identifier;
- (BOOL)hasActiveDownloads;
- (void)cancelDownloadWithIdentifier:(nonnull NSString *)identifier;
- (nullable HWIFileDownloadProgress *)downloadProgressForIdentifier:(nonnull NSString *)identifier;
Progress
HWIFileDownloadProgress
exposes these properties:
@property (nonatomic, assign, readonly) float downloadProgress;
@property (nonatomic, assign, readonly) int64_t expectedFileSize;
@property (nonatomic, assign, readonly) int64_t receivedFileSize;
@property (nonatomic, assign, readonly) NSTimeInterval estimatedRemainingTime;
@property (nonatomic, assign, readonly) NSUInteger bytesPerSecondSpeed;
@property (nonatomic, strong, readwrite, nullable) NSString *lastLocalizedDescription;
@property (nonatomic, strong, readwrite, nullable) NSString *lastLocalizedAdditionalDescription;
@property (nonatomic, strong, readonly, nonnull) NSProgress *nativeProgress;
Demo App
The demo app shows a sample setup and integration of HWIFileDownload with an Objective-C application.
The app download store is implemented with the custom class DemoDownloadStore
.
The app delegate of the demo app holds an instance of the DemoDownloadStore
and an instance of the HWIFileDownloader
.
Workflows and Scenarios
Start and Restart
On app start a list of all downloads is collected.
Pause and Resume
On "Pause" the download is stopped. The incomplete download data is preserved as resume data. With "Resume" the download can be continued, starting with the already downloaded data.
On iOS 6 pause and resume is not available. On iOS 7 and iOS 8 resume data needs to be managed by the app client. Since iOS 9 NSProgress
manages the resume data transparently with the resume method.
Cancel
On "Cancel" the download is stopped. No resume data is preserved. No re-download is offered.
Crash
On "Crash" the app crashes. On iOS 7 (and later) started downloads continue in the background even though the app is not running anymore. On iOS 6 download does not continue.
Force Quit
After the app has been killed by the user, downloads do not continue in the background. On iOS 7 (and later) resume data is passed back after the app launched again. Interrupted downloads can be resumed.
Background
When running in the background, all running downloads continue on iOS 7 (and later). On iOS 6 all running downloads continue as background task for about 10 minutes.
Network Interruption
When loosing network connection, all running downloads pause after request timeout. On iOS 7 (and later) the downloads resume when network becomes available again. On iOS 6 downloads are stopped after request timeout; they start again with the next app start.
Customization
Two delegate calls provide hooks for adjusting connection parameters:
- (void)customizeBackgroundSessionConfiguration:(nonnull NSURLSessionConfiguration *)backgroundSessionConfiguration;
- (nullable NSURLRequest *)urlRequestForRemoteURL:(nonnull NSURL *)remoteURL; // iOS 6 only
Timeout
With the delegate calls, timeout behaviour can be customized. On iOS there are two timeouts: request timeout and resource timeout.
The request timeout fires "if no data is transmitted for the given timeout value, and is reset whenever data is transmitted". iOS's system default value is 60 seconds.
The resource timeout (available with NSURLSession
) fires "if a resource is not able to be retrieved within a given timeout". The resource timeout fires even if data is currently received. It is reset with the first download task resuming on a background session with no download tasks running. iOS's system default value is 604800 seconds (7 days).
If the host of the network request is not reachable, NSURLConnection
checks for host availability right after request start and fails immediately with an error if the host is not reachable (NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found."). NSURLSession
only terminates when the resource timeout fires.
Authentication
If authentication is required for a file download, you need to implement the delegate method
- (void)onAuthenticationChallenge:(nonnull NSURLAuthenticationChallenge *)challenge
downloadIdentifier:(nonnull NSString *)downloadIdentifier
completionHandler:(void (^ _Nonnull)(NSURLCredential * _Nullable credential, NSURLSessionAuthChallengeDisposition disposition))completionHandler;
The demo app code includes a deactivated sample implementation.
Integration
App Delegate
See the sample code for advice on source code integration with the app delegate.
Dependencies
HWIFileDownload has no third-party dependencies.
Font Awesome
The demo app uses Font Awesome for the download, cancel, pause, resume, completed, error, and cancelled icons.
Notes
Please note that a system bug with iOS 10 broke correct progress reporting after resuming download until iOS 10.2. With the release of iOS 10.2 the bug was fixed by Apple (#23).