I am reading a very large json file and pulling in data. I can currently pull in the correct data, but when I try and append it to an array, it ends up replacing it.
func parseJSON(json: JSON){
let predicate = {
(json: JSON) -> Bool in
if let jsonID = json["class"].string where jsonID == "sg-header-heading"{
return true
}
return false
}
var backclass = ViewController()
let foundJSON = json.findmultiple(backclass, predicate: predicate)
Using this extension to search for multiple values that match "sg-header-heading"
When I print the array in the extension this is what I get. But when I print it above I get nil. Also I am only getting one value per instead of 6 values in the end.
extension JSON{
func findmultiple(viewclass: ViewController, #noescape predicate: JSON -> Bool) -> JSON? {
var backclass = ViewController()
if predicate(self) {
return self
}
else {
if let subJSON = (dictionary?.map { $0.1 } ?? array) {
for json in subJSON {
if let foundJSON = json.findmultiple(backclass, predicate: predicate) {
let shorten = foundJSON["html"].stringValue
let firstshort = shorten.componentsSeparatedByString(" ")
let secondshort = firstshort[1].componentsSeparatedByString("\r")
let classname = secondshort[0]
if(classname == "STUDY HALL (INSTRUCT)"){
print("skip")
}else{
backclass.importClass.append(classname)
print("fsgsfg \(backclass.importClass)")
//backclass.collection.reloadData()
}
}
}
}
}
return nil
}
}
You are first calling findmultiple() from the outermost context (let foundJSON = json.findmultiple(predicate)). As the code is executed and findmultiple() is actually executed it creates a new instance of with var backclass = ViewController(). As the code goes into its loop it executes findmultiple() again, recursively. As the code goes into its loop an additional time it creates a new instance of backclass. Each time it goes through the loop, the recursion goes deeper and a new instance of ViewController is created each time so backclass always points to a new instance of ViewController.
The solution to this would be to create backclass at a higher level. You might try an approach like this:
var backclass = ViewController()
let foundJSON = json.findmultiple(backclass, predicate)
Note that this would mean passing backclass to findmultiple from inside the loop where the recursive call happens.
Related
I've been trying to extract some data from a dictionary. There should be 5 values. The first code snippet fails, only populating the scheduleForCurrentDay with 1 value, whereas the second snippet works, getting all 5. Can anyone explain why the first one doesn't work? I'm guessing it's something to do with the dictionary being copied but I'm not really sure.
// Fails; only gets one value
private var categories = [StudyCategory]() {
didSet {
let c = categories
for subject in c {
guard let target = subject.quota[currentDayComponent] else { continue }
scheduleForCurrentDay[subject.title] = target
}
}
}
// Succeeds; gets all 5 values
private var categories = [StudyCategory]() {
didSet {
let c = categories
var schedule = [String:Double]()
for subject in c {
schedule[subject.title] = subject.quota[currentDayComponent]
}
scheduleForCurrentDay = schedule
}
}
I am trying to read from Firestore into a Dictionary[Any] type using Struct. I can get the values loaded into variable "data" dictionary with Any type.
However I cannot loop thru it to access normal nested Dictionary variable.
I cannot get Key, values printed.
Following is my code:
class PullQuestions {
//shared instance variable
**public var data = [Any]()**
private var qdb = Firestore.firestore()
public struct questionid
{
let qid : String
var questions : [basequestion]
var answers: [baseans]
}
public struct basequestion {
let category : String
let question : String
}
public struct baseans {
let answer : String
}
class var sharedManager: PullQuestions {
struct Static {
static let instance = PullQuestions()
}
return Static.instance
}
static func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = PullQuestions.sharedManager.qdb.collection("questions")
//var data = [Any]()
rootCollection.order(by: "upvote", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
// var questiondoc = [basequestion]()
for questioncollection in topSnapshot {
rootCollection.document(questioncollection.documentID).collection("answers").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var answers = [baseans]()
for document in snapshot { //There should be only one Document for each answer collection
//Read thru all fields
for i in 0..<document.data().count
{
let newAns = baseans(answer: answer)
print("Answer Docs=>", (answer))
answers.append(newAns)
}
}
let qid = questioncollection.documentID
let category = questioncollection.data()["category"] as! String
let question = questioncollection.data()["question"] as! String
let newQuestions = basequestion(category: category ,question: question)
let newQuestionDict = questionid(qid: qid, questions: [newQuestions], answers: answers)
PullQuestions.sharedManager.data.append(newQuestionDict)
//Return data on completion
completion(PullQuestions.sharedManager.data)
})
}
}
})
}
}
I can print like this
print("Count =>", (PullQuestions.sharedManager.data.count))
// print(PullQuestions.sharedManager.data.first ?? "Nil")
print(PullQuestions.sharedManager.data[0])
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
I could access only the key.. how do i go and get the nested values ?
First of all, consider using Swift code conventions (e.g. your structs are named with small letters, but you should start with capital), this will make your code more readable.
Returning to your question. You use an array instead of dictionary (this piece of code: public var data = [Any]()). And here you are trying to print values:
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
In this context element is an Any object, thus you cannot access any underlying properties. In order to do this you have two options:
1. You should specify the type of array's objects in it's declaration like this:
public var data = [questionid]()
or you can user this:
public var data: [questionid] = []
These two are equals, use the one you prefer.
2. If for any reasons you don't want to specify the type in declaration, you can cast it in your loop. Like this:
for element in PullQuestions.sharedManager.data
{
if let element = element as? quetionid {
print("Elements in data:=>", (element))
// you can also print element.qid, element.questions, element.answers
} else {
print("Element is not questionid")
}
}
You could of course use the force cast:
let element = element as! questionid
and avoid if let syntax (or guard let if you prefer), but I wouldn't recommend this, because it (potentially) can crash your app if element will be nil or any other type.
I am trying to get an array of temperatures in a given time period from an API in JSON format. I was able to retrieve the array through a completion handler but I can't save it to another variable outside the function call (one that uses completion handler). Here is my code. Please see the commented area.
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "api address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
temps.append(tempF)
winds.append(windspeed)
}
}
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
I am calling this method from my view controller class. Here is the code.
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
}
//Here it prints out an empty array
print(weatherData)
}
The issue is that API takes some time to return the data, when the data is return the "Completion Listener" is called and it goes inside the "getWeather" method implementation, where it prints the data of array. But when your outside print method is called the API hasn't returned the data yet. So it shows empty array. If you will try to print the data form "weatherData" object after sometime it will work.
The best way I can suggest you is to update your UI with the data inside the "getWeather" method implementation like this:
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
// Update your UI here.
}
//Here it prints out an empty array
print(weatherData)
}
It isn't an error, when your controller get loaded the array is still empty because your getWeather is still doing its thing (meaning accessing the api, decode the json) when it finishes the callback will have data to return to your controller.
For example if you were using a tableView, you will have reloadData() to refresh the UI, after you assign data to weatherData
Or you could place a property Observer as you declaring your weatherData property.
var weatherData:[Double]? = nil {
didSet {
guard let data = weatherData else { return }
// now you could do soemthing with the data, to populate your UI
}
}
now after the data is assigned to wheaterData, didSet will be called.
Hope that helps, and also place your jsonParsing logic into a `struct :)
I have an issue with code order in a ItemsViewController.swift
When I run my code it starts the for items loop before my api returns the values for the items. This is done in the line: self.viewModel/getItemsTwo... Therefore it thinks that items is nil by the time the loop starts, so it errors with:
fatal error: unexpectedly found nil while unwrapping an Optional value
How can I start the loop only after items has been filled by the api call/function call?
class ItemsViewController: UIViewController {
private let viewModel : ItemsViewModel = ItemsViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
var items = self.viewModel.items
for item in items! {
print(item)
}
}
...
The getItemsTwo function in the viewModel sets the viewModel.items variable when it is called
EDIT 1
ItemsViewModel.swift
...
var items : JSON?
...
func getItemsTwo(user: MYUser) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
}
...
EDIT 2
I am trying to do this:
just change it in the ViewController to:
var items = self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
and the ViewModel to:
func getItemsTwo(user: MYUser) -> JSON {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
return self.items
}
But the return statement still errors as if self.items in nil.
Maybe you could expand your getItemsTwo method to take a callback closure, something like:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void)
Meaning that you have a parameter called callback which is a closure function that returns Void and takes an array of JSON items as an input parameter.
Once you have added newdata to self.items you could call your callback closure like so:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = new data
//Items are now populated, call callback
callback(items: self.items)
}
}
And then, in your ItemsViewController you could say:
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser()) { items in
for item in items {
print(item)
}
}
}
Notice that if you add a closure as the last parameter you can use a so called "Trailing Closure" and place it "outside" or "after" your function as described in this chapter of "The Swift Programming Language".
Hope that helps you (I haven't checked in a compiler so you might get some errors, but then well look at them OK :)).
I want to load a PDF that is in my application bundle into a CGPDFDocument.
Is there some way of calling a function that if any of the parameters that don't accept options have values that are nil, the function isn't called and nil is returned.
eg:
let pdfPath : String? = NSBundle.mainBundle().pathForResouce("nac_06", ofType:"pdf")
//I want to do this
let data : NSData? = NSData(contentsOfFile:pdfPath)
//I have to do this
let data : NSData? = pdfPath != nil ? NSData(contentsOfFile:pdfPath) : nil
let doc : CGPDFDocumentRef? = CGPDFDocumentCreateWithProvider(CGDataProviderCreateWithCFData(data));
//pageView.pdf is optional, nicely this function accepts the document as an optional
pageView.pdfPage = CGPDFDocumentGetPage(doc, 1);
Because NSData.init?(contentsOfFile path:String), doesn't define path as optional, even though it is has an optional return value, I have to check before and if the parameter is nil, return nil. Is there some syntactic sugar for the data assignment (instead of the ?: operator)?
Either use multiple optional bindings separated by commas
func loadPDF() -> CGPDFDocumentRef?
{
if let pdfPath = NSBundle.mainBundle().pathForResouce("nac_06", ofType:"pdf"),
data = NSData(contentsOfFile:pdfPath),
doc = GPDFDocumentCreateWithProvider(CGDataProviderCreateWithCFData(data)) {
return doc
} else {
return nil
}
}
or use the guard statement
func loadPDF() -> CGPDFDocumentRef?
{
guard let pdfPath = NSBundle.mainBundle().pathForResouce("nac_06", ofType:"pdf") else { return nil }
guard let data = NSData(contentsOfFile:pdfPath) else { return nil }
return GPDFDocumentCreateWithProvider(CGDataProviderCreateWithCFData(data))
}
All explicit type annotations are syntactic sugar and not needed.
Edit:
In your particular case you need only to check if the file exists and even this – the file is missing – is very unlikely in iOS. Another benefit is to be able to return a non-optional PDFDocument.
func loadPDF() -> CGPDFDocumentRef
{
guard let pdfPath = NSBundle.mainBundle().pathForResource("nac_06", ofType:"pdf") else {
fatalError("file nac_06.pdf does not exist")
}
let data = NSData(contentsOfFile:pdfPath)
return CGPDFDocumentCreateWithProvider(CGDataProviderCreateWithCFData(data!))!
}
I assume that you also don't want to continue with the execution of the function if pdfPath or data is nil. In this case, guard would be the best choice:
guard let pdfPath = NSBundle.mainBundle().pathForResouce("nac_06", ofType:"pdf") else {
// eventually also report some error
return
}
guard let data = NSData(contentsOfFile:pdfPath) else {
// eventually also report some error
return
}
// at this point you have a valid data object
You could also combine this into a single guard statement, to reduce the code duplication, you'll loose however in this case the possibility to know which of the two failed.
guard let pdfPath = NSBundle.mainBundle().pathForResource("nac_06", ofType:"pdf"),
data = NSData(contentsOfFile:pdfPath) else {
// eventually also report some error
return
}
There is two ways to achieve this.
Extend NSData class and create your own convenience init? method
Use the guard statement
I prefer the second method:
func getPDF(path : String?) -> CGPDFDocumentRef?
{
guard let filePath = path,
data = NSData(contentsOfFile: filePath),
pdf = GPDFDocumentCreateWithProvider(CGDataProviderCreateWithCFData(data)) else
{
return nil
}
return pdf
}
Call the method like:
let doc = getPDF(path : NSBundle.mainBundle().pathForResouce("nac_06", ofType:"pdf"))
You could do something fancy by defining a custom operator to deal with this situation. For example:
infix operator ^> {associativity left precedence 150}
func ^><T, U>(arg: T?, f: T->U?) -> U?{
if let arg = arg {
return f(arg)
} else {
return nil
}
}
The operator takes an optional left-side argument and a function that takes a non-optional and returns another optional as a right-side argument.
You could then write your code like this:
let pdfPath = NSBundle.mainBundle().pathForResource("nac_06", ofType:"pdf")
//the line below needs a NSData extension
let data = pdfPath ^> NSData.fileContents
let doc = data ^> CGDataProviderCreateWithCFData ^> CGPDFDocumentCreateWithProvider
//pageView.pdf is optional, nicely this function accepts the document as an optional
pageView.pdfPage = CGPDFDocumentGetPage(doc, 1)
Note that for this to work you need to add an extension to NSData, as you cannot map the init(contentsOfFile:) initializer to a generic function that can be passed to ^>.
extension NSData {
class func fileContents(path: String) -> NSData? {
return NSData(contentsOfFile: path)
}
}
The usage of the ^> operator reverts however the order you write the function names, if you prefer having the function names in the same order as the original code, you can add a reversed operator that does the same thing:
infix operator ^< {associativity right precedence 150}
func ^< <T, U>(f: T->U?, arg: T?) -> U?{
if let arg = arg {
return f(arg)
} else {
return nil
}
}
let pdfPath = NSBundle.mainBundle().pathForResource("nac_06", ofType:"pdf")
let data = NSData.fileContents ^< pdfPath
let doc = CGPDFDocumentCreateWithProvider ^< CGDataProviderCreateWithCFData ^< data
//pageView.pdf is optional, nicely this function accepts the document as an optional
pageView.pdfPage = CGPDFDocumentGetPage(doc, 1)