Wednesday, August 8, 2018

Quandl API Client: Part 2

If you are interested in more iOS tutorials, please visit my portfolio site at https://suzhoupanda.github.io/. Some of my games and apps haven’t been updated for a while and may be a little buggy, but they are free, so feel free to download and enjoy.
This tutorial is the second part in a two part series. Previously, we defined an APIClient class that can connect to the Quandl API REST endpoint, perform an API request, and download JSON data. If you are interested in Part 1 of this tutorial, just click here, otherwise keep reading. Here, we define a JSONReader class that will be able to parse the downloaded JSON data and convert it to NSManagedObject classes that can be saved to the SQLite database underlying a CoreData persistent store.
Since we are using CoreData to persist data, we define a class for our CoreDataStack. This is a common technique in CoreData programming, and if you selected the “Use Core Data” option when you started your project, you should have boilerplat code generated by Xcode automatically in the AppDelegate class. Defining an independent CoreData stack, then, allows us to access the the CoreData SQLite store without having to go through the delegate:
 

import Foundation
import CoreData

class CoreDataStack{
    

    var managedContext: NSManagedObjectContext{
        return persistentContainer.viewContext
    }


    var modelName: String

    init(withModelName modelName: String) {
    
        self.modelName = modelName
    }

    lazy private var persistentContainer: NSPersistentContainer = {
    
        let container = NSPersistentContainer(name: self.modelName)
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
            
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    func saveContext(){
        do {
            print("Performing save...")
            try self.managedContext.save()
            print("Save completed...")
            
        } catch let error as NSError {
            print("Error: unable to save data: \(error.localizedDescription)")
        }
    }
}

Now that we have the CoreDataStack defined, we proceed to define a JSONReader class. This class has two instance variables, coreDataStack, a reference to a CoreDataStack which we just defined and which will be force unwrapped, as well as an optional string for the modelName, which corresponds to the name of our .momd file in the project.
In addition, we define a computed property, managedContext, which is used to quickly access the managedObjectContext associated with the persistent store contained in the CoreData stack.
There are two initializers defined for this class: a default initializer that automatically generates a JSONReader object based on the name of the primary .xcdatamodeld file in the project, in this case, “EDGAR_Financials_Helper”). Two additional initializers are provided, one that takes a CoreDataStack as an argument and another that takes a modelName string argument, which in turn is used to instantiate a CoreDataStack indirectly. The extra initializers are provided for convenience in case we generate additional .xcdatamodeld files in our project, which may happen in order to keep the project more organized and to avoid clutter.
 


class JSONReader{
    
   
    var coreDataStack: CoreDataStack!
    var modelName: String?
    
    
    init(){
        self.coreDataStack = CoreDataStack(withModelName: "EDGAR_Financials_Helper")
    }
    
    init(withCoreDataStack stack: CoreDataStack){
        self.coreDataStack = stack
    }
    
    init(withModelName name: String){
        self.modelName = name
        self.coreDataStack = CoreDataStack(withModelName: name)
    }
    
