Introduction

At some point in your iOS development career, you’ll likely need to allow users to download files from a UIWebView. Unfortunately, this is one of the hardest concepts to find a good tutorial for, even though it’s actually fairly easy to do.

To benefit developers of all experience levels, this tutorial will start at the beginning by setting up a new project, and then walk you through building a simple download manager (elements of which you’ll be able to easily copy / paste into your own project later on).

If you want to skip right to the code for downloading files from a URL, click here.


Setting Up The Project

Figure 1
First, open Xcode and make a new project. Select Single View Application and click Next. Now, make the menu look like the above image (replace with your own details where necessary). Both languages will be covered in this tutorial, so select whichever one you prefer.

Next, select a place to save your project. If you want, you can check the option to create a Git repository (if you don’t know what Git is, then don’t bother).

Now, your Xcode window should look like this:

— Objective-C

Figure 2-1

— Swift

Figure 2-2


Designing With InterfaceBuilder

Next, we need to set up the app’s user interface in InterfaceBuilder. Start by deleting the default ViewController in the InterfaceBuilder.

Now, add a UITableViewController to the InterfaceBuilder Editor and check the option Is Initial View Controller, like so:

— Objective-C & Swift

Figure 3

Then, embed it in a UINavigationController by going to the menu bar and selecting Editor ⟩ Embed In ⟩ Navigation Controller:

— Objective-C & Swift

Figure 4

Next, add a UIBarButtonItem to the top right of the UINavigationBar in the UITableViewController. Set the ‘System Item’ to ‘Search’:

— Objective-C & Swift

Figure 5

Now, add a new UIViewController to the InterfaceBuilder. Then, control + click & drag from the search button to the new UIViewController and select ‘Show’. This will result in a UINavigationBar appearing in the new UIViewController:

— Objective-C & Swift

Figure 6

To help keep things organized, change the title of the new UIViewController to “Web Browser”. Then, add a UIToolBar to the bottom of it. Then, add a UIWebView to the center and resize it to fill the full width of the UIView and the vertical space between the UIToolBar and the UINavigationBar:

— Objective-C & Swift

Figure 7

Now, its time to add buttons to the UIToolBar that will control various actions on the UIWebView. One button (named “item”) is already there by default. Select this button, and change the value of ‘System Item’ to ‘Rewind’. Then, control + click & drag from this button to the UIWebView, and select ‘goBack’ from the menu that pops up.

Next, add another UIBarButtonItem to the UIToolBar. Set the value of its ‘System Item’ to ‘Refresh’ and control + click & drag from the button to the UIWebView and select ‘reload’.

Do this again, setting the ‘System Item’ to ‘Stop’ and control + click & drag from the button to the UIWebView and select ‘stopLoading’.

Now, add a fourth UIBarButtonItem to the UIToolbar and set the value of its ‘System Item’ to ‘Fast Forward’. Then, control + click & drag from the button to the UIWebView and select ‘goFoward’.

Finally, drag & drop a Flexible Space Bar Button Item in-between each of the previously placed UIBarButtonItems. Now your InterfaceBuilder should look like this:

— Objective-C & Swift

Figure 8


Creating Classes For The ViewControllers

To start, you’re going to delete the files for the default ViewController:

— Objective-C

   delete: ViewController.h & ViewController.m

— Swift

   delete: ViewController.swift

Now, click File ⟩ New ⟩ File, select ‘Cocoa Touch Class’, and click ‘Next’. Set the value of ‘Class’ to “FileBrowserTableViewController”, set the value of ‘Subclass of’ to ‘UITableViewController’, and set the value of ‘Language’ to whichever language you are using. Then click ‘Next’ and ‘Create’.

Next, create another new ‘Cocoa Touch Class’, and then set the value of ‘Class’ to “WebBrowserViewController”, set the value of ‘Subclass of’ to ‘UIViewController’, and like before set the value of ‘Language’ to whichever language you’re using.

Then, go back to InterfaceBuilder. Set the class of the UITableViewController to “FileBrowserTableViewController”, and the class of the UIViewController to “WebBrowserViewController”.


Creating A Custom UITableViewCell Class

Before we make a custom UITableViewCell, we need to make some adjustments to the existing cell in InterfaceBuilder. First, select a cell and change its ‘Identifier’ to “Cell”. After that, add a label to the cell:

— Objective-C & Swift

Figure 3-1

