Downloading images using the Flickr API

Registering for a free Flickr API key ⬇️🌍

To download photos shot near the selected location, we use the photo platform Flickr. On this platform, users can upload their pictures along with their EXIF data (e.g., including the GPS coordinates). Flickr allows us to access these images by using the Flickr API. 

To learn more about the Flickr API and its functionalities, see the Flickr Developer Guide.

To use the API, we first need to apply for an API key. Don’t worry, for non-commercial projects, this is free. You only need to create a free Flickr account.

To generate your personal API key, open this page and click on “Request an API Key”. Next, you need to log into your Flickr account or create one for free.

After you have logged in, you can create a Non-Commercial Key. 

Attention: Non-commercial applies to this project only as long as you use it only for learning and testing purposes. As soon as you publish an app that uses the Flickr API, you may need to apply for a commercial key!

Next, you have to enter the name of your app and briefly describe what it’s about. Also, check the boxes to indicate that you respect third parties rights and agree to the Flickr API terms. Then click on “Submit”. 

Next, you’ll see the personal API key that Flickr has generated for you.

Since we will need this key all the time, we will add it to our Xcode project. To do this, we create a new Swift file and name it “APIHelper.swift”. In this file, we declare a constant called “apiKey” and copy-paste the just generated API key as a String.

let apiKey = "*YOUR API KEY*"

How to call the API

But how do we send a request to Flickr to download images? Flickr provides us with many different API methods to do this. For our purpose, we need the “flickr.photos.search” method. To see how it works, open the following link

Here we can select different parameters we want to use to download suitable images. First, checkmark “apiKey” and use your API key as the value. We want to download pictures from a specific location. Thus, mark “lat” and “lon” as well. For testing purposes, you can use the (decimal) Latitude “48.864716” and the (decimal) Longitude “2.349014”. Also, we only want to download pictures from a certain radius around the given location.

So for example use “1” as “radius” and “km” as “radius_units”. Eventually, we want to download only a limited number of images. To do this, enter “40” as the value of the argument “per_page”. Important: Select “JSON” as the output format and select the option “Do not sign call”.

To perform the test API request, click “Submit”! 

As you can see, Flickr returns a bunch of data as JSON. 

Note: JSON is a data format that has a tree structure and is easy for humans to read. To use the data from a JSON, we need to extract it properly. This process is called parsing. We can use the parsed data to download the individual images (we will learn how this works in a moment). At the moment, the most interesting part of our test request is the URL generated at the bottom of the page. The URL consists of the different arguments of our request. We can use this URL structure for our app to make dynamic requests for different locations.

Thus, create a function named “generateFlickrURL” below the apiKey constant. We want to specify the longitude, longitude, and the number of images to be downloaded for each request. 

func generateFlickrURL(latitude: Double, longitude: Double, numberOfPhotos: Int) -> String {

}

We already know how the URL must be structured:

func generateFlickrURL(latitude: Double, longitude: Double, numberOfPhotos: Int) -> String {
    let url = "https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=\(apiKey)&lat=\(latitude)&lon=\(longitude)&radius=1&radius_units=km&per_page=\(numberOfPhotos)&format=json&nojsoncallback=1"
    return url
}

Using this function, we can now generate the matching request URL, which returns the corresponding data as JSON.

Fetching the JSON data ⬇️

To receive the JSON with the generateFlickrURL, parse it, and finally download the images, we create a new file called “DownloadHelper.swift”.

First, we set the number of photos to be downloaded by declaring a constant in our DownloadHelper.swift file.

let numberOfPhotos = 400

Next, we create the function that we’ll use to fetch the URLs for every Image that should be downloaded. We mark this function with “throws” to handle potential errors.

func fetchImageURLs(fromFlickrURL: String) throws -> [String] {

    
}

To handle those potential errors, we need to create an enum conforming to the Error protocol. Fortunately, conforming to it doesn’t require us to add any specific properties or methods. And since we are only facing the possible error of a bad URL, we only need to define one error case. Thus, let’s add an appropriate enum to our DownloadHelper.swift file.

enum FetchError: Error {
    case noURL
}

Let’s continue with trying to generate the URL that returns the JSON data. If this fails we throw our noURL error we just defined.

func fetchImageURLs(fromFlickrURL: String) throws -> [String] {

    guard let url = URL(string: fromFlickrURL) else { throw FetchError.noURL }

}

We use this url to create the actual request out of it:

func fetchImageURLs(fromFlickrURL: String) throws -> [String] {

    guard let url = URL(string: fromFlickrURL) else { throw FetchError.noURL }
   let request = URLRequest(url: url)

}