     var managedContext: NSManagedObjectContext{
        
        return self.coreDataStack.managedContext
    }

Since our JSONReader will instantiate CoreData managed objects that can be stored in a SQLite database, we click on our .xcdatamodeld file, in this case named “EDGAR_Financials_Helper” and define two entities in our data model editor in Xcode (show below), StockQuotePeriod and DailyQuoteSummary.




From the previous tutorial, we know that the JSON data returned from the REST API endpoint looks something like what is shown below:
 
["dataset_data": {
    collapse = "";
    "column_index" = "";
    "column_names" =     (
        Date,
        Open,
        High,
        Low,
        Close,
        Volume,
        "Ex-Dividend",
        "Split Ratio",
        "Adj. Open",
        "Adj. High",
        "Adj. Low",
        "Adj. Close",
        "Adj. Volume"
    );
    data =     (
                (
            "2005-10-14",
            "86.90000000000001",
            "87.7",
            "85.95999999999999",
            "87.59999999999999",
            1373400,
            0,
            1,
            "80.950577512303",
            "81.69580722472899",
            "80.074932600202",
            "81.60265351067601",
            1373400
        )

);
    "end_date" = "2005-10-14";
    frequency = daily;
    limit = "";
    order = "";
    "start_date" = "2001-01-05";
    transform = "";
}]

The “data” key basically maps to an array of nested arrays, where the nested arrays contain the attributes for an individual stock quote on a given day. That is, each member of the nested array contains the following stock quote attributes:
Date = Date of the stock quote for the day in question
Open = Opening price of the stock
High = High price of the stock for the day in question
Low = Low price of the stock for the day in question
Close = Closing price of the stock for the day in question
Volume = Total volume of stocks traded for the day in question
“Ex-Dividend” = Ex-Dividend value for stocks for the day in question
“Split Ratio” = Split Ratio
“Adj. Open” = Adjusted Opening Price
“Adj. High” = Adjusted High Price
“Adj. Low” = Adjusted Low Price
“Adj. Close” = Adjusted Closing Price
“Adj. Volume” = Adjusted Volume for Total Stocks Traded

In order to convert this array into a corresponding DailyQuoteSummary object, we implement the method saveDailyQuote(forStockQuotePeriod:dailyQuote:) show below. This method will take as an argument stockQuotePeriod, which has a reference to a set of DailyQuoteSummary objects in a to-many relationship, as can be seen from the data model editor snapshots above. CoreData automatically generates the classes for these NSManagedObject classes, and among the methods provided by CoreData is addToDailyQuoteSummary, which is a convenience method defined for an entity that has a to-many relationship with another object as represented in a relationship attribute.
    func saveDailyQuote(forStockQuotePeriod stockQuotePeriod: StockQuotePeriod, dailyQuote: [Any]){
        
        let dailyQuoteSummary = DailyQuoteSummary(context: self.managedContext)
        
        if let dateStr = (dailyQuote[0] as? String), let date = dateFormatter.date(from: dateStr){
            dailyQuoteSummary.date = date
        }
    
        let open = (dailyQuote[1] as! NSNumber).floatValue
        dailyQuoteSummary.open = open
        
        
        let high = (dailyQuote[2] as! NSNumber).floatValue
        dailyQuoteSummary.high = high
        
        
        let low = (dailyQuote[3] as! NSNumber).floatValue
        dailyQuoteSummary.low = low
        
        
        let close = (dailyQuote[4] as! NSNumber).floatValue
        dailyQuoteSummary.close = close
        
        
        let volume = (dailyQuote[5] as! NSNumber).int64Value
        dailyQuoteSummary.volume = volume
        
        let exDividend = (dailyQuote[6] as! NSNumber).int64Value
        dailyQuoteSummary.exDividend = exDividend
        
        let splitRatio = (dailyQuote[7] as! NSNumber).floatValue
        dailyQuoteSummary.splitRatio = splitRatio
        
        let adjOpen = (dailyQuote[8] as! NSNumber).floatValue
        dailyQuoteSummary.adjustedOpen = adjOpen
        
        let adjHigh = (dailyQuote[9] as! NSNumber).floatValue
        dailyQuoteSummary.adjustedHigh = adjHigh
        
        let adjLow = (dailyQuote[10] as! NSNumber).floatValue
        dailyQuoteSummary.adjustedLow = adjLow
        
        let adjClose = (dailyQuote[11] as! NSNumber).floatValue
        dailyQuoteSummary.adjustedClose = adjClose
        
        let adjVolume = (dailyQuote[12] as! NSNumber).int64Value
        dailyQuoteSummary.adjustedVolume = adjVolume
        
        stockQuotePeriod.addToDailyQuoteSummary(dailyQuoteSummary)
    }

Since each of the members of the array, with the exception of the date, is returned as an NSNumber wrapper object, we have to extract the float value from each NSNumber in order to assign a value to the corresponding attribute in our DailyQuoteSummary managed object. The date is returned as a string, which in turn is converted to a Date object using a dateFormatter, which we define a lazy variable for the JSONReader class, as shown below:
 
 lazy var dateFormatter: DateFormatter = {
        
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        
        return dateFormatter
        
    }()
Note that the format string for the dateFormatter is set such that it matches the format of the date string obtained from the JSON response.

Now that we have a method for defining the individual DailyQuoteSummary objects, we need a method that will create a StockQuotePeriodNSManagedObject from the JSON data and then proceed to call the saveDailyQuote method defined above for each of the stock quote summaries in the “Data” array provided in the JSON response. To this end, we implement the function shown below:
 
func saveQuandlData(forCompanyName companyName: String, forJSONResponseDict jsonResponseDict: JSONResponseDict){
        
        DispatchQueue.main.async {
            
            
            print("Saving data from JSON response...")
            
            guard let dataDict = jsonResponseDict["dataset_data"] as? [String: Any] else {
                print("Error: unable to save data due to inability to extract data dictionary  from JSON response")
                return
            }
            
            let stockQuotePeriod = StockQuotePeriod(context: self.managedContext)
            
            let startDateStr = dataDict["start_date"] as! String
            let endDateStr = dataDict["end_date"] as! String
            
            stockQuotePeriod.startDate = self.dateFormatter.date(from: startDateStr)!
            stockQuotePeriod.endDate = self.dateFormatter.date(from: endDateStr)!
            
            stockQuotePeriod.companyName = companyName
            
            self.coreDataStack.saveContext()
            
            guard let dailyQuotes = dataDict["data"] as? [[Any]] else {
                print("Error: unable to retrieve daily quotes from JSON data")
                return
            }
            
        
            dailyQuotes.forEach({
                
                dailyQuote in
              
                self.saveDailyQuote(forStockQuotePeriod: stockQuotePeriod, dailyQuote: dailyQuote)
            
            })
            
            self.coreDataStack.saveContext()
            
        }
    }
    
With this method now in hand, we can fully understand how our QuandlAPIClient is able to not only make an API request to the REST API endpoint but also saved and persist the returned JSON data. As you will recall from the previous tutorial, the JSONReader object is used in a completion handler stored as an instance variables in the QuandlAPIClient class show below:
 

    var persistDataCompletionHandler: QuandlCompletionHandler = {
        
        companyName, jsonData, error in
        
        if(jsonData != nil){
            
            print("About to save JSON data to persistent store....")
            
            
            //TODO:  Use jsonReader to save data
            let jsonReader = JSONReader()
            jsonReader.saveQuandlData(forCompanyName: companyName, forJSONResponseDict: jsonData!)
            
            print("Data successfully saved!")
            
        } else {
            
            print("An error occurred while attempting to get the JSON data: \(error!)")
        }
        
    }

Congratulations on making it this far! You are amazing.
If you are interested in more iOS tutorials, please visit my portfolio site at https://suzhoupanda.github.io/. Some of my games and apps haven’t been updated for a while and may be a little buggy, but they are free, so feel free to download and enjoy.

No comments:

Post a Comment