In order to have cells we can customize, we need to make our own custom subclass of UITableViewCell. To do this, go to File ⟩ New ⟩ File, select ‘Cocoa Touch Class’, and click ‘Next’. Set the value of ‘Class’ to “FileTableViewCell”, set the value of ‘Subclass of’ to ‘UITableViewCell’, and set the value of ‘Language’ to whichever language you are using. Then click ‘Next’ and ‘Create’.

Now, we need to add an IBOutlet to connect the previously added label to the code. To do this, go back to InterfaceBuilder, select the cell from before, and change the value of ‘Class’ to “FileTableViewCell”.

— Objective-C

Open the AssistantEditor to FileTableViewCell.h and control + click & drag from the label in the InterfaceBuilder to the line after @interface FileTableViewCell : UITableViewCell. Then, set ‘Name’ to “fileNameLabel” and click ‘Connect’: Figure 9

— Swift

Open the AssistantEditor to FileTableViewCell.swift and control + click & drag from the label in the InterfaceBuilder to the line after the class declaration. Then, set ‘Name’ to “fileNameLabel” and click ‘Connect’: Figure 10

Now you can close AssistantEditor.


Preparing FileBrowserTableViewController

Before we actually start downloading files, we’ll need to prepare FileBrowserTableViewController so that it can display files.

We’ll start off by setting the title of the its UINavigationItem:

— Objective-C

After [super viewDidLoad]; in the viewDidLoad method of FileBrowserTableViewController.m, add:

self.navigationItem.title = @"My Files";

— Swift

After super.viewDidLoad() in the viewDidLoad method of FileBrowserTableViewController.swift, add:

self.navigationItem.title = "My Files"

Next, we need to make an array that contains paths to all the files in the Documents Directory:

— Objective-C

In FileBrowserTableViewController.h, add:

@property (strong, nonatomic) NSMutableArray *documentsDirectoryFiles;

Then, in FileBrowserTableViewController.m, right after the @implementation, add:

static NSString * const reuseIdentifier = @"Cell";

Back in the viewDidLoad method, add:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
_documentsDirectoryFiles = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:documentsDirectory  error:nil];

— Swift

Right after the class declaration in FileBrowserTableViewController.swift add:

var documentsDirectoryFiles : NSMutableArray = []
let reuseIdentifier : String = "Cell"

Then, back in the viewDidLoad method, add:

let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
do
{
	documentsDirectoryFiles = try NSMutableArray.init(array: FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil, options: []))
}
catch let error as NSError
{
	print(error.localizedDescription)
}

Now, its time to tell the UITableView how to populate itself. Essentially, it will iterate through all the indices of the documentsDirectoryFiles array, and create a cell for the index. For the purposes of this app, we only need one section in the UITableView.

— Objective-C

Firstly, we need to import our custom cell. So, in FileBrowserTableViewController.m, add this line to the #import statements:

#import "FileTableViewCell.h"

Next, delete the #warning from the numberOfSectionsInTableView method, and have it return 1.
Then, in numberOfRowsInSection method, delete the #warning and change the return statement to:

return [_documentsDirectoryFiles count];

Next, uncomment the cellForRowAtIndexPath method. Delete all the code that’s inside of it by default, and add these lines:

FileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];
	NSString *fileName = [NSString stringWithFormat:@"%@", _documentsDirectoryFiles[indexPath.row]];
	fileName = [fileName lastPathComponent];
	fileName = [fileName stringByReplacingOccurrencesOfString:@"%20" withString:@" "];
	cell.fileNameLabel.text = fileName;

	return cell;

What this does is create our custom cell. Then we get the filename, by simply taking the last piece of the full file path from documentsDirectoryFiles. When downloading from the web, filenames will often have “%20” instead of spaces. So, next we replace all occurrences of “%20” in fileName with a space. Then, we change the text of the cell’s fileNameLabel to fileName. Finally, we return the cell.

— Swift

With Swift, we don’t need to import FileTableViewCell, the compiler handles all that for us. So, we can skip right to uncommenting the cellForRowAt indexPath method. Then, delete all the code that’s inside of it by default. Now, add these lines to it:

let cell : FileTableViewCell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! FileTableViewCell
let file : NSURL = documentsDirectoryFiles[indexPath.row] as! NSURL
var fileName : NSString = NSString(string: file.absoluteString!)
fileName = fileName.replacingOccurrences(of: "%20", with: " ") as NSString
cell.fileNameLabel.text = fileName as String

