How to sort an array by a specific key - arrays

I am using swapi.co as my source of json data and my response looks like the following: https://swapi.co/api/people/
My array of "characters" has the structure
// MARK: - CharactersResult
struct CharactersResult: Codable {
let name, height, mass, hairColor: String
let skinColor, eyeColor, birthYear: String
let gender: Gender
let homeworld: String
let films, species, vehicles, starships: [String]
let created, edited: String
let url: String
enum CodingKeys: String, CodingKey {
case name, height, mass
case hairColor = "hair_color"
case skinColor = "skin_color"
case eyeColor = "eye_color"
case birthYear = "birth_year"
case gender, homeworld, films, species, vehicles, starships, created, edited, url
}
}
I would like to get the smallest character and the largest out of my array. My function is:
func getCharacters(urlToGet: String){
do{
if let url = URL(string: urlToGet) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let jsonCharacters = try JSONDecoder().decode(Characters.self, from: data)
self.characters = self.characters + jsonCharacters.results
self.nextPageUrlForCharacters = jsonCharacters.next
self.updatePicker()
} catch let error {
print(error)
}
}
}.resume()
}
}
}
My main question is where to do the sorting and what is the most efficient way to get the smallest and the largest character.

Assuming smallest and largest is related to the height of the character (then shortest and tallest are more appropriate), sort the array by that struct member. As the value is String you have to add the .numeric option.
The descending order starts with the largest value
let sortedCharacters = self.characters.sorted{$0.height.compare($1.height, options: .numeric) == .orderedDescending}
let tallestCharacter = sortedCharacters.first
let shortestCharacter = sortedCharacters.last
Side note: You can get rid of the CodingKeys if you add the convertFromSnakeCase key decoding strategy.

