I'm trying to retrieve data from a local JSON file like this:
[{
"name": "John",
"lastname": "Doe",
"age": "30"
},
{
"name": "Jane",
"lastname": "Doe",
"age": "20"
}
,
{
"name": "Baby",
"lastname": "Doe",
"age": "3"
}]
The user, using a datapicker can select name and/or lastname
import SwiftUI
struct ContentView : View {
var names = ["John", "Jane", "Baby"]
var lastnames = ["Doe", "Boe"]
#State private var selectedNameItem = 0
#State private var selectedLastNameItem = 0
var body: some View {
VStack {
Picker(selection: $selectedNameItem, label: Text("Names:")) {
ForEach(0 ..< names.count) {
Text(self.names[$0]).tag($0)
}
}
Text("Your choice: ")
+ Text("\(names[selectedNameItem])")
}
VStack {
Picker(selection: $selectedLastNameItem, label: Text("LastName:")) {
ForEach(0 ..< lastnames.count) {
Text(self.lastnames[$0]).tag($0)
}
}
Text("Your choice: ")
+ Text("\(lastnames[selectedLastNameItem])")
}
}
}
Once selected the name/lastyname (as parameter) I want to show a text that said for example: "John Doe is 30 years old"
How I can read the data from JSON and return exactly the age of the user selected without list all the elements?
Thanks a lot,
Fabrizio
To start, I recommend making a struct to represent your JSON structure. Try the following:
struct Person: Codable, Hashable {
let name: String
let lastname: String
let age: String
}
typealias People = [Person]
I usually make a typealias to handle the array version of my data. Once you have defined your data structure to match your JSON, I usually extend my data to make loading from JSON easy.
extension Person {
static func load(fromJson json: URL) -> People {
guard let data = try? Data(contentsOf: json) else {
preconditionFailure("Unable to read data from URL")
}
let jsonDecoder = JSONDecoder()
var people = People()
do {
people = try jsonDecoder.decode(People.self, from: data)
} catch {
print(error)
}
return people
}
}
There are more generic ways to do this to support a wider swath of models, but for your example, this is quick and easy.
Now that you have an easy way to work with your model, an easy solution to what you have in mind could be done by extending the Array type like so:
extension Array where Element == Person {
func retrievePeople(byName name: String) -> People {
return self.filter { $0.name == name }
}
func retrievePeople(byLastName lastname: String) -> People {
return self.filter { $0.lastname == lastname }
}
func retrievePeople(byAge age: String) -> People {
return self.filter { $0.age == age }
}
}
This will allow you to query the entire range of objects by any of the elements and in turn, return the array of matches. If you're certain that there's only one return, you could use the following code to get the first element:
// Let's assume your array of people is stored in this variable
var myPeople: People
if let person = myPeople.retrievePeople(byName: "John").first {
// Do the things you want with this person object here
}
The nice thing about this style of loading/working with data is that it's easy to generate quickly and it will support returning 0 objects, 1 object, and multiple objects. This will also allow you to move the model over to use the features of SwiftUI/Combine (which is what it looks like you're hoping to do above).
Related
In my new project that act like a RPC, in some moment i receive a JSON with function name and a list of parameters. Example:
{
"f": "sample.login",
"p": [
{"n": "param1", "v": "value1"},
{"n": "param2", "v": true},
{"n": "param3", "v": {"id": 1, "title": "title xyz"}}
[...] any amount of params [...]
]
}
In other moments, i need create the same structure and encode as JSON. Example:
public class Param: Codable {
public var n: String
public var v: Any?
init(n: String, v: Any?) {
self.n = n
self.v = v
}
}
struct Todo: Codable {
var id: Int64
var title: String
var data: [String: String]
var done: Bool
}
public class JsonSerializer: Serializer {
private var decoder = JSONDecoder()
private var encoder = JSONEncoder()
public func encodeRequest(functionName: String, params: [Param]) -> String {
do {
let request = JsonRequestData(f: functionName, p: params)
let data = try encoder.encode(request)
if let result = String(data: data, encoding: .utf8) {
return result
} else {
print("[JsonSerializer : encodeRequest] Error when try to encode data")
}
} catch let e {
print("[JsonSerializer : encodeRequest] Error when try to encode data: \(e.localizedDescription)")
}
return ""
}
struct JsonRequestData: Codable {
let f: String
var p: [Param]
init(f: String, p: [Param]) {
self.f = f
self.p = p
}
}
}
let todo = Todo(id: 1, title: "Title 1", data: [:], done: true)
let name = "sample.todo.single"
var params: [Param] = []
params.append(Param(n: "suffix", v: "%"))
params.append(Param(n: "p2", v: todo))
let s = JsonSerializer()
let json = s.encodeRequest(functionName: name, params: params)
print(json)
I made it work in C++ (nlohmann json) and Kotlin (with gson). Only left make it work in Swift.
I know of course Swift doesn't support encoding ANY type. And I'm aware of some limitations on this in Swift.
But I would like to find a plausible solution to my problem.
Even if the user has to implement a protocol on his side for his types, or enter his type in a list of known types or something.
The project is at this URL, if you want to see the codes in more depth:
https://github.com/xplpc/xplpc
Removing this lock, the code is practically ready.
I tried on Apple forums, search on Google and on iOS group inside Slack.
Thanks for answers.
But after try a lot, i decide to use AnyCodable project (https://github.com/Flight-School/AnyCodable) with my modifications (https://github.com/xplpc/xplpc/tree/main/swift/lib/Sources).
AnyCodable let me use all swift types and if i use these types on my class/struct it works without problems.
To use any custom type, only need add more lines on AnyEncodable and AnyDecodable class.
Thanks.
Its seems that I cannot extract all the values from from JSON. I can see the output just fine but I cannot put it in a array to present it in labels.
What is it that Im doing wrong?
struct EfectivO: Codable {
public var counted: Int
public var registered: Int
}
struct TotalsByPaymentType: Codable {
public var Efectivo: EfectivO
}
struct Results: Codable {
var locale: String
var date: String
var totalsByPaymentType: TotalsByPaymentType
}
struct Loan: Codable {
var results: [Results]
var petitions: [Loan] = []
}
do {
let courses = try JSONDecoder().decode(Loan.self, from: data)
for item in courses.results {
self.petitions.append(courses.self)
print(item)
}
return
} catch let jsonErr {
print("Error serializing json:", jsonErr)
return
}
I havent post the raw JSON but I can see that there correct response.
This is actual JSON:
{
"results": [
{
"locale": "GB",
"date": "2020-01-29",
"totalsByPaymentType": {
"Efectivo": {
"counted": 108130,
"registered": 106135
},
"Tarjeta de Credito": {
"counted": 209720,
"registered": 209720
},
"Cheque Gourmet": {
"counted": 1800,
"registered": 1800
},
"Ticket Restaurantes": {
"counted": 3800,
"registered": 3800
},
"Resto-In": {
"counted": 0,
"registered": 0
},
"Sodexo": {
"counted": 1921,
"registered": 1921
},
"Friend": {
"counted": 0,
"registered": 0
}
}
}
]
}
As you can seem its very complicated structure and I have a nice response.
The root is has just a single array that is [Loan]
- turns out this is not what you have. Your JSON is a top-level object with one property results, which contains an array of objects. Also, the data structure does not look like it's recursive, as the data model in your question suggests.
Given the example JSON, you could either name each of the results explicitly, like so:
struct Results: Codable {
let results: [Result]
}
struct Result: Codable {
let locale, date: String
let totalsByPaymentType: TotalsByPaymentType
}
struct TotalsByPaymentType: Codable {
let efectivo, tarjetaDeCredito, chequeGourmet, ticketRestaurantes,
restoIn, sodexo, friend: Total
enum CodingKeys: String, CodingKey {
case efectivo = "Efectivo"
case tarjetaDeCredito = "Tarjeta de Credito"
case chequeGourmet = "Cheque Gourmet"
case ticketRestaurantes = "Ticket Restaurantes"
case restoIn = "Resto-In"
case sodexo = "Sodexo"
case friend = "Friend"
}
}
struct Total: Codable {
let counted, registered: Int
}
or collect all the payment totals in a dictionary:
struct Results: Codable {
let results: [Result]
}
struct Result: Codable {
let locale, date: String
let totalsByPaymentType: [String: Total]
}
Which approach to use depends on how dynamic the "payment types" values change. Is it a fixed list? Then go for properties. Does is change every couple of weeks or months? Then use a dictionary.
In any case, you parse that using
do {
let results = try JSONDecoder().decode(Results.self, from: data)
for result in results.results {
// handle each result
}
} catch {
print(error)
}
(NB you should probably avoid generic names like Result in this case, but I don't understand your use case well enough to make appropriate suggestions)
Imagine that I have following json in Data format
var data = Data("""
{
"name": "Steve",
"age": 30,
"pets": [
"dog"
]
}
""".utf8)
How can I change let's say first element of the pets to "cat" with keeping my data variable?
First you need struct conforming to Codable matching to your json
struct Person: Codable {
var name: String
var age: Int
var pets: [String]
}
then you can use this generic method which takes object in Data type, decodes it and changes it how you declare later in changeBlock closure. Then it encodes it back
extension Data {
mutating func update<T: Codable>(changeBlock: (inout T) -> Void) throws {
var decoded = try JSONDecoder().decode(T.self, from: self)
changeBlock(&decoded)
self = try JSONEncoder().encode(decoded)
}
}
Usage:
do {
try data.update { (person: inout Person) -> Void in
person.pets[0] = "cat"
}
} catch { print(error) }
//print(String(data: data, encoding: .utf8))
I have JSON like this:
{
"success": true,
"message": "",
"result": {
"buy": [
{
"Quantity": 0.0056,
"Rate": 18527
},
{
"Quantity": 0.11431426,
"Rate": 18526
}
],
"sell":[
{
"Quantity": 8.20604116,
"Rate": 18540
},
{
"Quantity": 0.95600491,
"Rate": 18574.99999998
}
]
}
}
and another set of JSON like this:
{
"lastUpdateId": 1027024,
"bids": [
[
"4.00000000", // PRICE
"431.00000000", // QTY
[] // Can be ignored
]
],
"asks": [
[
"4.00000200",
"12.00000000",
[]
]
]
}
What is the best way to parse these two responses using codable. They both need to be parsed using the same struct or need to be converted to the same struct (whatever will do the job faster). I don't want to create a struct for the entire first response because I am not going to use keys like "success" and "message". I basically want to ignore those and get directly to the "result" key But in the second response, I will being using all the data so I have created a struct for that called TotalOrderBook. What is the best way to do this?
What is confusing me is ignoring the keys "success" and "message" in the first JSON response and getting straight to the value for the key "result". Is it possible to do that without creating an additional struct?
This is what I have right now. I would like to avoid adding another struct since the only thing I really need is the values under buy/bid and sell/sell.
struct TotalOrderBook:Codable{
var buy:[UniversalOrder]?
var sell:[UniversalOrder]?
var bid:[UniversalOrder]?
var ask:[UniversalOrder]?
var buyOrderBook: [UniversalOrder] {
return bid ?? buy ?? [UniversalOrder]()
}
var sellOrderBook: [UniversalOrder] {
return ask ?? sell ?? [UniversalOrder]()
}
var updatedTime:Date
}
struct UniversalOrder:Codable{
var price : Double {
return Double(rate ?? binPrice ?? 0)
}
var size : Double {
return Double(quantity ?? binQuantity ?? 0 )
}
//let numOrders : Int
var quantity:Double?
var rate:Double?
var binPrice:Double?
var binQuantity:Double?
private enum CodingKeys: String, CodingKey {
case rate = "Rate"
case quantity = "Quantity"
//case numOrders, binPrice,
}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
binPrice = Double(try container.decode(String.self)) ?? nil
binQuantity = Double(try container.decode(String.self)) ?? nil
quantity = nil
rate = nil
}
}
This is how I am decoding:
let decoder = JSONDecoder()
let data = try! JSONSerialization.data(withJSONObject: value) //value is the response from Alamofire
var theWholeOrderBook:UniversalOrder!
do {
theWholeOrderBook = try decoder.decode(UniversalOrder.self, from: data)
} catch let error {
//print ("error is \(e) ** \(value)")
}
To answer your questions directly, yes it is very easy to ignore the success and message key-value pairs and head straight to results.
Despite this it will be a bit complicated to have a single struct to parse both of these JSON responses.
Both of them have a very different structure which will make it easier to use two different structs to use encoding. To highlight some differences :
buy, sell are nested inside results. bids, asks aren't.
The keys are completely different.
buy, sell have an array of key-value pairs while bids, asks simple have an array of values.
Codable structs should be simple and clear. It's better to have two of those corresponding to each response.
Using Alamofire and SwiftyJSON to retrieve some JSON is trivial:
Given JSON such as
{
"results": [
{
"id": "123",
"name": "Bob"
},
{
"id": "456",
"name": "Sally"
}
}
This function will work:
func loadSomeJSONData() {
Alamofire.request(.GET, "http://example.com/json/")
.responseJSON { (_, _, data, _) in
let json = JSON(data!)
if let firstName = json["results"][0]["name"].string {
println("first name: \(firstName)") // firstName will equal "Bob"
}
}
}
All well and good. My problem arises when I need to load JSON from a paged API, that is, when the data is collected from multiple calls to an API endpoint, where the JSON looks more like:
{
"currentPage": "1",
"totalPages": "6"
"results": [
{
"id": "123",
"name": "Bob"
},
{
"id": "456",
"name": "Sally"
}
]
}
and then the next block would look like:
{
"currentPage": "2",
"totalPages": "6"
"results": [
{
"id": "789",
"name": "Fred"
},
{
"id": "012",
"name": "Jane"
}
]
}
In this case, I can recursively call a function to gather all the "pages" but I'm not sure how to put all the JSON fragments together properly:
func loadSomeJSONDataFromPagedEndPoint(page : Int = 1) {
Alamofire.request(.GET, "http://example.com/json/" + page)
.responseJSON { (_, _, data, _) in
let json = JSON(data!)
if let totalPages = json["totalPages"].description.toInt() {
if let currentPage = json["currentPage"].description.toInt() {
let pageOfJSON = json["results"]
// add pageOfJSON to allJSON somehow??
if currentPage < totalPages {
self.loadSomeJSONDataFromPagedEndPoint(page: currentPage+1)
} else {
// done loading all JSON pages
}
}
}
var allJSON
loadSomeJSONDataFromPagedEndPoint()
What I'd like to happen is to have the "results" portion of each JSON response eventually collected into a single array of objects (the { "id": "123", "name": "Bob"} objects)
Bonus question: I'm not sure why I need to do json["totalPages"].description.toInt() in order to get the value of totalPages, there must be a better way?
You have several questions in here, so let's take them one at a time.
I can't tell from your post if you get valid JSON back for each page call or whether you need to put them altogether to complete the JSON. So let's walk through both cases.
Option 1 - Valid JSON from each Page
You're already very close, you just need to tweak your JSON parsing a bit and store the results. Here's what this could look like.
class PagedDownloader {
var pagedResults = [AnyObject]()
func loadSomeJSONDataFromPagedEndPoint(page: Int) {
let request = Alamofire.request(.GET, "http://example.com/json/\(page)")
request.responseJSON { [weak self] _, _, jsonData, _ in
if let strongSelf = self {
let json = JSON(jsonData!)
let totalPages = json["totalPages"].stringValue.toInt()!
let currentPage = json["currentPage"].stringValue.toInt()!
let results = json["results"].arrayObject!
strongSelf.pagedResults += results
if currentPage < totalPages {
strongSelf.loadSomeJSONDataFromPagedEndPoint(currentPage + 1)
} else {
strongSelf.parsePagedResults()
}
}
}
}
func parsePagedResults() {
let json = JSON(pagedResults)
println(json)
}
}
You seem to know your way around SwiftyJSON so I'll let you handle the parsePagedResults implementation.
Option 2 - Pages must be assembled to create valid JSON
Paging JSON
First off, you can't parse partial JSON, it just won't work. The NSJSONSerialization will fail. This means that you can't use the responseJSON serializer with paged JSON because data will always be nil and error will always be the json serialization error. Long story short, you need cache all your data until it's valid JSON, then you can parse.
Storing Paged JSON
If you're going to store it, this is what it could look like as a simple example without Alamofire getting in the mix.
class Pager {
let page1 = "{\"currentPage\":\"1\",\"totalPages\":\"3\",\"results\":[{\"id\":\"123\",\"name\":\"Bob\"},"
let page2 = "{\"id\":\"456\",\"name\":\"Sally\"},{\"id\":\"234\",\"name\":\"Christian\"},"
let page3 = "{\"id\":\"567\",\"name\":\"Jerry\"},{\"id\":\"345\",\"name\":\"John\"}]}"
let pages: [String]
let jsonData: NSMutableData
init() {
self.pages = [page1, page2, page3]
self.jsonData = NSMutableData()
}
func downloadPages() {
for (index, page) in enumerate(pages) {
jsonData.appendData(page.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!)
}
let json = JSON(data: jsonData)
println(json)
if let totalPages = json["totalPages"].string?.toInt() {
println("Total Pages Value: \(totalPages)")
}
}
}
Your bonus question is answered at the end of that code chunk. You don't want to use description from SwiftyJSON, but instead the string optional cast and then optional chain into the toInt method.
Paging and Storing with Alamofire
Now that you have a simple example of how to write the JSON pages into data chunks, let's look at how that same approach could be used with the response serializer in Alamofire.
class Downloader {
var jsonData = NSMutableData()
var totalPagesDownloaded = 0
let totalPagesToDownload = 6
func loadSomeJSONDataFromPagedEndPoint() {
for page in 1...self.totalPagesToDownload {
let request = Alamofire.request(.GET, "http://example.com/json/\(page)")
request.response { [weak self] _, _, data, _ in
if let strongSelf = self {
strongSelf.jsonData.appendData(data as NSData)
++strongSelf.totalPagesDownloaded
if strongSelf.totalPagesDownloaded == strongSelf.totalPagesToDownload {
strongSelf.parseJSONData()
}
}
}
}
}
func parseJSONData() {
let json = JSON(data: jsonData)
println(json)
}
}
Parsing the Resulting JSON with SwiftyJSON
Inside the parseJSONData function, just use all the awesome features of SwiftyJSON to parse out the values you need.
I'm pretty sure that covers all your possible use cases and questions. Hope that helps!