return cell

What this does is create our custom cell. Then we get the filename, by simply taking the last piece of the full file path from documentsDirectoryFiles. When downloading from the web, filenames will often have “%20” instead of spaces. So, next we replace all occurrences of “%20” in fileName with a space. Then, we change the text of the cell’s fileNameLabel to fileName. Finally, we return the cell.

Now, our UITableView is ready to display any files in the Documents Directory of the app. Just to review, this is what your code should look like at this point:

— Objective-C

FileTableViewCell.h:

#import <UIKit/UIKit.h>


@interface FileTableViewCell : UITableViewCell

@property (weak, nonatomic) IBOutlet UILabel *fileNameLabel;

@end

FileBrowserTableViewController.h:

import <UIKit/UIKit.h>


@interface FileBrowserTableViewController : UITableViewController

@property (strong, nonatomic) NSMutableArray *documentsDirectoryFiles;

@end

FileBrowserTableViewController.m:

#import "FileBrowserTableViewController.h"
#import "FileTableViewCell.h"



@interface FileBrowserTableViewController ()

@end



@implementation FileBrowserTableViewController

static NSString * const reuseIdentifier = @"Cell";


- (void)viewDidLoad
{
	[super viewDidLoad];

	self.navigationItem.title = @"My Files";

	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	_documentsDirectoryFiles = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:documentsDirectory  error:nil];
}


- (void)didReceiveMemoryWarning
{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}


#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 1;
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [_documentsDirectoryFiles count];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	FileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];
	NSString *fileName = [NSString stringWithFormat:@"%@", _documentsDirectoryFiles[indexPath.row]];
	fileName = [[fileName lastPathComponent] stringByDeletingPathExtension];
	fileName = [fileName stringByReplacingOccurrencesOfString:@"%20" withString:@" "];
	cell.fileNameLabel.text = fileName;

	return cell;
}


@end

— Swift

FileTableViewCell.swift:

import UIKit



class FileTableViewCell: UITableViewCell
{
	@IBOutlet weak var fileNameLabel: UILabel!


	override func awakeFromNib()
	{
		super.awakeFromNib()
		// Initialization code
	}


	override func setSelected(_ selected: Bool, animated: Bool)
	{
		super.setSelected(selected, animated: animated)
		// Configure the view for the selected state
	}
}

FileBrowserTableViewController.swift:

import UIKit



class FileBrowserTableViewController: UITableViewController
{
	var documentsDirectoryFiles : NSMutableArray = []
	let reuseIdentifier : String = "Cell"


	override func viewDidLoad()
	{
		super.viewDidLoad()

		self.navigationItem.title = "My Files"

		let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

		do
		{
			documentsDirectoryFiles = try NSMutableArray.init(array: FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil, options: []))
		}
		catch let error as NSError
		{
			print(error.localizedDescription)
		}
	}


	override func didReceiveMemoryWarning()
	{
		super.didReceiveMemoryWarning()
		// Dispose of any resources that can be recreated.
	}


	// MARK: - Table view data source

	override func numberOfSections(in tableView: UITableView) -> Int
	{
		return 1
	}


	override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
	{
		return 0
	}


	override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
	{
		let cell : FileTableViewCell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! FileTableViewCell
		let file : NSURL = documentsDirectoryFiles[indexPath.row] as! NSURL
		var fileName : NSString = NSString(string: file.absoluteString!)
		fileName = fileName.lastPathComponent as NSString
		fileName = fileName.replacingOccurrences(of: "%20", with: " ") as NSString
		cell.fileNameLabel.text = fileName as String

		return cell
	}
}


Preparing The WebBrowserViewController

Currently, our UIWebView doesn’t do anything, and it isn’t connected to the WebBrowserControllerClass. So, we must go to the InterfaceBuilder, open AssistantEditor and connect everything properly.

— Objective-C

Open AssistantEditor to WebBrowserViewController.h. Then we need to add an IBOutlet for the UIWebView, so control + click & drag from the UIWebView in InterfaceBuilder to the line below the @interface declaration in WebBrowserViewController.h. Change the ‘Name’ field to “webView” and click ‘Connect’: Figure 11

— Swift

Open AssistantEditor to WebBrowserViewController.swift. Then we need to add an IBOutlet for the UIWebView, so control + click & drag from the UIWebView in InterfaceBuilder to the line below the class declaration in WebBrowserViewController.swift. Change the ‘Name’ field to “webView” and click ‘Connect’: Figure 12

