Duc's Blog

Learn. Create. Contribute.



REST API and JSON: Build iTunes Store Search


Download Starter and Complete Projects

Apps are now driven by the power of the web. It means that you can connect your app to virtually any service and get data from it.

Want the latest stock prices, weather forecast, upload a photo to generate hashtags, search iTunes Store, request a Uber Ride, ANYTHING!

To do this, as a developer, you need to know how to:
+ Build a Networking Stack
+ Use iOS Multi-threading
+ Make network request to REST API
+ Parse JSON data to Foundation Objects

In this episode, Duc will help you learn exactly that and even more!


Source Code in This Training:

NETWORKPROCESSOR.SWIFT

 

//
//  NetworkProcessor.swift
//  AppStore-DucTran
//
//  Created by Duc Tran on 6/20/17.
//  Copyright © 2017 Developers Academy. All rights reserved.
//

import Foundation

// Input: URLRequest (it has a URL)
// Output: returns JSON, or raw data
// Algo: make a request to some server, download the data, return the data

public let NetworkingErrorDomain = "\(Bundle.main.bundleIdentifier!).NetworkingError"
public let MissingHTTPResponseError: Int = 10
public let UnexpectedResponseError: Int = 20

// itunes.apple.com/search?term=iron+man&entity=movie

class NetworkProcessor
{
    let request: URLRequest
    lazy var configuration: URLSessionConfiguration = URLSessionConfiguration.default
    lazy var session: URLSession = URLSession(configuration: self.configuration)
    
    init(request: URLRequest) {
        self.request = request
    }
    
    // IT constructs a URLSession, then download data from the Internet (it takes some time)
    // Returns the data
    // Multi-threading
    
    typealias JSON = [String : Any]
    typealias JSONHandler = (JSON?, HTTPURLResponse?, Error?) -> Void
    typealias DataHandler = (Data?, HTTPURLResponse?, Error?) -> Void
    
    func downloadJSON(completion: @escaping JSONHandler)
    {
        let dataTask = session.dataTask(with: self.request) { (data, response, error) in
            // OFF THE MAIN THREAD
            // Error: missing http response
            guard let httpResponse = response as? HTTPURLResponse else {
                let userInfo = [NSLocalizedDescriptionKey : NSLocalizedString("Missing HTTP Response", comment: "")]
                let error = NSError(domain: NetworkingErrorDomain, code: MissingHTTPResponseError, userInfo: userInfo)
                completion(nil, nil, error as Error)
                return
            }
            
            if data == nil {
                if let error = error {
                    completion(nil, httpResponse, error)
                }
            } else {
                switch httpResponse.statusCode {
                case 200:
                    // OK parse JSON into Foundation objects (array, dictionary..)
                    do {
                        let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String : Any]
                        completion(json, httpResponse, nil)
                    } catch let error as NSError {
                        completion(nil, httpResponse, error)
                    }
                default:
                    print("Received HTTP response code: \(httpResponse.statusCode) - was not handled in NetworkProcessing.swift")
                }
            }
        }
        
        dataTask.resume()
    }
    
    func downloadData(completion: @escaping DataHandler)
    {
        let dataTask = session.dataTask(with: self.request) { (data, response, error) in
            // OFF THE MAIN THREAD
            // Error: missing http response
            guard let httpResponse = response as? HTTPURLResponse else {
                let userInfo = [NSLocalizedDescriptionKey : NSLocalizedString("Missing HTTP Response", comment: "")]
                let error = NSError(domain: NetworkingErrorDomain, code: MissingHTTPResponseError, userInfo: userInfo)
                completion(nil, nil, error as Error)
                return
            }
            
            if data == nil {
                if let error = error {
                    completion(nil, httpResponse, error)
                }
            } else {
                switch httpResponse.statusCode {
                case 200:
                    completion(data, httpResponse, nil)
                default:
                    print("Received HTTP response code: \(httpResponse.statusCode) - was not handled in NetworkProcessing.swift")
                }
            }
        }
        
        dataTask.resume()
    }
}

APPSTOREENDPOINT.SWIFT

//
//  AppStoreEndpoint.swift
//  AppStore-DucTran
//
//  Created by Duc Tran on 6/20/17.
//  Copyright © 2017 Duc Tran. All rights reserved.
//

import Foundation

// example: https://itunes.apple.com/search?term=iron man&entity=...&country=japan

enum AppStoreEndpoint
{
    case search(term: String, entity: String)
    
    var request: URLRequest {
        var components = URLComponents(string: baseURL)!
        components.path = path
        components.queryItems = queryComponents
        
        let url = components.url!
        return URLRequest(url: url)
    }
    
    private var baseURL: String {
        return "https://itunes.apple.com/"
    }
    
    private var path: String {
        switch self {
        case .search: return "/search"
        }
    }
    
    private struct ParameterKeys {
        static let country = "country"
        static let term = "term"
        static let entity = "entity"
    }
    
    private struct DefaultValues {
        static let country = "us"
        static let term = "apple"
    }
    
    // ["term" : "instagram", "entity" : "software", "country" : "us"]
    private var parameters: [String : Any] {
        switch self {
        case .search(let term, let entity):
            let parameters: [String : Any] = [
                ParameterKeys.term : term,
                ParameterKeys.country : DefaultValues.country,
                ParameterKeys.entity : entity
            ]
            
            return parameters
        }
    }
    
    private var queryComponents: [URLQueryItem] {
        var components = [URLQueryItem]()
        
        for (key, value) in parameters {
            let queryItem = URLQueryItem(name: key, value: "\(value)")
            components.append(queryItem)
        }
        
        return components
    }
}

