I have to send array of dictionaries via POST request. For example:
materials: [[String, String]] = [
[
"material_id": 1,
"qty": 10
],
[
"material_id": 2,
"qty": 5
]
]
Alamofire.request sends the next post data:
materials => array(
[0] => array("material_id" => 1),
[1] => array("qty" => 10),
[2] => array("material_id" => 2),
[3] => array("qty" => 5),
)
I want receive that representation:
materials => array(
[0] => array(
"material_id" => 1,
"qty" => 10
),
[1] => array(
"material_id" => 2,
"qty" => 5
),
)
The problem was in append method. I have coded on PHP 5 years and forgoted that in Swift the indexes not automatically assigned like in PHP. So, my first bugged code was:
func getParameters() -> [[String: AnyObject]] {
var result = [[String: AnyObject]]()
for mmap in mmaps {
let material: [String: AnyObject] = [
"material_id": mmap.material.id,
"quantity": mmap.qty
]
result.append(material)
}
return result
}
The answer is hard assign the keys as you need:
func getParameters() -> [String: [String: AnyObject]] {
var result = [String: [String: AnyObject]]()
let mmaps = self.mmaps.allObjects as [Mmap]
for i in 0..<mmaps.count {
let mmap = mmaps[i]
let material: [String: AnyObject] = [
"material_id": mmap.material.id,
"quantity": mmap.qty
]
result["\(i)"] = material
}
return result
}
A couple of thoughts:
It would be easiest if you sent the response as a dictionary with one key, and it will correctly encode the array within the dictionary:
let materials = [ "materials":
[
[
"material_id": 1,
"qty": 10
],
[
"material_id": 2,
"qty": 5
]
]
]
You could then just supply that as the parameters of request(), and Alamofire will properly encode that for you.
If you wanted to send an array of dictionaries, an alternative would be to change the web service to accept JSON. You could then encode the JSON yourself (using JSONSerialization or JSONEncoder), set the body of the request, and then send that request.
If you want to send application/x-www-form-urlencoded request with the array of dictionaries, you'd have to encode that yourself. In Swift 3 and later, that might look like:
func encodeParameters(_ object: Any, prefix: String? = nil) -> String {
if let dictionary = object as? [String: Any] {
return dictionary.map { key, value -> String in
self.encodeParameters(value, prefix: prefix != nil ? "\(prefix!)[\(key)]" : key)
}.joined(separator: "&")
} else if let array = object as? [Any] {
return array.enumerated().map { (index, value) -> String in
return self.encodeParameters(value, prefix: prefix != nil ? "\(prefix!)[\(index)]" : "\(index)")
}.joined(separator: "&")
} else {
let escapedValue = "\(object)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
return prefix != nil ? "\(prefix!)=\(escapedValue)" : "\(escapedValue)"
}
}
Where
extension CharacterSet {
/// Returns the character set for characters allowed in the individual parameters within a query URL component.
///
/// The query component of a URL is the component immediately following a question mark (?).
/// For example, in the URL `http://www.example.com/index.php?key1=value1#jumpLink`, the query
/// component is `key1=value1`. The individual parameters of that query would be the key `key1`
/// and its associated value `value1`.
///
/// According to RFC 3986, the set of unreserved characters includes
///
/// `ALPHA / DIGIT / "-" / "." / "_" / "~"`
///
/// In section 3.4 of the RFC, it further recommends adding `/` and `?` to the list of unescaped characters
/// for the sake of compatibility with some erroneous implementations, so this routine also allows those
/// to pass unescaped.
static var urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]#" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
Obviously, use whatever response method is appropriate for the nature of your server's response (e.g. response vs. responseJSON vs. ...).
Anyway, the above generates a request body that looks like:
materials[0][material_id]=1&materials[0][qty]=10&materials[1][material_id]=2&materials[1][qty]=5
And this appears to be parsed by servers as you requested in your question.
It's worth noting that this final point illustrates the preparation of an application/x-www-form-urlencoded request with nested dictionary/array structure, as contemplated here. This works on my server run by a major ISP, but I must confess that I haven't seen this convention documented in formal RFCs, so I'd be wary of doing it. I'd personally be inclined to implement this as JSON interface.
For prior versions of Swift, see previous revision of this answer.
You can make your array as JSON string and post in to server, then parse JSON in server end and from that you can get your desired data, lke this:
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject: yourArry options:NSJSONWritingPrettyPrinted error:&error];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
Hope this helps.. :)
Related
What is the best way to get the id value from this url:
URL(string: "urlScheme://search/:id=0001")
I've been trying to route this URL using a deep link request. However, my url routing solution JLRoutes shows the parameters as key = id and value = :id=0001.
I instead need the parameters to be key = id and value = "0001".
In an ideal world I would just be using a URL string like "urlScheme://search/0001" and not have any problem but the ":id=" part has to be in there. George's comment about converting the parameter to a URL in of itself and using .pathComponents.last does work, but I think a regex solution is probably going to scale better going forward.
The answer from #George should work fine, but two things struck me: you decided you wanted a regex solution, and to make this generic seemed to be asking for a recursive solution.
The below approach uses regex to identify up to the last /: delimiter, then has to do a bit of inelegant string handling to split it into the base string and the final pair of (key: value) params. I'd hoped to be able to write a regex that just matches those final parameters as that would be a far cleaner range to work with, but haven't managed it yet!
func paramsFrom(_ str: String) -> [String: String] {
guard let baseRange = str.range(of:#"^.+\/:"#, options: .regularExpression ) else { return [:] }
let base = String(str[baseRange].dropLast(2))
let params = str.replacingCharacters(in: baseRange, with: "").components(separatedBy: "=")
return [params.first! : params.last!].merging(paramsFrom(base)){(current, _) in current}
}
using this on your example string returns:
["id": "0001", "title": "256", "count": "100"]
EDIT:
Managed to dig out the old regex brain cells and match just the final pair of parameters. You could adapt the above to use the regex
(?<=\/:)[a-zA-Z0-9=]+$
and the have slightly cleaner string handling as the shortened base string becomes
String(str.dropLast(str[paramsRange].count))
If your URL is in the form of an actual URL query, e.g. urlScheme://search?id=0001, there is a nice way to do this.
With thanks to vadian, this is really simple. You can just do the following:
let components = URLComponents(string: "urlScheme://search?id=0001&a=2")!
let dict = components.queryItems?.reduce(into: [:]) { partialResult, queryItem in
partialResult[queryItem.name] = queryItem.value
}
Or a slightly more compact version for dict:
let dict = components.queryItems?.reduce(into: [:], { $0[$1.name] = $1.value })
Result from given input:
["id": "0001", "a": "2"]
If you must use the current URL form
You can replace the URL string, such as:
let urlStr = "urlScheme://search/:id=0001/:a=2"
let comps = urlStr.components(separatedBy: "/:")
let newUrl: String
if comps.count > 1 {
newUrl = "\(comps.first!)?\(comps.dropFirst().joined(separator: "&"))"
} else {
newUrl = urlStr
}
print(newUrl)
Prints: urlScheme://search?id=0001&a=2
Original answer (slightly modified)
If you have a URL with queries separated by /: you can use the following:
// Example with multiple queries
let url = URL(string: "urlScheme://search/:id=0001/:a=2")!
let queries = url.lastPathComponent.dropFirst().split(separator: "/:")
var dict = [String: String]()
for query in queries {
let splitQuery = query.split(separator: "=")
guard splitQuery.count == 2 else { continue }
let key = String(splitQuery.first!)
let value = String(splitQuery[1])
dict[key] = value
}
print(dict)
Result is same as before.
You can use next regex approach to enumerate parameters in your url path:
let urlString = "urlScheme://search/:id=0001" as NSString
let regex = try! NSRegularExpression(pattern: "([^:\\/]+)=([0-9]+)")
if let match = regex.matches(in: urlString as String, options: [], range: NSMakeRange(0, urlString.length)).first, match.numberOfRanges == 3 {
let key = urlString.substring(with: match.range(at: 1))
let value = urlString.substring(with: match.range(at: 2))
print(key, ":", value)
}
// Prints
id : 0001
I'm trying to make a put request with Alamofire and I want to pass in body something like this:
[
{
"id" : 1,
"position": 0
},
{
"id" : 2,
"position": 1
},
{
"id" : 6,
"position": 2
}
]
Normally, to do a request with alamofire I do this:
request = Alamofire
.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: buildHeaders());
Alamofire forces me to make parameters a dictionary but I want that paramaters to be an array of dictonary. How can I do this?
Thanks.
Alamofire added support for Encodable parameters in Alamofire 5, which provides support for Array parameters. Updating to that version will let you use Array parameters directly. This support should be automatic when passing Array parameters, you just need to make sure to use the version of request using encoder rather than encoding if you're customizing the encoding.
Well, the body of your parameters has type as [[String: Any]], or if you using Alamofire [Parameters].
So you if you parsing some Array of Objects to create this Array of parameters. You can do like this:
var positionedArray = [[String : Any]]()
for (index, item) in dataArray.enumerated() {
guard let id = item.id else {
return
}
let singleParameters : [String: Any] = ["id": id, "position" : index]
sorted.append(singleParameters)
}
As result, you can use this body (parameters), for your request.
Also, you should use JSONSerialization:
For example, if you using a customized Alamofire client, just use extension:
let data = try JSONSerialization.data(withJSONObject: parameters, options: JSONSerialization.WritingOptions.prettyPrinted)
let json = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
request.httpBody = json!.data(using: String.Encoding.utf8.rawValue)
var finalRequest = try URLEncoding.default.encode(request, with: nil)
I have Swift dictionary:
private var params = [String : AnyObject]()
This contains query items like:
"lat" = "40"
"lon" = "100"
I would like to map this dictionary to NSURLQueryItem array. I want to make it "swifty":
params.map{NSURLQueryItem.init}
But I get an error. Even if I change the map to be [String:String?]. I know I can do something like this elegant one-liner. Can anybody tell how?
You just need to add a failable initializer to NSURLQueryItem that receives a tuple like this (String, AnyObject)
extension NSURLQueryItem {
convenience init?(tuple: (String, AnyObject)) {
guard let value = tuple.1 as? String else { return nil }
self.init(name: tuple.0, value: value)
}
}
That's it!
let params: [String:AnyObject] = ["lat": "40", "lon": "100"]
let queryItems = params.flatMap(NSURLQueryItem.init)
Does your value for the dictionary need to be an optional? In a dictionary, when you assign its key as nil, the entry is deleted.
var params = [String:String?]()
params["lat"] = "40"
params["lon"] = "100"
params["key"] = "hey"
print(params) //result: ["lat": Optional("40"), "lon": Optional("100"), "key": Optional("hey")]
params["key"] = nil
print(params) //result: ["lat": Optional("40"), "lon": Optional("100")]
I suggest using a non optional-value dictionary. I have successfully written the code below:
import UIKit
var params = [String:String]()
params["lat"] = "40"
params["lon"] = "100"
let nsurl = params.map() {NSURLQueryItem.init(name: $0, value: $1)}
print(nsurl)
//Result:
//[<NSURLQueryItem 0x7f8252d29730> {name = lat, value = 40}, <NSURLQueryItem 0x7f8252d29700> {name = lon, value = 100}]
I hope this helps
To you can create one expression like this:
(1...100).map(String.init)
The class has to support it, the String has one initializer with the following signature:
public init(stringInterpolationSegment expr: Int)
With allow it to you use the String.init referred as Int -> String.
But in your case the NSURLQueryItem has not the desired initializer, so the more close you can do it is using map like in the conventional way and passing the parameters to the init of the class NSURLQueryItem like in this way:
let listMapped = params.map { NSURLQueryItem(name: $0.0, value: $0.1 as? String) }
I hope this help you.
I looked at What's the cleanest way of applying map() to a dictionary in Swift? and came up with these two answers:
var params = ["lat": "40", "lon":"100"]
var p:[NSURLQueryItem] = []
var result1 = params.map { (key, value) in p.append(NSURLQueryItem(name:key, value:value)) } // version 1
var result2 = params.reduce([NSURLQueryItem]()) { $0 + [NSURLQueryItem(name:$1.0, value:$1.1)] } // version 2
In version 1, the parameter passed by map() is a (String, String) tuple. In version 2, the parameters passed by reduce() are [NSURLQueryItem] and a (String, String) tuple
Firstly, the the block or closure you're providing to the map function isn't quite right. Blocks are basically nameless functions, they take some number of parameters and return some type. If we were to be verbose, a solution would look something like this:
params.map { (a: (String, String)) -> NSURLQueryItem in
return NSURLQueryItem(name: a.0, value: a.1)
}
However we can simplify this bit of code. The dictionary is [String : String] so it can be inferred that the map function will take a (String, String) as a parameter, so we don't need to write that explicitly.
Swift also allows $n to refer to the nth element of a tuple parameter. Since we only have 1 parameter, $0 will refer to the first element of the first parameter.
The return type of the block can also be inferred, since we're creating a NSURLQueryItem, meaning we don't need to specify that either. This means we also don't need to write return in the block, we can just create the object.
Lastly, you should not call NSURLQueryItem.init() to create a new NSURLQueryItem, you should instead just say NSURLQueryItem().
Here's the minimal solution:
params.map { NSURLQueryItem(name: $0, value: $1) }
I am new to SwiftyJSON, and I'm having some trouble with it. I can get it to return the entire JSON file as a string, but the moment I try to parse it, I keep getting empty variables back, and I'm not sure what I'm doing wrong.
This is the formatting of my JSON file:
[
{
"entryID": 1,
"from": "String",
"to": "String",
"value": "String"
},
{
...
},
...
]
And this is roughly what I want to do with it (in quite inelegant code, I do apologise, I'm new to Swift):
for entry: JSON in indexJSON.arrayValue {
var vEntryID: Int
var vFrom: String
var vTo: String
var vValue: String
for (dictKey: String, dictVal: JSON) in entry.dictionaryValue {
if(dictKey=="entryID") {vEntryID = dictVal.intValue}
if(dictKey=="from") {vFrom = dictVal.stringValue}
if(dictKey=="to") {vTo = dictVal.stringValue}
if(dictKey=="value") {vValue = dictVal.stringValue}
}
someSwiftObject[vEntryID]["from"] = vFrom
someSwiftObject[vEntryID]["to"] = vTo
someSwiftObject[vEntryID]["value"] = vValue
}
However, this block never executes at all, because indexJSON.arrayValue is always empty.
When I try to run the following, it correctly prints the complete file contents to the console:
let indexJSON = JSON(content!)
println(indexJSON.stringValue)
But when I try to go deeper, to fetch any element, it returns nothing:
if(indexJSON.arrayValue.isEmpty==true) {println("indexJSON.arrayValue is Empty")}
if(indexJSON[0].arrayValue.isEmpty==true) {println("indexJSON[0].arrayValue is Empty")}
if(indexJSON[0].dictionaryValue.isEmpty==true) {println("indexJSON[0].dictionaryValue is Empty")}
if(indexJSON[0]["entryID"]==nil) {println("indexJSON[0][\"entryID\"].stringValue is Empty")}
Output:
indexJSON.arrayValue is Empty
indexJSON[0].arrayValue is Empty
indexJSON[0].dictionaryValue is Empty
indexJSON[0]["entryID"].stringValue is Empty
I'd be grateful for any help! What am I doing wrong?
I checked SwiftyJSON source code and I think I know where the problem is.
I suppose that you are using String to initialize the JSON object like this
let s = "{\"entryID\": 1,\"from\": \"String\",\"to\": \"String\",\"value\": \"String\"}"
let j = JSON(s)
In this case the JSON object is actuall given a type "String", not Array. That's why it's not iterable and its arrayValue is empty.
To do what you want to do, you need to initialize it with an Array object:
let arr = [
[
"entryID":1,
"from":"String",
"to":"String",
"value":"String",
]
]
let j2 = JSON(arr)
Now j2 is an array JSON object and iterable.
SwiftyJSON can only be initialized with NSData and object. So if you want to initialize it with a String you need to do this:
if let data = s.dataUsingEncoding(NSUTF8StringEncoding) {
let j = JSON(data:data)
println(j)
}
first of all, make sure the format of your json string is correct. in your question, your json string is a array, just format the string like this(the content is from my code):
let jsonStr = "[{\"name\": \"hangge\", \"age\": 100, \"phones\": [{\"name\": \"公司\",\"number\": \"123456\"}, {\"name\": \"家庭\",\"number\": \"001\"}]}, {\"name\": \"big boss\",\"age\": 1,\"phones\": [{ \"name\": \"公司\",\"number\": \"111111\"}]}]"
then you can use SwityJson to get the array object, like this:
let strData = jsonStr.data(using: String.Encoding.utf8, allowLossyConversion: false)
let json = JSON(data: strData!)
for object in json.arrayValue {
let name = object["name"].string
}
Take a look at the documentation here: https://github.com/lingoer/SwiftyJSON#loop
You are iterating it incorrectly. You should be iterating over the array with a for loop like this:
for (index: String, subJson: JSON) in json {
//Do something you want
}
When using the Alamofire Framework, my responses don't seem to be getting parsed correctly. The JSON response I get has some keys that appear to not be strings, and I don't know how to reference them/get their values.
Here is the part of my code that makes the call:
var url = "http://api.sandbox.amadeus.com/v1.2/flights/low-fare-search"
var params = ["origin": "IST",
"destination":"BOS",
"departure_date":"2014-10-15",
"number_of_results": 1,
"apikey": KEY]
Alamofire.request(.GET, url, parameters: params)
.responseJSON { (_, _, json, _) in
println(json)
}
}
And here is the first section printout when that function is called
Optional({
currency = USD;
results = ({
fare = {
"price_per_adult" = {
tax = "245.43";
"total_fare" = "721.43";
};
restrictions = {
"change_penalties" = 1;
refundable = 0;
};
"total_price" = "721.43";
};
...
});
});
You'll notice that results is not "results", but "price_per_adult" is the correct format. Is there some step I'm missing? When I cast it to NSDictionary it doesn't do anything to help the key format either.
I also tried the same endpoint in javascript and ruby, and both came back without problem, so I'm fairly confident that it is not the API that is causing problems.
Those keys are still Strings, that's just how Dictionarys are printlnd. It looks like it will surround the String in quotes when printing it only if it contains non-alphanumeric characters (_ in this case). You can test this by manually creating a Dictionary similar to the one you're getting back from your API request and then printing it:
let test = [
"currency": "USD",
"results": [
[
"fare": [
"price_per_adult": [
"tax": "245.43",
"total_fare": "721.43"
],
"restrictions": [
"change_penalties": 1,
"refundable": 0
],
"total_price": "721.43"
]
]
]
]
println(test)
Outputs:
{
currency = USD;
results = (
{
fare = {
"price_per_adult" = {
tax = "245.43";
"total_fare" = "721.43";
};
restrictions = {
"change_penalties" = 1;
refundable = 0;
};
"total_price" = "721.43";
};
}
);
}