What is the best way to make this API request in Swift? - swift

Sorry if this question is too repetitive here but I cant figured out how to made it :(
I need to send data to an API (https://www.opticutter.com/public/doc/api#introduction) my data comes from somes TextFields and this is what the API expect.
curl -H "Content-type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-X POST -d '{
"stocks" : [{
"length" : "60",
"width" : "40",
"count": "10",
"grainDirection": null
},{
"length" : "40",
"width" : "35",
"grainDirection": null
}],
"requirements" : [{
"length" : "30",
"width" : "20",
"count": "8",
"grainDirection": null
},
{
"width" : "20",
"length" : "20",
"count": "3",
"grainDirection": null
}],
"settings" : {
"kerf": "0"
}
}'
So I already have the code to make the request but I cant realize how convert the data into that.
Hope do you understand what I need.
Thanks in advance!

The best approach in my opinion is to map your data into structs/classes and use the Decodable protocol.
struct Stock: Encodable {
let length: Int
let width: Int
let count: Int
let grainDirection: Int?
}
struct Requirement: Encodable {
let length: Int
let width: Int
let count: Int
let grainDirection: Int?
}
struct Setting: Encodable {
let kerf: Int
}
struct MyObject: Encodable {
let stocks: [Stock]
let requirements: [Requirement]
let settings: [Setting]
}
Then you build the MyObject with the data from the textfields:
let myObjectData = MyObject(stocks: [
Stock(length: 60, width: 40, count: 10, grainDirection: nil),
Stock(length: 40, width: 35, count: 0, grainDirection: nil)
],
requirements: [
Requirement(length: 30, width: 20, count: 8, grainDirection: nil),
Requirement(length: 20, width: 20, count: 3, grainDirection: nil),
],
settings: [Setting(kerf: 0)])
Finally on the request you would have to encode the data, converting it into a JSON String for example:
do {
let jsonData = try JSONEncoder().encode(myObjectData)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString)
} catch {
debugPrint(error)
}
This will produce a json string with the following result:
{
"stocks":[
{
"width":40,
"count":10,
"length":60
},
{
"width":35,
"count":0,
"length":40
}
],
"requirements":[
{
"width":20,
"count":8,
"length":30
},
{
"width":20,
"count":3,
"length":20
}
],
"settings":[
{
"kerf":0
}
]
}

Related

How to decode a dictionary with a nested array inside?

I'm fetching some data from an endpoint, and the response looks like this:
{
"count": 29772,
"next": null,
"previous": null,
"results": [
{
"id": 29,
"book_name": "Book title",
"book_id": 70,
"chapter_number": 1,
"verse": "Text",
"verse_number": 20,
"chapter": 96
},
{
"id": 30,
"book_name": "Book Title",
"book_id": 70,
"chapter_number": 1,
"verse": "Lorem ipsum",
"verse_number": 21,
"chapter": 96
}
]
}
The struct looks fine:
struct SearchResults: Decodable {
let count: Int
let next: String?
let previous: String?
let results: [Verse]
}
However, how do I initialize this dictionary with a nested array? I tried something like
// here is the issue - what should the searchResults structure look like?
// to properly store the response
var searchResults: [String: AnyObject] = [String: AnyObject]()
...
let response = try JSONDecoder().decode(SearchResults.self, from: safeData)
DispatchQueue.main.async {
self.searchResults = response // error here
}
But get the error message
Cannot assign value of type 'SearchResults' to type '[String : AnyObject]'
Change the type of searchResults from [String: AnyObject] to 'SearchResults'
var searchResults : SearchResults?

Can I request a two-dimensional associative array in swift Moya?

