I have code that get weather api but information from "weather" prints "id" and "main" information in console and also in text label with brackets in console
(
250
)
Here is some relevant code:
if let weather = jsonObj.value(forKey: "weather") as? NSArray {
let idObject = weather.value(forKey: "id")
// DispatchQueue.main.sync {
// self.weatherLabel.text = idObject as? String }
print (idObject)
let mainObject = weather.value(forKey: "main")
print(mainObject as Any)
}
Below, in the second half of this answer, I’ll describe what the issue is in your code snippet. But you are using Swift, so you shouldn’t be using NSDictionary and NSArray at all. You should use JSONDecoder to parse your json, and deal solely with Swift objects.
So you might define structures like so:
struct WeatherReport: Decodable {
let id: Int
let main: String
}
struct ResponseObject : Decodable {
let weather: [WeatherReport]
}
And decode it like so:
do {
let responseObject = try JSONDecoder().decode(ResponseObject.self, from: data)
for weatherReport in responseObject.weather {
print(weatherReport.id)
}
} catch {
print(error)
}
Now, my definition of WeatherReport is undoubtedly incorrect, as it needs to match your JSON structure, which you haven’t shared with us. For example, I’m assuming your id values are numeric (no quotes around them; we can’t tell by looking at your output). Also, you should check the documentation and figure out what the JSON looks like if there was an error. So you’d have to check that. But hopefully this illustrates the idea.
Regarding why you’re seeing the ( and ) around your values, that’s because the objects you are printing are, themselves, NSArray objects. When you call value(forKey:) on a NSArray of NSDictionary objects, it will go through the array and grab the value associated with that key for each dictionary in your array, and will return an array of values. Hence the ( and ) in the output. Sure, your “array of dictionaries” appears to currently have only one dictionary in it, but the fact that it is an array suggests that it’s possible that weather can possibly contain multiple dictionaries.
Consider this over-simplified version of your JSON:
{
"weather": [
{
"id": 42,
"main": "Rain"
},
{
"id": 520
"main": "Cloudy"
}
]
}
Thus
if let weather = jsonObj.value(forKey: "weather") as? NSArray {
let identifiers = weather.value(forKey: "id")
print (identifiers)
}
Will output
(
42,
520
)
Now, your array of dictionaries obviously only has one dictionary at this point, but this illustrates what’s going on.
If you don’t want the brackets, get the firstObject from the array:
guard
let weather = jsonObj.value(forKey: "weather") as? NSArray,
let weatherReport = weather.firstObject as? NSDictionary
else {
return
}
if let idObject = weatherReport.value(forKey: "id") as? Int {
print(idObject)
}
Or, if you want to print all of them:
guard
let weather = jsonObj.value(forKey: "weather") as? NSArray
else {
return
}
for object in array {
if let weatherReport = weather as? NSDictionary, let identifier = weatherReport["id"] as? Int {
print(identifier)
}
}
This is how one navigates a NSArray of NSDictionary objects. But, as described at the beginning of my answer, you shouldn’t be using these NS types at all. Get your raw Data, and decode it to model object types using JSONDecoder, instead.
Related
I am downloading data from Firebase and storing into UserDefaults. I am encoding and decoding it properly, and I can retrieve the data once its decoded. The problem I am stuck on is how to "return" any of the save values when needed. The struct I used to store that data contains 3 different types of value types: Bool String and Int.
There are parts of my app where I am going to need to retrieve any one of the different value types, depending on the scenario. I wrote a function that I was hoping to use to pull data from the decoded object when needed. Since the data is already saved as the correct value type. I can't seem to find a way to make the "return" change or be agnostic depending on the value type. I did some reading on Generics in Swift, but I am still unsure if that is the right solution. I have outlined my code below to explain further. Any suggestions or solutions would be much appreciated.
This function below is what I am referring to where I am stuck. I put the return type as Any, to experiment and see what would happen. Its not working right since the data is already formatted with the correct type so when I retrieve it using Any it would need me to convert it back to a String Bool or Int. Additionally, the data: UserData is an enum I made to allow selection, so that I could use dot notation to select whichever item I would want.
static func getUserInfo(data: UserData) -> Any { **// <- Return type here, also data is an enum below**
if let savedPerson = defaults.object(forKey: "userInfo") as? Data {
let decoder = JSONDecoder()
if let specificUser = try? decoder.decode(User.self, from: savedPerson) {
switch data {
case .number:
return specificUser.mobile
case .name:
return specificUser.mobile
case .pictureUrl:
return specificUser.pictureUrl!
case .accountType:
return specificUser.accountType
case .isEmailVarified:
return specificUser.isEmailVarified
default:
break
}
}
}
return ""
}
Here is the UserData enum:
enum UserData {
case number
case name
case pictureUrl
case accountType
case isEmailVarified
}
Here is the command I was intended to use to "retrieve" whatever option I wanted
let number = SaveToDefaults.getUserInfo(data: .mobile) as! String
myLabel.text = number
EDIT:
Here is the original struct that that all of the data is retrieved from Firebase with and then encoded into UserDefaults
struct User: Encodable, Decodable {
var uid: String
var name: String
var email: String
var mobile: String
var pictureUrl: String?
var accountType: AccountType!
var token: String?
var stripeId: String?
var pending: PendingStatus?
var isEmailVarified: Bool?
init(uid: String, dictionary: [String: Any]) {
self.uid = uid
self.name = dictionary["name"] as? String ?? ""
self.email = dictionary["email"] as? String ?? ""
self.mobile = dictionary["mobile"] as? String ?? ""
self.pictureUrl = dictionary["pictureUrl"] as? String ?? ""
if let index = dictionary["accountType"] as? Int {
self.accountType = AccountType(rawValue: index)
}
self.token = dictionary["token"] as? String ?? ""
self.stripeId = dictionary["stripe_id"] as? String ?? ""
if let pendingUser = dictionary["pendingUser"] as? Int {
self.pending= PendingStatus(rawValue: accountonline)
}
if let driverstatus = dictionary["isDriverPending"] as? Int {
self.driverPendingStatus = DriverStatus(rawValue: driverstatus)
}
self.isEmailVerified = false
}
}
There are some languages, like PHP, which let you return any data type in a single function. Generally speaking, languages that allow this get a bad reputation for maintainability.
That disclaimer out of the way:
If you’re really just displaying the result as a String on the screen, there’s probably nothing inherently evil in using your case statement to cast each value as a string and return that.
E.g.
case .isEmailVarified:
return String(specificUser.isEmailVarified) // EDITED to include a non-String variable after you EDITED your question to include struct.
EDIT:
I agree with the other commenters who say passing the Struct around instead of individual values is a good thing. One trick you could try, assuming you can target iOS 14, is to take advantage of the new #AppStorage construct instead of userDefaults. This might just be personal preference on my part, but the below feels pretty legible.
#AppStorage("user") var user = ""
// to persist your object as JSON string
let u = try! JSONEncoder().encode(userObjectFromFirebase)
result = String(data: u, encoding: .utf8) ?? ""
self.user = result
// to fetch your object as User object
let u = try! JSONDecoder().decode(User.self, from: self.user.data(using: .utf8)!)
// reference your values
myLabel.text = u.number
myLabel2.text = String(u.isEmailVerified)
I have successfully parsed json data using URLSession and now I want to add the parsed data to an array. Doing this using an ordinary array works fine, but I'm learning Rx and thus want to use a subject.
So, this works:
var parsedJson = [Employees]()
self.parsedJson = decodedJson.people
But this gives an error:
var parsedJson: PublishSubject<[Employees]> = PublishSubject<[Employees]>()
self.parsedJson = decodedJson.people
Cannot assign value of type '[Employees]' to type 'PublishSubject<[Employees]>'
Here is the URLSession code:
// var parsedJson = [Employees]()
var parsedJson: PublishSubject<[Employees]> = PublishSubject<[Employees]>()
func getJSON(completion: #escaping () -> Void) {
guard let url = URL(string:"https://api.myjson.com/bins/jmos6") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
do {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
jsonDecoder.dateDecodingStrategy = .iso8601
let decodedJson = try jsonDecoder.decode(People.self, from: data)
self.parsedJson = decodedJson.people
completion()
} catch {
print(error)
}
}.resume()
}
Anyone know how to do this and why there is an error in the first place? Doesn't the <> simply indicate which type should be observed? Didn't get .accept() to work either.
EDIT
let parsedJson: BehaviorRelay<[Employees]> = BehaviorRelay(value: [])
self.parsedJson.accept(decodedJson.people)
This worked, but what is the equivalent to BehaviorSubject and PublishSubjuct?
The error message is pretty clear: you have a type-mismatch. You would get the same error message if you tried to assign a String to an Int variable, for example. A PublishSubject is not an array. Its a mechanism (think of it as a pipeline) for sending a stream of certain types of values (here an array of Employees).
You typically use Subjects by subscribing to them like so:
var parsedJson = PublishSubject<[Employee]>()
// the 'next' block will fire every time an array of employees is sent through the pipeline
parsedJson.next { [weak self] employees in
print(employees)
}
The above next block will fire every time you send an array through the PublishSubject like so:
let decodedJson = try jsonDecoder.decode(People.self, from: data)
self.parsedJson.onNext(decodedJson.people)
From your EDIT it seems that you moved on to trying to use a BehaviorRelay. I would recommend reading up on the differences between these two classes before decided which is appropriate for your use case. This article was really helpful to me when trying to learn the differences between the different types of Subjects and Relays: https://medium.com/#dimitriskalaitzidis/rxswift-subjects-a2c9ff32a185
Good luck!
Try
self.parsedJSON.onNext(decodedJson.people)
I have a string in Swift that contains an array in it. Is it possible to convert the string into an array? All I have found on the internet is converting "abc" to ["a","b","c"], which I would not like to do.
String: "[\"value1\",\"value2\",\"value3\"]"
Result: ["value1","value2","value3"]
I am getting the string from a web request. The code for the request is here:
func webRequest(uri:String)->String{
var value = "";
let request = URLRequest(url: NSURL(string: uri)! as URL)
do {
let response: AutoreleasingUnsafeMutablePointer<URLResponse?>? = nil
let data = try NSURLConnection.sendSynchronousRequest(request, returning: response)
value = String(data: data, encoding: .utf8)!;
} catch _ {
}
return value;
}
First off, the problem here is not converting your string into an array. The problem is getting the array from the web request in the first place.
Let me update your web request function.
func webRequest(url: URL, completion: ([String]?) -> () { // I have updated this function to be asynchronous
let dataTask = URLSession.shared.dataTask(with: url) {
data, urlResponse, error in
// you might want to add more code in here to check the data is valid etc...
guard let data = data,
let arrayOfStrings = JSONDecoder().decode([String].self, from: data) else {
// something went wrong getting the array of strings so return nil here...
completion(nil)
return
}
completion(arrayOfStrings)
}
dataTask.resume()
}
Using this code instead of the code in your question you now have an asynchronous function that will not block the app and one that will pass your array of strings into the completion.
You can now run it like this...
webRequest(url: someURL) { strings in
guard let strings = strings else {
// strings is nil because something went wrong with the web request
return
}
print(strings)
}
Creating the URL
In your question you have this code... NSURL(string: someString)! as URL
You can change this to... let url = URL(string: someString)
Quick side note
Careful where you find tutorials and using code you find on the web. The code used in this question is very old. (at least 4 or 5 years "out of date").
If you're looking for tutorials to help with Swift then some recommendations are...
Ray Wenderlich
Hacking with swift
I have an empty global array. The only simple thing I want to do is add an element to this array. It seems in swift this seemingly simple task is proving to be difficult. I am just left with an empty array and nothing is appending to my global array.
I can see that it prints out values in the for loop. So the values are actually there.
This is some stuff I have declared globally (Yes, I know global variables are bad but I will sort that out later):
struct HouseDetails: Decodable {
let median_price: String
let sale_year: String
let transaction_count: String
let type: String
}
var hsArray: [HouseDetails] = []
and in the viewDidLoad() function I have the data which I am storing in local variable "houses". When I loop through the array it prints median_price, showing that the values are there.
However when I do hsArray.append(h) it seems to do nothing.
let jsonUrlString = "https://data.melbourne.vic.gov.au/resource/i8px-csib.json"
guard let url = URL(string: jsonUrlString)
else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let houses = try JSONDecoder().decode([HouseDetails].self, from: data)
for h in houses {
hsArray.append(h)
print(h.median_price)
}
}
catch let jsonErr {
print("Error with json serialization", jsonErr)
}
}.resume()
Thank you for any help. In other languages I am used to being able to append an element to the end of an existing array, so I am sure it is just a small error.
Firstly, why don't you simply do
hsArray.append(contentsOf: houses)
instead of all that for loop
for h in houses {
hsArray.append(h)
print(h.median_price)
}
The issue might be the time at which you are using hsArray. See if the response is received after you use hsArray.
I am trying to fetch records in Core Data.
func fetchOrg() {
var **internalOrganization** = [InternalOrganizationMO]() //NSManagedClass
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "InternalOrganization")
fetchRequest.returnsObjectsAsFaults = false
do {
let result = try managedObjectContext.fetch(fetchRequest) as **internalOrganization** **////Compiler flags Error here**
} catch {
fatalError("Failed to fetch internal organization \(error)")
}
}
InternalOrganizationMO is a ManagedObject Class corresponding to the object model and it seems clear to me that internalOrganization is declared to be an array of those objects, so the flagged error seems to be off. My understanding is that this is the kind of object that is supposed to be the target of a fetch, but I am definitely on the learning curve here.
Is it that the fetch needs to be targeted at a Type instead of an array--thus, the complaint about my not providing a named type? If that is it, am I simply supposed to provide the ManagedObject? If that is so, how on earth do I determine how many records are returned?
Is this really better than just using the interface to SQLite?
Thanks, sorry for the rant.
You typecast objects as types, not objects as objects.
Example:
let a = b as! [String]
and not:
let a = [String]()
let c = b as! a
Solution #1:
Change NSFetchRequest<NSFetchRequestResult> to specify the type to be explicitly InternalOrganizationMO, like so:
NSFetchRequestResult<InternalOrganizationMO>
This fetchRequest has now the proper associated type InternalOrganizationMO and will be used accordingly to return objects of this type.
You then won't need to typecast result again and the following code should work just fine:
func fetchOrg() {
let fetchRequest = NSFetchRequest<InternalOrganizationMO>(entityName: "InternalOrganization")
do {
let internalOrganization = try managedContext.fetch(fetchRequest)
/*
internalOrganization will be of type [InternalOrganizationMO]
as that is the return type of the fetch now.
*/
//handle internalOrganization here (within the do block)
print(internalOrganization.count)
}
catch {
fatalError("Failed to fetch internal organization \(error)")
}
}
Solution #2:
If you want this method to work even if the fetchRequest try or typecasting fails then you can do this:
func fetchOrg() {
var internalOrganization = [InternalOrganizationMO]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "InternalOrganization")
do {
internalOrganization = try managedContext.fetch(fetchRequest) as? [InternalOrganizationMO]
/*
You ofcourse wouldn't want the above optional binding to fail but even if it
does, atleast your method can stay consistent and continue with an empty array
*/
}
catch {
fatalError("Failed to fetch internal organization \(error)")
}
//handle internalOrganization here
print(internalOrganization.count)
}
The choice of solution, depends on your design and requirements.