This simplified version of your code illustrates how to sort by an arbitrary key, as long as that key is Comparable (so I've made your height and mass properties both Ints rather than String):
struct MovieCharacter: Codable {
let name: String
let height: Int
let mass: Int
}
let fred = MovieCharacter(name: "Fred", height: 123, mass: 99)
let wilma = MovieCharacter(name: "Wilma", height: 158, mass: 47)
let chars = [fred, wilma]
let heaviestToLightest = chars.sorted { $0.mass > $1.mass }
// Prints "Fred, Wilma"
print(heaviestToLightest.map { $0.name }.joined(separator: ", "))
let tallestToShortest = chars.sorted { $0.height > $1.height }
// prints "Wilma, Fred"
print(tallestToShortest.map { $0.name }.joined(separator: ", "))
To change the sort order, reverse the >' in the comparison closure. If you want to know the "most" and "least" of a particular order, use.firstand.last` on the result.
Also, to save yourself from having to maintain the CodingKeys enum, use .keyDecodingStrategy of a JSONDecoder:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let characters = try decoder.decode(Characters.self, from: jsonData)

Related

Build a json array from multiple properties of a custom type in Swift

After being pointed towards using Codable to format json in Swift, I am trying to add multiple results to a json array. I am unfortunately struggling to work out how to build this json array for a set of results that contains multiple properties.
Here is an example of my required json array for two 'sets' of results (N.B. the number of sets can vary in the data recorded):
[{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"orientation_forward","value":"2"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"start_forward","value":"3.45"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"finish_forward","value":"5.29"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"minimum_forward","value":"7.81"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"maximum_forward","value":"9.20"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"1","field_name":"range_forward","value":"2.39"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"orientation_forward","value":"1"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"start_forward","value":"1.89"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"finish_forward","value":"4.20"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"minimum_forward","value":"8.26"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"maximum_forward","value":"46.82"},
{"record":"10","redcap_repeat_instrument":"range_of_motion_result","redcap_repeat_instance":"2","field_name":"range_forward","value":"2.98"}]
And below is my Swift code. The results arrive as an argument within an array of custom type ORKRangeOfMotionResult (from ResearchKit with each set of results giving me a value for orientation, start, finish, minumum, maximum and range properties). Each set of ORKRangeOfMotionResult is also accompanied by an identifier (variableIdentifier), pulled in as another result that I can use to ensure I know which set is which.
I am currently using a loop to extract individual values for each ORKRangeOfMotionResult property. After this, it is relatively easy to add one result to the json array [items] using the variableIdentifier in if statements (as can be seen below for start) but I don't know how to add all properties from a set of results to the array. I'm guessing a loop, but haven't figured out how as yet.
func buildJsonArrayOfResults(_ rangeOfMotionResults: [ORKRangeOfMotionResult], withIdentifier variableIdentifiers: [String]) {
let record = getIDNumber().stringValue
let repeat_instance = getRepeatInstance().stringValue
let repeat_instrument = "range_of_motion_result"
let event = getEventName()
var fieldName: String?
var identifier: String?
var stringData: String?
var value: String?
var items: [Item] = []
struct Item: Codable {
let record: String
let fieldName: String
let repeatInstance: String
let repeatInstrument: String
let value: String
let event: String
enum CodingKeys: String, CodingKey {
case record
case fieldName = "field_name"
case repeatInstance = "redcap_repeat_instance"
case repeatInstrument = "redcap_repeat_instrument"
case value
case event = "redcap_event_name"
}
}
var setNumber: Int = 0
while setNumber < rangeOfMotionResults.count {
setNumber += 1
// obtain results for each set of movements as a string
let orientation = String(rangeOfMotionResults[setNumber - 1].orientation)
let start = String(rangeOfMotionResults[setNumber - 1].start)
let finish = String(rangeOfMotionResults[setNumber - 1].finish)
let minimum = String(rangeOfMotionResults[setNumber - 1].minimum)
let maximum = String(rangeOfMotionResults[setNumber - 1].maximum)
let range = String(rangeOfMotionResults[setNumber - 1].range)
// assign one value and its redcap field name
identifier = variableIdentifiers[setNumber - 1]
if identifier!.contains("forward") {
fieldName = "start_forward"
value = start
} else if identifier!.contains("backward") {
fieldName = "start_backward"
value = start
} else if identifier!.contains("left.bending") {
fieldName = "start_left_bending"
value = start
} else if identifier!.contains("right.bending") {
fieldName = "start_right_bending"
value = start
} else if identifier!.contains("left.rotation") {
fieldName = "start_left_rotation"
value = start
} else if identifier!.contains("right.rotation") {
fieldName = "start_left_rotation"
value = start
}
let item = Item(record: record, fieldName: fieldName!, repeatInstance: String(repeat_instance), repeatInstrument: repeat_instrument, value: value!, event: event)
items.append(item)
}
do {
let data = try JSONEncoder().encode(items)
stringData = String(data: data, encoding: .utf8)
print(stringData!)
} catch {
print(error)
}
// now do something with stringData
}
All hep graciously received. Thank you in advance.
According to the given scenario you need only these properties
func buildJsonArrayOfResults(_ rangeOfMotionResults: [ORKRangeOfMotionResult], withIdentifier variableIdentifiers: [String]) {
let record = getIDNumber().stringValue
let repeatInstance = getRepeatInstance().stringValue
let repeatInstrument = "range_of_motion_result"
let event = getEventName()
var items: [Item] = []
struct Item: Codable { ... }
And as the order of the identifiers match the order of the result array the recommended way to enumerate an array by index and element is enumerated
for (index, result) in rangeOfMotionResults.enumerated() {
let identifier = variableIdentifiers[index]
and it seems that finish, minimum, maximum and range are unused
// obtain results for each set of movements as a string
let orientation = String(result.orientation)
let value = String(result.start)
a switch expression is swiftier than an if - else chain
let fieldName : String
switch identifier {
case let id where id.contains("forward"): fieldName = "start_forward"
case let id where id.contains("backward"): fieldName = "start_backward"
case let id where id.contains("left.bending"): fieldName = "start_left_bending"
case let id where id.contains("right.bending"): fieldName = "start_right_bending"
case let id where id.contains("left.rotation"): fieldName = "start_left_rotation"
case let id where id.contains("right.rotation"): fieldName = "start_left_rotation"
default: fieldName = "unknown"
}
let item = Item(record: record, fieldName: fieldName, repeatInstance: repeatInstance, repeatInstrument: repeatInstrument, value: value, event: event)
items.append(item)
}
do {
let data = try JSONEncoder().encode(items)
let stringData = String(data: data, encoding: .utf8)!
print(stringData)
// now do something with stringData
} catch {
print(error)
}
}
I recommend also to insert this line before the for loop
assert(rangeOfMotionResults.count == variableIdentifiers.count, "The size of rangeOfMotionResults and variableIdentifiers must be equal")
It checks if the sizes of both arrays are equal, the expression is ignored in a Release Build.
Thanks to the answer from #vadian, I was able to completely solve this by adding a new struct containing a key-value pair with an identifier (and an array of these 'trios') and then adding a second for-in loop. I'm sure my code can be optimised some more (this would be interesting to see) but I'm very pleased (and grateful) that this is now building the json array in the way I wanted. Here is my final solution:
func buildJsonArrayOfResults(_ rangeOfMotionResults: [ORKRangeOfMotionResult], withIdentifier variableIdentifiers: [String]) {
let record = getIDNumber().stringValue
let repeatInstance = getRepeatInstance().stringValue
let repeatInstrument = "range_of_motion_result"
let event = getEventName()
var fieldName: String?
var identifier: String?
var stringData: String?
var value: String?
var items: [Item] = []
struct Item: Codable {
let record: String
let fieldName: String
let repeatInstance: String
let repeatInstrument: String
let value: String
let event: String
enum CodingKeys: String, CodingKey {
case record
case fieldName = "field_name"
case repeatInstance = "redcap_repeat_instance"
case repeatInstrument = "redcap_repeat_instrument"
case value
case event = "redcap_event_name"
}
}
struct Trio {
let key: String
let value: String
let identifier: String
}
var trios = [Trio]()
assert(rangeOfMotionResults.count == variableIdentifiers.count, "The size of rangeOfMotionResults and variableIdentifiers must be equal")
for (index, result) in rangeOfMotionResults.enumerated() {
identifier = variableIdentifiers[index]
// add key-value pairs with their identifiers to array
let orientation = String(result.orientation)
trios.append(Trio(key: "orientation", value: orientation, identifier: identifier!))
let start = String(result.start)
trios.append(Trio(key: "start", value: start, identifier: identifier!))
let finish = String(result.finish)
trios.append(Trio(key: "finish", value: finish, identifier: identifier!))
let minimum = String(result.minimum)
trios.append(Trio(key: "minimum", value: minimum, identifier: identifier!))
let maximum = String(result.maximum)
trios.append(Trio(key: "maximum", value: maximum, identifier: identifier!))
let range = String(result.range)
trios.append(Trio(key: "range", value: range, identifier: identifier!))
}
for (_, result) in trios.enumerated() {
let ident = result.identifier
value = result.value
let fieldName: String
switch ident {
case let id where id.contains("forward"):
fieldName = result.key + "_forward"
case let id where id.contains("backward"):
fieldName = result.key + "_backward"
case let id where id.contains("left.bending"):
fieldName = result.key + "_left_bending"
case let id where id.contains("right.bending"):
fieldName = result.key + "_right_bending"
case let id where id.contains("left.rotation"):
fieldName = result.key + "_left_rotation"
case let id where id.contains("right.rotation"):
fieldName = result.key + "_left_rotation"
default:
fieldName = result.key + "_unknown"
}
let item = Item(record: record, fieldName: fieldName, repeatInstance: String(repeatInstance), repeatInstrument: repeatInstrument, value: value!, event: event)
items.append(item)
}
do {
let data = try JSONEncoder().encode(items)
stringData = String(data: data, encoding: .utf8)
print(stringData!)
// now do something with stringData
} catch {
print(error)
}
}

How to parse this JSON format in swift

I have this JSON format:
{
"version":"7.0.19",
"fields": ["ID","pm","age","pm_0","pm_1","pm_2","pm_3","pm_4","pm_5","pm_6","conf","pm1","pm_10","p1","p2","p3","p4","p5","p6","Humidity","Temperature","Pressure","Elevation","Type","Label","Lat","Lon","Icon","isOwner","Flags","Voc","Ozone1","Adc","CH"],
"data":[[20,0.0,1,0.0,0.0,0.0,0.0,0.0,0.0,0.0,97,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,null,null,null,1413,0,"Oakdale",40.603077,-111.83612,0,0,0,null,null,0.01,1]],
"count":11880
}
but I cannot work out how to use a Codable protocol to parse the json response.
this would be my desired model.
struct Point: Codable {
let pm2: String?
let latitude, longitude: Double?
let temp: String?
let iD: String?
enum CodingKeys: String, CodingKey {
case pm2 = "pm", temp = "Temperature", iD = "ID", latitude = "Lat", longitude = "Lon"
}
}
Here is a URL to the json
https://webbfoot.com/dataa.json
You can use Codable to parse this:
struct Response: Decodable {
let version: String
let fields: [String]
let data: [[QuantumValue?]]
let count: Int
}
enter code here
enum QuantumValue: Decodable {
case float(Float), string(String)
init(from decoder: Decoder) throws {
if let int = try? decoder.singleValueContainer().decode(Float.self) {
self = .float(float)
return
}
if let string = try? decoder.singleValueContainer().decode(String.self) {
self = .string(string)
return
}
throw QuantumError.missingValue
}
enum QuantumError:Error {
case missingValue
}
}
QuantumValue will handle both Float and String and ? will handle the null part.
This one is tricky and requires manual decoding. The principle would be to define a mapping between the fields you expect to decode and the properties of your object, then depending on the type of the property (String, Double, etc...), attempt to decode the data.
Second, since you have an array of points, you need some kind of container object to hold the array, for example:
struct Points {
var data: [Point] = []
}
First, some of your model properties don't match the type in the data, e.g. iD is a String, but the data has an Int. For simplicity, I'll redefine your model to match the data
struct Point {
var pm2: Int? = nil
var latitude: Double? = nil
var longitude: Double? = nil
var temp: Int? = nil
var iD: Int? = nil
}
Now, write the manual decoder for the parent container Points:
extension Points: Decodable {
static let mapping: [String: PartialKeyPath<Point>] = [
"pm": \Point.pm2,
"Lat": \Point.latitude,
"Lon": \Point.longitude,
"Temperature": \Point.temp,
"ID": \Point.iD
]
enum CodingKeys: CodingKey { case fields, data }
private struct Dummy: Decodable {} // see below why
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let fields = try container.decode([String].self, forKey: .fields)
var data = try container.nestedUnkeyedContainer(forKey: .data)
while !data.isAtEnd {
var row = try data.nestedUnkeyedContainer()
var point = Point()
for field in fields {
let keyPath = Points.mapping[field]
switch keyPath {
case let kp as WritableKeyPath<Point, String?>:
point[keyPath: kp] = try row.decodeIfPresent(String.self)
case let kp as WritableKeyPath<Point, Int?>:
point[keyPath: kp] = try row.decodeIfPresent(Int.self)
case let kp as WritableKeyPath<Point, Double?>:
point[keyPath: kp] = try row.decodeIfPresent(Double.self)
default:
// this is a hack to skip this value
let _ = try? row.decode(Dummy.self)
}
}
self.data.append(point)
}
}
}
Once you have that, you can decode the JSON like so:
let points = try JSONDecoder().decode(Points.self, from: jsonData)
let firstPoint = points.data[0]