The following two forms of data were successfully requested.
{
"ride_fare": 1000,
"km": 7
]
}
{
"ride_fare": 1000,
"km": 7,
"options": [ 0, 1, 2]
}
However, I don't know how to request a two-dimensional associative array like the one below.
How can I request it?
{
"ride_fare": 1000,
"km": 7,
"option_fares": [
{
"price": 200,
"name": "立ち寄り",
"id": 1
}
]
}
The code that I wrote:
var options = [Any]()
for option in optionFares {
let params = [
"id" : option.id ?? 0,
"name" : option.name ?? "",
"price" : option.price ?? 0
] as [String : Any]
options.append(params)
}
let faresData = [
"id" : driverOrder.id ?? 0,
"km" : driverOrder.distance ?? 0,
"option_fares" : options,
"ride_fare" : driverOrder.ride_fare ?? 0
] as [String : Any]
First, create a struct that matches the json format you want to request.
struct Params: Codable {
let rideFare, km: Int
let optionFares: [OptionFare]
enum CodingKeys: String, CodingKey {
case rideFare = "ride_fare"
case km
case optionFares = "option_fares"
}
}
struct OptionFare: Codable {
let price: Int
let name: String
let id: Int
}
And you must create a request parameter in Moya's task.
import Moya
extension APITarget: TargetType {
var task: Task {
case .yourCaseName(let price, let name, let id, let rideFare, let km):
let encoder: JSONEncoder = JSONEncoder()
let optionFareData: [OptionFare] = []
optionFareData.append(OptionFare(price, name, id))
let paramsData = Params(rideFare, km, optionFareData)
let jsonData: Data = try! encoder.encode(paramsData)
return .requestData(jsonData)
}
}
}

Send JSON array as a parameter Alamofire