Now that we have the UIWebView connected to WebBrowserViewController, we can control it. But first, we need to make WebBrowserViewController a UIWebViewDelegate:

— Objective-C

Close AssistantEditor, and open WebBrowserViewController.h. In it, change the @interface to look like this:

@interface WebBrowserViewController : UIViewController <UIWebViewDelegate>

Then, we have to set the UIWebView’s delegate to WebBrowserViewController. To do this, add this line after [super viewDidLoad]:

_webView.delegate = self;

— Swift

Close AssistantEditor, and open WebBrowserViewController.swift. In it, change the class declaration to look like this:

class WebBrowserViewController: UIViewController, UIWebViewDelegate

Then, we have to set the UIWebView’s delegate to WebBrowserViewController. To do this, add this line after super.viewDidLoad():

webView.delegate = self

Next, let’s set the homepage to Google. In the viewDidLoad method, add this line after the code from the previous step:

— Objective-C

[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.google.com"]]];

— Swift

webView.loadRequest(URLRequest(url: URL(string: "https://www.google.com")!))


Downloading Files From The UIWebView

Well, technically, we aren’t downloading from the UIWebView, we’re simply checking the URL before it’s loaded by the UIWebView to see if it’s a filetype we want to download. If it is a filetype we want to download, we tell the UIWebView to not load the URL, and instead create an NSData object with the data from the URL, and then write that data to a file in the Documents Directory.

To start, we need to add the shouldStartLoadWithRequest method to WebBrowserViewController:

— Objective-C

In WebBrowserViewController.m, add this after the end of the didRecieveMemoryWarning method:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{

}

— Swift

In WebBrowserViewController.swift, add this after the end of the didRecieveMemoryWarning method:

func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool
{

}

What this method does, is allow you to check the URL before it is loaded, and determine whether or not the UIWebView should load it or not.

Next, we need a method to check if the URL is a file or a webpage. We could just do this inside the shouldStartLoadWithRequest method, but code gets very messy very fast. To help keep the code clean (and easily modifiable), we’re going to create a new method that checks the URL and determines if it is a downloadable file or not:

— Objective-C

After the end of the shouldStartLoadWithRequest method, add this method:

- (BOOL)requestIsDownloadable: (NSURLRequest *)request
{
	NSString *requestString = [[request URL] absoluteString];
	NSString *fileType = [requestString pathExtension];

	BOOL *isDownloadable = (
		([fileType caseInsensitiveCompare:@"zip"] == NSOrderedSame) ||
		([fileType caseInsensitiveCompare:@"rar"] == NSOrderedSame)
	);


	return isDownloadable;
}

— Swift

After the end of the shouldStartLoadWithRequest method, add this method:

func requestIsDownloadable( request: URLRequest) -> Bool
{
	let requestString : NSString = (request.url?.absoluteString)! as NSString
	let fileType : String = requestString.pathExtension
	print(fileType)
	let isDownloadable : Bool = (
		(fileType.caseInsensitiveCompare("zip") == ComparisonResult.orderedSame) ||
		(fileType.caseInsensitiveCompare("rar") == ComparisonResult.orderedSame)
	)


	return isDownloadable
}

For the purposes of this tutorial, we only want to download zip and rar files. However, you can change the file extension to any file type you’d like to be downloaded. What this method does, is check if the URL’s extension is a zip or rar file, and returns true if it is (e.g. it’s something we want to download).

To help keep the code clean, we are also going to create a separate method for downloading the file. So, after the end of the requestIsDownloadable method, do the following:

— Objective-C

Create a new void method called “initializeDownload”

- (void)initializeDownload: (NSURLRequest *)download
{

}

When there is a file available to download, the user should be alerted of this and given the option whether or not to let the download proceed. So, inside of the initializeDownload method, create a UIAlertController with “Cancel” and “Download” actions:

UIAlertController *downloadAlertController = [UIAlertController
												  alertControllerWithTitle:@"Download Detected!"
												  message:@"Would you like to download this file?"
												  preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction
							   actionWithTitle:NSLocalizedString(@"Nope", @"Cancel action")
							   style:UIAlertActionStyleCancel
							   handler:^(UIAlertAction *action)
							   {
								   NSLog(@"Download Cancelled.");
							   }];
UIAlertAction *okAction = [UIAlertAction
						   actionWithTitle:NSLocalizedString(@"Yes!", @"OK action")
						   style:UIAlertActionStyleDefault
						   handler:^(UIAlertAction *action)
						   {

						   }];
[downloadAlertController addAction:cancelAction];
[downloadAlertController addAction:okAction];
[self presentViewController:downloadAlertController animated:YES completion:nil];

We could simply start the download within the okAction, but we should give the user some indication that the file is in the process of downloading. So, inside of the okAction we are going to present another UIAlertController with no options, and then start the download. The following goes in-between the curly braces in okAction:

UIAlertController *downloadingAlertController = [UIAlertController
	alertControllerWithTitle:[NSString stringWithFormat:@"Downloading..."]
	message:@"Please wait while your file downloads.\nThis alert will disappear when it's done."
	preferredStyle:UIAlertControllerStyleAlert];

[self presentViewController:downloadingAlertController animated:YES completion:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
   NSString *urlToDownload = [[download URL] absoluteString];
   NSURL  *url = [NSURL URLWithString:urlToDownload];
   NSData *urlData = [NSData dataWithContentsOfURL:url];

   if (urlData)
   {
	   NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	   NSString *documentsDirectory = [paths objectAtIndex:0];
	   NSString *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [urlToDownload lastPathComponent]];

	   dispatch_async(dispatch_get_main_queue(), ^{
		   [urlData writeToFile:filePath atomically:YES];
		   [downloadingAlertController dismissViewControllerAnimated:YES completion:nil];
	   });
   }
});

