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
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