I'm having difficulty with a parameter which contains a Json array using Alamofire
Here is a string of my Json:
"[{\"id\":546836102,\"count\":1},{\"id\":216479424,\"count\":1}]"
Here is my code where I make the request:
let data = (params["cart"] as! String).data(using: .utf8)!
do {
if let jsonArray = try JSONSerialization.jsonObject(with: data, options : .fragmentsAllowed) as? [Dictionary<String, Int>] {
let parameters: Parameters = [
"address_id": params["address_id"] as! Int,
"delivery_day": params["delivery_day"] as! String,
"delivery_hour": params["delivery_hour"] as! Int,
"cart": jsonArray,
"via": params["via"] as! String
]
Alamofire.request(Global.baseURL + "orders/finish", method: .post, parameters: parameters, encoding: URLEncoding.default, headers: header)
.responseSwiftyJSON {
Printing out my parameters
["cart": [["id": 546836102, "count": 1], ["count": 1, "id": 216479424]], "address_id": 641589205, "delivery_day": "1399-01-20", "delivery_hour": 21, "via": "ios"]
Backend must receive the cart as below:
[
{
"id": 123456,
"count": 2
},
{
"id": 654321,
"count": 3
}
]
But instead it gets the data like this:
{
"delivery_hour" : "21",
"delivery_day" : "1399-01-20",
"cart" : [
{
"count" : "1"
},
{
"id" : "546836102"
},
{
"count" : "1"
},
{
"id" : "216479424"
}
],
"via" : "ios",
"address_id" : "641589205"
}
I have tried JSONEncoding and URLEncoding options by Alamofire but nothing seems to work and this is the closest I've gotten to the API template so far.
What am I doing so wrong here?
// MARK: - Update
So I updated to the latest version of Alamofire and still no good results :(
Here is the code:
let payload = MyParameters(address_id: 641589205, delivery_day: "1399-01-21", delivery_hour: 21, cart: items, via: "ios")
AF.request(url, method: .post, parameters: payload, encoder: JSONParameterEncoder.default, headers: .init(header), interceptor: nil).response { dataResponse in
switch dataResponse.result {
case .success(let value):
if dataResponse.response?.statusCode == 200 {
let json = JSON(value!)
completionHandler(json, nil)
} else {
print(dataResponse.response?.statusCode ?? -1)
}
case .failure(let error):
print(error)
completionHandler(nil, error.localizedDescription)
}
}
My payload looks exactly what I want it to be
{
"cart" : [
{
"count" : 1,
"id" : 546836102
},
{
"count" : 1,
"id" : 216479424
}
],
"via" : "ios",
"address_id" : 641589205,
"delivery_day" : "1399-01-21",
"delivery_hour" : 21
}
But then again the endpoint receives this:
{
"{\"delivery_day\":\"1399-01-21\",\"address_id\":641589205,\"delivery_hour\":21,\"cart\":" : {
"{\"id\":546836102,\"count\":1},{\"id\":216479424,\"count\":1}" : null
}
}
Using Alamofire 4, you can't send arrays using the method you've shown, as the Parameters type is [String: Any]. You're also not using the correct ParameterEncoding. If you want to send a JSON body, you need to use the JSONEncoding. What you can do is manually serialize the array into a URLRequest and make that request using Alamofire.
Additionally, if you update to Alamofire 5, Encodable parameters are now supported, so you if you can define your parameters as an Encodable type, sending them would be as easy as passing an array.
AF.request(..., parameters: [some, encodable, values], encoder: JSONEncoder.default)...

Cannot convert value of type '[String : Any]' to expected argument type 'Data' Alamofire - Codable

I'm new to programming so apologies if the fix is a simple one. I'm trying to get the JSON data from the Alamofire request to show up not as an optional in the console.
I've already tried response.data which does give me the data as an optional but I don't know how to unwrap that optional in this call. I've searched and have seen that result.value might be closer to what I need. Below is what I have so far. This results in a "Cannot convert value of type '[String : Any]' to expected argument type 'Data'" error.
JSON File-->
"forecasts": [
{
"dateLabel": "今日",
"telop": "晴時々曇",
"date": "2019-08-16",
"temperature": {
"min": null,
"max": null
},
"image": {
"width": 50,
"url": "http://weather.livedoor.com/img/icon/2.gif",
"title": "晴時々曇",
"height": 31
}
},
{
"dateLabel": "明日",
"telop": "晴時々曇",
"date": "2019-08-17",
"temperature": {
"min": {
"celsius": "27",
"fahrenheit": "80.6"
},
"max": {
"celsius": "37",
"fahrenheit": "98.6"
}
},
"image": {
"width": 50,
"url": "http://weather.livedoor.com/img/icon/2.gif",
"title": "晴時々曇",
"height": 31
}
},
{
"dateLabel": "明後日",
"telop": "晴時々曇",
"date": "2019-08-18",
"temperature": {
"min": null,
"max": null
},
"image": {
"width": 50,
"url": "http://weather.livedoor.com/img/icon/2.gif",
"title": "晴時々曇",
"height": 31
}
}
],
"location": {
"city": "東京",
"area": "関東",
"prefecture": "東京都"
},
"publicTime": "2019-08-16T17:00:00+0900",
"copyright": {
"provider": [
{
"link": "http://tenki.jp/",
"name": "日本気象協会"
}
],
"link": "http://weather.livedoor.com/",
"title": "(C) LINE Corporation",
"image": {
"width": 118,
"link": "http://weather.livedoor.com/",
"url": "http://weather.livedoor.com/img/cmn/livedoor.gif",
"title": "livedoor 天気情報",
"height": 26
}
}
Data model-->
import Foundation
import Alamofire
// MARK: - WeatherData
struct WeatherData: Codable {
let forecasts: [Forecast]
}
struct Forecast: Codable {
let dateLabel, telop, date: String
let temperature: Temperature
let image: Image
enum CodingKeys: String, CodingKey {
case dateLabel = "dateLabel"
case telop = "telop"
case date = "date"
case temperature
case image
}
}
struct Image: Codable {
let width: Int
let url: String
let title: String
let height: Int
enum CodingKeys: String, CodingKey {
case width = "width"
case url = "url"
case title = "title"
case height = "height"
}
}
struct Temperature: Codable {
let min, max: Max?
enum CodingKeys: String, CodingKey {
case min = "min"
case max = "max"
}
}
struct Max: Codable {
let celsius, fahrenheit: String
enum CodingKeys: String, CodingKey {
case celsius = "celsius"
case fahrenheit = "fahrenheit"
}
}
viewcontroller-->
import UIKit
import Alamofire
class ForecastTableViewController: UITableViewController {
let WEATHER_URL = "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010"
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(WEATHER_URL).responseJSON { (response) in
if let data = response.result.value as? [String: Any]{
let decoder = JSONDecoder()
let forecast = try? decoder.decode(WeatherData.self, from: data)
print(forecast?.forecasts)
}
}
}
My ultimate goal is to print out the JSON data into a tableview, including the images and dates. I think being able to unwrap this optional is the first step before I figure out the next part.
I've already tried response.data which does give me the data as an optional but I don't know how to unwrap that optional in this call.
You definitely should learn how to unwrap optionals properly. It basically comes down to what do you want to do when the value is nil. response.data could be nil when somehow the data could not be fetched, there's no internet, the server doesn't respond, or whatever reason it might be.
Think about what you want to happen in such a situation. Do you want to show an error as an alert to the user? Do you want to try again, or just do nothing?
And then use this code:
Alamofire.request(WEATHER_URL).responseData { (response) in
guard let data = response.data else {
// do the stuff you want to do when the data failed to be fetched
return
}
let decoder = JSONDecoder()
guard let forecast = try? decoder.decode(WeatherData.self, from: data) else {
// do the stuff you want to do when the data is not in the right format
return
}
print(forecast?.forecasts)
}
If you want to access the raw Data returned from a request, you need to use responseData. responseJSON parses the response using JSONSerialization and gives you an Any value. responseData gives you the raw Data returned, so you can unwrap it as you're currently doing and use the JSONDecoder.
You can also update to Alamofire 5 (currently in beta) and use responseDecodable to be able to parse Decodable types automatically.

How do I specify more than one type to decode in the enum CodingKeys when using Codable?

I have a struct type which lists different types which must be decoded from JSON and I cannot find any information on how to specify more than one type in my enum called CodingKeys. In the innermost dictionary sometimes one key:value pair will be [String:String] and another key:value pair will be [String:Int]
I have tried specifying just String and CodingKey as in the code snippet below but Xcode reports 2 error messages.
struct JSONSiteData : Codable {
let destinationAddresses : [String]
let originAddresses : [String]
let rows : [ [String : [[String:[[String: [String:Any]]]]]] ]
let status : String
}
enum CodingKeys : String, CodingKey {
case destinationAddresses = "destination_addresses"
case originAddresses = "origin_addresses"
case rows
case status
}
I get the following error messages from Xcode;
Type 'JSONSiteData' does not conform to protocol 'Decodable'
Type 'JSONSiteData' does not conform to protocol 'Encodable'
Here is my JSON;
{
"destination_addresses": [
"1 Dunwell Ln, Bolam, Darlington DL2 2UW, UK",
"Unnamed Road, Newton Aycliffe DL5 6QZ, UK",
"Preston Manor Farm, Preston le Skerne, Newton Aycliffe DL5 6JH, United Kingdom",
"6 Middridge Farms, Middridge, Newton Aycliffe DL5 7JQ, UK",
"1 The Gardens, Hunwick, Crook DL15 0XW, UK"
],
"origin_addresses": [
"42 Drovers Way, Dunstable LU6 1AW, UK"
],
"rows": [
{
"elements": [
{
"distance": {
"text": "220 mi",
"value": 353731
},
"duration": {
"text": "3 hours 45 mins",
"value": 13475
},
"status": "OK"
},
{
"distance": {
"text": "222 mi",
"value": 356696
},
"duration": {
"text": "3 hours 45 mins",
"value": 13471
},
"status": "OK"
},
{
"distance": {
"text": "222 mi",
"value": 358053
},
"duration": {
"text": "3 hours 46 mins",
"value": 13545
},
"status": "OK"
},
{
"distance": {
"text": "225 mi",
"value": 361421
},
"duration": {
"text": "3 hours 49 mins",
"value": 13768
},
"status": "OK"
},
{
"distance": {
"text": "229 mi",
"value": 369280
},
"duration": {
"text": "3 hours 57 mins",
"value": 14238
},
"status": "OK"
}
]
}
],
"status": "OK"
}
You can try this,
// To parse the JSON, add this file to your project and do:
//
// let jsonSiteData = try? newJSONDecoder().decode(JSONSiteData.self, from: jsonData)
import Foundation
struct JSONSiteData: Codable {
let destinationAddresses, originAddresses: [String]
let rows: [Row]
let status: String
enum CodingKeys: String, CodingKey {
case destinationAddresses = "destination_addresses"
case originAddresses = "origin_addresses"
case rows, status
}
}
struct Row: Codable {
let elements: [Element]
}
struct Element: Codable {
let distance, duration: Distance
let status: String
}
struct Distance: Codable {
let text: String
let value: Int
}
Refer this link to generate models from JSON Strings.
https://app.quicktype.io/
In terms of Codable there are no different types.
Decode these structs
struct JSONSiteData : Decodable {
let destinationAddresses : [String]
let originAddresses : [String]
let rows : [Row]
let status : String
}
struct Row : Decodable {
let elements : [Element]
}
struct Element : Decodable {
let distance : Item
let duration : Item
let status : String
}
struct Item : Decodable {
let text : String
let value : Int
}