Array from a jsonDecoder object

This might be a very easy question (sorry!).
I would like to links a mySQL database to a Quiz App in Swift 4.
Therefore I connected to a service.php and got the information with decodable.
How can I access this information to show in a label? Do I have to make a new Array and append the objects?
import UIKit
struct Question: Codable {
let id: String?
let frage: String?
let antwort1: String?
let antwort2: String?
let antwort3: String?
let antwort4: String?
let correct: String?
let notiz: String?
let lernsektorID: String?
let lerneinheitID: String?
let lernbereichID: String?
let schwierigkeitID: String?
let redakteur: String?
let createdAt: String?
enum CodingKeys: String, CodingKey {
case id = "ID"
case frage = "Frage"
case antwort1 = "Antwort1"
case antwort2 = "Antwort2"
case antwort3 = "Antwort3"
case antwort4 = "Antwort4"
case correct = "Correct"
case notiz = "Notiz"
case lernsektorID = "LernsektorID"
case lerneinheitID = "LerneinheitID"
case lernbereichID = "LernbereichID"
case schwierigkeitID = "SchwierigkeitID"
case redakteur = "Redakteur"
case createdAt = "created_at"
}
}
var fragen = [Question]()
let url = "https://redaktion.pflegonaut.de/service.php"
let urlObj = URL(string: url)
URLSession.shared.dataTask(with: urlObj!) { (data, response, error) in
do {
self.fragen = try JSONDecoder().decode([Question].self, from: data!)
// can I use .append() here?
// maybe in a for loop?
} catch {
print(error)
}
}.resume()
So I can use the elements like:
//
// let randomizedQuestion = fragen.frage.randomElement()
//
// questionOutlet.text = randomizedQuestion
Thanks!
// NECESSARY?
var QuestionBankJson: [QuestionJson] {
var questionListJson = [QuestionJson]()
}
No, just declare one array and name the struct simply Question
var questions = [Question]()
and assign
do {
self.questions = try JSONDecoder().decode([Question].self, from: data!)
print(self.questions[1].Frage!)
} catch {
print(error) // never print something meaningless like "we got an error"
}
Notes:
Please conform to the naming convention that variable names start with a lowercase letter.
If you are responsible for the JSON declare also the keys lowercased otherwise use CodingKeys.
Declare the struct members as much non-optional as possible.
Never print a meaningless literal string when catching Codable errors. Print always the error instance.
Use a better date string format than this arbitrary German format. UNIX time stamp, SQL date string or ISO8601 are sortable and can be even decoded to Date.