— Swift

Create a new void method called “initializeDownload”

func initializeDownload( download: URLRequest)
{

}

When there is a file available to download, the user should be alerted of this and given the option whether or not to let the download proceed. So, inside of the initializeDownload method, create a UIAlertController with “Cancel” and “Download” actions:

let downloadAlertController : UIAlertController = UIAlertController(title: "Download Detected!", message: "Would you like to download this file?", preferredStyle: UIAlertControllerStyle.alert)

let cancelAction : UIAlertAction = UIAlertAction(title: "", style: UIAlertActionStyle.cancel, handler:
	{(alert: UIAlertAction!) in
		print("Download Cancelled.")
})

let okAction : UIAlertAction = UIAlertAction(title: "", style: UIAlertActionStyle.default, handler:
	{(alert: UIAlertAction!) in

})

downloadAlertController.addAction(cancelAction)
downloadAlertController.addAction(okAction)
self.present(downloadAlertController, animated: true, completion: nil)

We could simply start the download within the okAction, but we should give the user some indication that the file is in the process of downloading. So, inside of the okAction we are going to present another UIAlertController with no options, and then start the download. The following goes in-between the curly braces (and after the in) in okAction:

let downloadingAlertController : UIAlertController = UIAlertController(title: "Downloading...", message: "Please wait while your file downloads.\nThis alert will disappear when it's done.", preferredStyle: UIAlertControllerStyle.alert)
self.present(downloadingAlertController, animated: true, completion: nil)

do
{
	let urlToDownload : NSString = (download.url?.absoluteString)! as NSString
	let url : NSURL = NSURL(string: urlToDownload as String)!
	let urlData : NSData = try NSData.init(contentsOf: url as URL)

	if urlData.length > 0
	{
		let paths : Array = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
		let documentsDirectory : String = paths[0]
		let filePath : String = String.localizedStringWithFormat("%@/%@", documentsDirectory, urlToDownload.lastPathComponent)

		urlData.write(toFile: filePath, atomically: true)
		downloadingAlertController.dismiss(animated: true, completion: nil)
	}
}
catch let error as NSError
{
	print(error.localizedDescription)
}

So, what exactly does this entire method do? Well, it starts by presenting a UIAlertController that gives the user the option to start or cancel the download. If the user starts the download, then another UIAlertController pops up. This one has no options and tells the user to wait for the download to finish. Then, we take the URL and make NSData out of it. If the data exists, we write it to a file in the Documents Directory, and then dismiss the UIAlertController.

Now, we need to go back into the shouldStartLoadWithRequest method and implements our new methods:

— Objective-C

if ([self requestIsDownloadable:request])
{
	[self initializeDownload:request];


	return NO;
}


return YES;

— Swift

if requestIsDownloadable(request: request)
{
	initializeDownload(download: request)


	return false
}


return true