Next, we want to send this request and retrieve the returned JSON data.

func fetchImageURLs(fromFlickrURL: String) throws -> [String] {

    guard let url = URL(string: fromFlickrURL) else { throw FetchError.noURL }
   let request = URLRequest(url: url)
   let (data, response) = try URLSession.shared.data(for: request)

}

Excursus: Concurrency in Swift 💡

Xcode prompts an error message reading: “‘async’ call in a function that does not support concurrency”. What does this mean?

For this purpose, we need to understand the difference between synchronous and asynchronous functions first. The system of the device our app runs on uses so-called threads to perform various processes, i.e. to perform a function defined by us. You can imagine threads as working units of the system.

A “normal” function is a synchronous function. A synchronous function blocks the thread until it finishes its work. As long as the thread is blocked, it cannot perform any other task. So a synchronous function is only suitable for fast executable and lightweight tasks.

An asynchronous function, on the other hand, does not block the thread. An asynchronous function can suspend, meaning giving control of the thread to the system that decides what is most important (called awaiting). At some time the system will resume the function (called resuming). This is especially useful for more complex tasks that may take a long time to complete. These include, in particular, networking processes whose speed depends on Internet performance or resource-intensive computations.

One such task is our URLSession. In particular, it depends on the user’s Internet connection how long it takes us to receive a response from the Flickr server. Therefore, it is necessary that we tell Swift to perform the URLSession asynchronously. However, this is only possible in an asynchronous context. This means that our fetchImageURLs must also be marked as asynchronous. And to do this, we mark our fetchImageURLs as asynchronously using the async keyword.

func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {

    //...

}

Now we can use the await keyword to tell Swift to perform the URLSession asynchronously as well. At this point, we can also omit the response because we won’t need it for this function.

func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {

    guard let url = URL(string: fromFlickrURL) else { throw FetchError.noURL }
   let request = URLRequest(url: url)
   let (data, _) = try await URLSession.shared.data(for: request)

}

The system now decides when to release the thread to perform the URLSession. We then just have to wait until we get the JSON back and then assign it to our data constant (usually this should only take a few seconds of course).

Creating the data model for our JSON data 🛠

Before we can parse the JSON data we retrieve, we need to create a corresponding data model. Take a look at the sample JSON data we before when testing the “flickr.photos.search” method.

To access values nested in the JSON we need to create a data model reflecting the structure of the JSON. Fortunately, that’s super simple using the quicktype.io tool. If we copy and paste the sample JSON it automatically generates a fitting Swift data model for us.

Let’s copy & paste this data model into our DownloadHelper.swift file:

struct Welcome: Codable {
    let photos: Photos
    let stat: String
}

struct Photos: Codable {
    let page, pages, perpage, total: Int
    let photo: [Photo]
}

struct Photo: Codable {
    let id, owner, secret, server: String
    let farm: Int
    let title: String
    let ispublic, isfriend, isfamily: Int
}

Parsing the JSON data ✂️

Let’s continue with parsing the received JSON. We can parse the received JSON data by using a JSONDecoder instance and our created data model.

func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {
    guard let url = URL(string: fromFlickrURL) else { throw FetchError.noURL }
    let request = URLRequest(url: url)
    let (data, _) = try await URLSession.shared.data(for: request)
    let parsedJSON = try JSONDecoder().decode(Welcome.self, from: data)
    
}

We can now cycle through every element in the photo set and use the contained values to build each corresponding photo URL String.

func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {

    //...
    
    var fetchedImageURLs = [String]()
    
    for photo in parsedJSON.photos.photo {
        let photoURL = "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_b.jpg"
        fetchedImageURLs.append(photoURL)
    }
}

Finally, we return the fetchedImageURLs array:

func fetchImageURLs(fromFlickrURL: String) async throws -> [String] {

    //...
    
    var fetchedImageURLs = [String]()
    
    for photo in parsedJSON.photos.photo {
        let photoURL = "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_b.jpg"
        fetchedImageURLs.append(photoURL)
    }
    
    return fetchedImageURLs
}

That’s it! We have everything we need to fetch the URLs of every photo we want to download. In the next section, we’ll use those URLs to fetch the actual image data using asynchronous Image views.

2 replies on “Downloading images using the Flickr API”

Wow! It worked!

Having done work like this professionally in Obj-C doing this in SwiftUI with your design was much easier but equally demanding to debug. I missed a few ‘\’s here and there when forming the image URL but once it was perfect it was a thrill to see images appear for my current location (downtown Vancouver).

Excellent lesson!

Leave a Reply

Your email address will not be published. Required fields are marked *