Swift appending to an array with custom objects and sorting them

I am making a collection view that shows somebody their 'feed' which includes their compliments, likes, replies, and sent. I have an object named Feed which is made of an array of compliments, an array of likes, and array of replies, and an array of sent. Every object within those smaller arrays has a date_created property.
I know how to fill a collection view based off of one array sorted by dates, but how can I load a collection view with every like, reply, compliment, and sent from four arrays based on their date sent?
Do I perhaps append every one of these objects to one array? If so, how can I then go through that array and sort everything in it?
struct Feed: Decodable {
let likes: [Like]
let replies: [Reply]
let compliments: [Compliment]
let sent: [Compliment]
}
struct Like: Decodable {
let id: Int
let user: QuickUser
let created_at: String
}
struct Reply: Decodable {
let parent: Compliment
let content: String
let created_at: String
}
struct Compliment: Decodable {
let id: Int
let content: String
let sender: QuickUser
let created_at: String
let is_liked: Bool
let recipient: QuickUser
let is_public: Bool
}
func getFeed() {
UserServices.getInboxFeed { feed in
guard let feed = feed else {
self.showOkayAlert(title: "Oops!", message: "We are having trouble loading your inbox.", completion: nil)
return
}
self.feed = feed
DispatchQueue.main.async {
//load collection view here
}
}
}
A mockup of what the collection view will look like
Well, to solve this i have made a dummy protocol that contains the shared properties they all share.
in this case it was created_at and i assume this should be in date format.
however the next step was to make an extension of Feed that contains a function
that returns the latest object that confirm to the dummy protocol that i called, it LatestAction,
it basically sort each array by date and return the first item.
in our case the latest date,
after that creates a [LatestAction] sort it again the same way and return the first item which is the LatestAction.
Observe the code below.
struct QuickUser: Codable {
}
struct Feed: Decodable {
let likes: [Like]
let replies: [Reply]
let compliments: [Compliment]
let sent: [Compliment]
}
extension Feed {
func latestAction() -> LatestAction {
let latestLike = self.likes.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestReply = self.replies.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestComp = self.compliments.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestActions: [LatestAction] = [latestReply, latestLike, latestComp].sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending })
return latestActions.first!
}
}
protocol LatestAction {
var created_at: Date { get }
}
struct Like: Decodable, LatestAction {
let id: Int
let user: QuickUser
let created_at: Date
}
struct Reply: Decodable, LatestAction {
let parent: Compliment
let content: String
let created_at: Date
}
struct Compliment: Decodable, LatestAction {
let id: Int
let content: String
let sender: QuickUser
let created_at: Date
let is_liked: Bool
let recipient: QuickUser
let is_public: Bool
}
// Simply used like this
var latest = feed.latestAction()
We are not done yet if you checked the latest type it would say LatestAction and only can access the one property in there which is created_at,
now the for the last part we are going to TypeCast the Object.
if let latest = feed.latestAction() as? Like {
// used as Like
} else if let latest = feed.latestAction() as? Compliment {
// used as Compliment
} else if let latest = feed.latestAction() as? Reply {
// used as reply
}