APP.SWIFT

//
//  App.swift
//  AppStore-DucTran
//
//  Created by Duc Tran on 6/20/17.
//  Copyright © 2017 Duc Tran. All rights reserved.
//

import Foundation

struct App
{
    var name: String
    var price: Double
    var description: String
    var formattedPrice: String
    var artworkUrl: URL?
    var itunesUrl: URL?
    
    private struct APIKeys {
        static let name = "trackName"
        static let artworkURL = "artworkUrl512"
        static let description = "description"
        static let formattedPrice = "formattedPrice"
        static let price = "price"
    }
    
    init?(dictionary: [String : Any])
    {
        guard let name = dictionary[APIKeys.name] as? String,
            let artworkURLString = dictionary[APIKeys.artworkURL] as? String,
            let description = dictionary[APIKeys.description] as? String,
            let formattedPrice = dictionary[APIKeys.formattedPrice] as? String,
            let price = dictionary[APIKeys.price] as? Double else {
            return nil
        }
        
        self.name = name
        self.artworkUrl = URL(string: artworkURLString)
        self.description = description
        self.formattedPrice = formattedPrice
        self.price = price
    }
}

APPSTORECLIENT.SWIFT

//
//  AppStoreClient.swift
//  AppStore-DucTran
//
//  Created by Duc Tran on 6/20/17.
//  Copyright © 2017 Duc Tran. All rights reserved.
//

import Foundation

struct AppStoreClient
{
    func fetchApps(withTerm term: String, inEntity entity: String, completion: @escaping ([App]?) -> Void)
    {
        print("2")
        // 1. endpoint
        let searchEndpoint = AppStoreEndpoint.search(term: term, entity: entity)
        let searchUrlRequest = searchEndpoint.request
        
        // 2. network processor
        let networkProcessor = NetworkProcessor(request: searchUrlRequest)
        networkProcessor.downloadJSON { (jsonResponse, httpResponse, error) in
            // THIS IS NOW OFF-THE-MAIN-THREAD!!!!
            // NOW, WE NEED TO GET BACK TO THE MAIN THREAD
            
            DispatchQueue.main.async {
                // 3. get the array of app dictionaries
                guard let json = jsonResponse,
                    let resultDictionaries = json["results"] as? [[String : Any]] else {
                        completion(nil)
                        return
                }
                
                // 4. create an array of apps
                let apps = resultDictionaries.flatMap({ appDictionary in
                    return App(dictionary: appDictionary)
                })
                
                // 5. call completion
                completion(apps)
            }
        }
    }
}

APPSTORETABLEVIEWCONTROLLER.SWIFT

//
//  AppStoreTableViewController.swift
//  AppStore-DucTran
//
//  Created by Duc Tran on 6/21/17.
//  Copyright © 2017 Duc Tran. All rights reserved.
//

import UIKit

class AppStoreTableViewController: UITableViewController
{
    var apps: [App]?
    var appStoreClient = AppStoreClient()
    
    // TODO: create a search controller in this TVC
    
    struct Storyboard {
        static let appCell = "AppCell"
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        fetchApps()
        
        tableView.estimatedRowHeight = tableView.rowHeight
        tableView.rowHeight = UITableViewAutomaticDimension
    }
    
    func fetchApps()
    {
        print("1")
        appStoreClient.fetchApps(withTerm: "instagram", inEntity: "software") { (apps) in
            self.apps = apps
            print(self.apps)
            self.tableView.reloadData()
        }
    }

    // MARK: - Table view data source

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

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let apps = apps {
            return apps.count
        }
        
        return 0
    }


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: Storyboard.appCell, for: indexPath) as! AppTableViewCell

        cell.app = apps?[indexPath.row]
        cell.selectionStyle = .none

        return cell
    }

}

APPTABLEVIEWCELL.SWIFT

//
//  AppTableViewCell.swift
//  AppStore-DucTran
//
//  Created by Duc Tran on 6/21/17.
//  Copyright © 2017 Duc Tran. All rights reserved.
//

import UIKit

class AppTableViewCell: UITableViewCell
{
    @IBOutlet weak var artworkImageView: UIImageView!
    @IBOutlet weak var appNameLabel: UILabel!
    @IBOutlet weak var appDescriptionLabel: UILabel!
    @IBOutlet weak var priceLabel: UILabel!

    var app: App! {
        didSet {
            self.updateUI()
        }
    }
    
    func updateUI()
    {
        appNameLabel.text = app.name
        appDescriptionLabel.text = app.description
        
        if app.price == 0 {
            priceLabel.text = app.formattedPrice
        } else {
            priceLabel.text = "$\(app.price)"
        }
        
        self.artworkImageView.image = nil
        if let url = app.artworkUrl {
            let request = URLRequest(url: url)
            let networkProcessor = NetworkProcessor(request: request)
            
            networkProcessor.downloadData(completion: { (data, response, error) in
                // WE'RE OFF THE MAIN QUEUE!!!!!!!!!
                // WE NEED TO GET BACK ON THE MAIN QUEUE
                DispatchQueue.main.async {
                    if let imageData = data {
                        self.artworkImageView.image = UIImage(data: imageData)
                        self.artworkImageView.layer.cornerRadius = 10.0
                        self.artworkImageView.layer.masksToBounds = true
                    }
                }
            })
        }
    }

}

Featured Free Trainings

Build Nike E-commerce Store

Get This Training
How to Build Facebook Newsfeed

Get This Training
How to Build Instagram

Get This Training