Basically, this checks if the request is downloadable. If it is, the download is initialized and the UIWebView is told to not load the request. Otherwise, the UIWebView loads the request normally.

Now that this is all done, here is what your WebBrowserViewController class should look like:

— Objective-C

WebBrowserViewController.h:

#import <UIKit/UIKit.h>

@interface WebBrowserViewController : UIViewController <UIWebViewDelegate>
@property (weak, nonatomic) IBOutlet UIWebView *webView;

@end

WebBrowserViewController.m:

#import "WebBrowserViewController.h"



@interface WebBrowserViewController ()

@end



@implementation WebBrowserViewController

- (void)viewDidLoad
{
	[super viewDidLoad];

	_webView.delegate = self;
	[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.google.com/"]]];
}


- (void)didReceiveMemoryWarning
{
	[super didReceiveMemoryWarning];
	// Dispose of any resources that can be recreated.
}


- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
	if ([self requestIsDownloadable:request])
	{
		[self initializeDownload:request];


		return NO;
	}


	return YES;
}


- (BOOL)requestIsDownloadable: (NSURLRequest *)request
{
	NSString *requestString = [[request URL] absoluteString];
	NSString *fileType = [requestString pathExtension];
	NSLog(@"FileType: %@", fileType);
	BOOL *isDownloadable = (
		([fileType caseInsensitiveCompare:@"zip"] == NSOrderedSame) ||
		([fileType caseInsensitiveCompare:@"rar"] == NSOrderedSame)
	);


	return isDownloadable;
}


- (void)initializeDownload: (NSURLRequest *)download
{
	UIAlertController *downloadAlertController = [UIAlertController
												  alertControllerWithTitle:@"Download Detected!"
												  message:@"Would you like to download this file?"
												  preferredStyle:UIAlertControllerStyleAlert];
	UIAlertAction *cancelAction = [UIAlertAction
								   actionWithTitle:NSLocalizedString(@"Nope", @"Cancel action")
								   style:UIAlertActionStyleCancel
								   handler:^(UIAlertAction *action)
								   {
									   NSLog(@"Download Cancelled.");
								   }];
	UIAlertAction *okAction = [UIAlertAction
							   actionWithTitle:NSLocalizedString(@"Download", @"OK action")
							   style:UIAlertActionStyleDefault
							   handler:^(UIAlertAction *action)
							   {
								   UIAlertController *downloadingAlertController = [UIAlertController
																					alertControllerWithTitle:[NSString stringWithFormat:@"Downloading..."]
																					message:@"Please wait while your file downloads.\nThis alert will disappear when it's done."
																					preferredStyle:UIAlertControllerStyleAlert];

								   [self presentViewController:downloadingAlertController animated:YES completion:nil];

								   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
									   NSString *urlToDownload = [[download URL] absoluteString];
									   NSURL  *url = [NSURL URLWithString:urlToDownload];
									   NSData *urlData = [NSData dataWithContentsOfURL:url];


									   if (urlData)
									   {
										   NSLog(@"Download Starting...");
										   NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
										   NSString *documentsDirectory = [paths objectAtIndex:0];
										   NSString *filePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [urlToDownload lastPathComponent]];
										   dispatch_async(dispatch_get_main_queue(), ^{
											   [urlData writeToFile:filePath atomically:YES];
											   NSLog(@"Done.");
											   [downloadingAlertController dismissViewControllerAnimated:YES completion:nil];
										   });
									   }
									});
							   }];
	[downloadAlertController addAction:cancelAction];
	[downloadAlertController addAction:okAction];
	[self presentViewController:downloadAlertController animated:YES completion:nil];
}
@end

— Swift

WebBrowserViewController.swift:

import UIKit



class WebBrowserViewController: UIViewController, UIWebViewDelegate
{
	@IBOutlet weak var webView: UIWebView!


	override func viewDidLoad()
	{
		super.viewDidLoad()

		webView.delegate = self
		webView.loadRequest(URLRequest(url: URL(string: "https://www.google.com/")!))
	}


	override func didReceiveMemoryWarning()
	{
		super.didReceiveMemoryWarning()
		// Dispose of any resources that can be recreated.
	}


	func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool
	{
		print(request)
		if requestIsDownloadable(request: request)
		{
			initializeDownload(download: request)


			return false
		}


		return true
	}