How can I decode a JSON response with an unknown key in Swift?

I would like to split up the data in https://blockchain.info/ticker so that each line is its own String in an array.
I'm making an app that gets the price of the selected currency. So if someone wants AUD then it will get the second string in the array and then show the price which is in the last tag.
I currently just have it downloading the json..
func reloadJson(){
if globalVariables.currencySelected == "" {
globalVariables.currencySelected = globalVariables.currencySelected + "AUD"
}
print(globalVariables.currencySelected)
if let blockchainTickerURL = URL(string: "https://blockchain.info/ticker") {
let request = NSMutableURLRequest(url: blockchainTickerURL)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
var message = ""
if error != nil {
print("error")
} else {
if let unwrappedData = data {
let dataString = NSString(data: unwrappedData, encoding: String.Encoding.utf8.rawValue)
Thats just a copy and paste of what I currently have, its not exactly formatted right.
Thanks
You should take a look at Swift4 Codable protocol.
Create a structure for the currency dictionary values that conforms to Codable with the corresponding properties:
struct Currency: Codable {
let fifteenM: Double
let last: Double
let buy: Double
let sell: Double
let symbol: String
private enum CodingKeys: String, CodingKey {
case fifteenM = "15m", last, buy, sell, symbol
}
}
To decode your JSON data you need to use JSONDecoder passing the dictionary with custom values [String: Currency] as the type to be decoded:
let url = URL(string: "https://blockchain.info/ticker")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
do {
let currencies = try JSONDecoder().decode([String: Currency].self, from: data)
if let usd = currencies["USD"] {
print("USD - 15m:", usd.fifteenM)
print("USD - last:", usd.last)
print("USD - buy:", usd.buy)
print("USD - sell:", usd.sell)
print("USD - symbol:", usd.symbol)
}
} catch { print(error) }
}.resume()
This will print
USD - 15m: 11694.03
USD - last: 11694.03
USD - buy: 11695.01
USD - sell: 11693.04
USD - symbol: $

Resources