	func requestIsDownloadable( request: URLRequest) -> Bool
	{
		let requestString : NSString = (request.url?.absoluteString)! as NSString
		let fileType : String = requestString.pathExtension
		print(fileType)
		let isDownloadable : Bool = (
			(fileType.caseInsensitiveCompare("zip") == ComparisonResult.orderedSame) ||
			(fileType.caseInsensitiveCompare("rar") == ComparisonResult.orderedSame)
		)


		return isDownloadable
	}


	func initializeDownload( download: URLRequest)
	{
		let downloadAlertController : UIAlertController = UIAlertController(title: "Download Detected!", message: "Would you like to download this file?", preferredStyle: UIAlertControllerStyle.alert)

		let cancelAction : UIAlertAction = UIAlertAction(title: "Nope", style: UIAlertActionStyle.cancel, handler:
			{(alert: UIAlertAction!) in
				print("Download Cancelled.")
		})

		let okAction : UIAlertAction = UIAlertAction(title: "Yes!", style: UIAlertActionStyle.default, handler:
			{(alert: UIAlertAction!) in
				let downloadingAlertController : UIAlertController = UIAlertController(title: "Downloading...", message: "Please wait while your file downloads.\nThis alert will disappear when it's done.", preferredStyle: UIAlertControllerStyle.alert)
				self.present(downloadingAlertController, animated: true, completion: nil)

				do
				{
					let urlToDownload : NSString = (download.url?.absoluteString)! as NSString
					let url : NSURL = NSURL(string: urlToDownload as String)!
					let urlData : NSData = try NSData.init(contentsOf: url as URL)

					if urlData.length > 0
					{
						let paths : Array = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
						let documentsDirectory : String = paths[0]
						let filePath : String = String.localizedStringWithFormat("%@/%@", documentsDirectory, urlToDownload.lastPathComponent)

						urlData.write(toFile: filePath, atomically: true)
						downloadingAlertController.dismiss(animated: true, completion: nil)
					}
				}
				catch let error as NSError
				{
					print(error.localizedDescription)
				}
		})

		downloadAlertController.addAction(cancelAction)
		downloadAlertController.addAction(okAction)
		self.present(downloadAlertController, animated: true, completion: nil)
	}
}


Finishing Touches

Now that everything is functional, there’s just a few final adjustments we need to make. If you’ve compiled and tested the app before now, you’ll probably notice that some webpages don’t load. This is because Apple’s App Transport Security Settings don’t allow sites without SSL to load. To change this, you will need to go to the info.plist file. Add a new row of type dictionary. Name it “App Transport Security Settings”. Then, add a row INSIDE of it called “Allow Arbitrary Loads” of type Boolean and set its value to YES.

On top of that, our label is too small for most filenames. To fix this, stretch it across the full width of the UITableViewCell in InterfaceBuilder.

Next, you may notice that some views aren’t quite sized right, depending on what device you build to and what device you use in InterfaceBuilder. This is because there aren’t any constraints to tell the app how to scale based on screen size. To fix this, select each scene, then go to the bottom and click the ‘Resolve Auto Layout Issues’ button, and select ‘Reset to Suggested Constraints’: Figure 13

One final minor issue is that files don’t appear in the UITableView until the app is relaunched. This is because after the initial launch of the app, the UITableView’s data is never reloaded. To solve this, we need to add the viewWillAppear method to FileBrowserTableViewController and write one simple line to it:

— Objective-C

In FileBrowserTableViewController.m add this after the end of the viewDidLoad method:

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];

	[self.tableView reloadData];
}

— Swift

In FileBrowserTableViewController.swift add this after the end of the viewDidLoad method:

override func viewWillAppear(_ animated: Bool)
{
	super.viewWillAppear(animated)

	self.tableView.reloadData()
}

If the user downloads a whole bunch of files, this implementation of refreshing the UITableView’s data may cause lag when returning form WebBrowserViewController. However for the scope of this project, it works well enough.


Outroduction

And with that, our Simple Download Manager is complete! If you wish, feel free to implement a refresh button (or better yet, pull to refresh) for the files list!

Both the Objective-C and Swift versions of this tutorial can be found on GitHub. If there’s something wrong with your code and you have no idea why, then you may want to take a look the completed projects:
Simple Download Manager (Objective-C): GitHub, Direct Download
Simple Download Manager (Swift): GitHub, Direct Download

If you have any questions or suggestions about this tutorial, feel free to contact me on Twitter (@TheTomMetzger) or via email (Tom@Southernerd.us).