Variable Not Passing In POST parameter in Swift - swift

I'm trying to get the cities by country using the POSTMAN Api. When I don't bring any variables into the parameter, the request works as expected. Though, when I try to use a global variable as a parameter, it returns empty. It works perfectly fine if it was coded as such: "country": "Nigeria" (everything else the same)
Code below:
let myCountry = selectedCountryString.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: "https://countriesnow.space/api/v0.1/countries/population/cities/filter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"limit": 10,
"order": "dsc",
"orderBy": "value",
"country": "\(myCountry)"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
let task = URLSession.shared.dataTask(with: request) {data, _, error in
guard let data = data, error == nil else {
return
}
do{
let response = try JSONDecoder().decode(CitiesPopModel.self, from: data)
onCompletion(response)
}
catch {
print("Error country -> \(myCountry)")
}
}
task.resume()
}
I switched my code to this and it is now working with the variable:
func callCitiesByPopAPI(completion: #escaping (CitiesPopModel?, Error?) -> ()) {
let url = "https://countriesnow.space/api/v0.1/countries/population/cities/filter"
let parameters: [String: Any] = [
"limit": 20,
"order": "dsc",
"orderBy": "value",
"country": selectedCountryString
]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: CitiesPopModel.self) { response in
if let error = response.error {
completion(nil, error)
return
}
if let result = response.value {
completion(result, nil)
print("City pop model result is \(result)")
return
}
}
}

Not worry about it you have to pass selectedCountryString value as like it then all goes perfectly
let selectedCountryString = "INDIA"

Replace value forHttpHeaderField for this:
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
And replace httpBody for this:
request.httpBody = body.percentEncoded()
I tested this with Algeria, it's worked.
extension Dictionary {
func percentEncoded() -> Data? {
map { key, value in
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
return escapedKey + "=" + escapedValue
}
.joined(separator: "&")
.data(using: .utf8)
}
}
extension CharacterSet {
static let 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
}()
}

Here is my test code that shows how to fetch the data from the server,
using a variable as a parameter (country) to the POST request, and display it in a view. Works very well for me.
struct ContentView: View {
#State var cityPop = CitiesPopModel()
var body: some View {
List(cityPop.data) { data in
VStack {
Text(data.city).foregroundColor(.blue)
ScrollView(.horizontal) {
HStack {
ForEach(data.populationCounts) { pop in
VStack {
Text(pop.year)
Text(pop.sex)
Text(pop.value).foregroundColor(.blue)
}
}
}
}
}
}
.onAppear {
getCountry(country: "Australia") { result in
if let popData = result {
cityPop = popData
}
}
}
}
func getCountry(country: String, completion: #escaping(CitiesPopModel?) -> Void) {
guard let url = URL(string: "https://countriesnow.space/api/v0.1/countries/population/cities/filter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"limit": 10,
"order": "dsc",
"orderBy": "value",
"country": country
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else { return }
do {
let response = try JSONDecoder().decode(CitiesPopModel.self, from: data)
return completion(response)
}
catch {
print("Error country -> \(country)")
}
completion(nil)
}.resume()
}
}
// MARK: - CitiesPopModel
struct CitiesPopModel: Codable {
var error: Bool = false
var msg: String = ""
var data: [City] = []
}
// MARK: - City
struct City: Identifiable, Codable {
let id = UUID()
let city: String
let country: String
let populationCounts: [Population]
enum CodingKeys: String, CodingKey {
case city, country, populationCounts
}
}
// MARK: - Population
struct Population: Identifiable, Codable {
let id = UUID()
let year, value, sex, reliabilty: String
enum CodingKeys: String, CodingKey {
case year, value, sex, reliabilty
}
}

Related

URLRequest Error "The given data was not valid JSON."

Trying to make a POST request with headers and params
Codeable code:
struct WelcomeMessage: Codable {
let receivedMessages: [ReceivedMessage]
}
// MARK: - ReceivedMessage
struct ReceivedMessage: Codable, Identifiable {
let ackID: String
let message: Message
let id = UUID()
enum CodingKeys: String, CodingKey {
case ackID
case message
}
}
// MARK: - Message
struct Message: Codable {
let data, messageID, publishTime: String
enum CodingKeys: String, CodingKey {
case data
case messageID
case publishTime
}
}
Service code:
class GetMessages: ObservableObject {
private var project_id: String = "redacted"
private var project_name: String = "redacted"
#Published var messages = [ReceivedMessage]()
func getMessages() {
guard let url = URL(string: "https://pubsub.googleapis.com/v1/projects\(project_id)/subscriptions\(project_name):pull") else {return}
var request = URLRequest(url: url)
let parameters : [String:Any] = [
"returnImmediately": false,
"maxMessages": 10]
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer ya29.redacted", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
return
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
// guard let data = data else {print("empty data"); return }
let theData = try! JSONDecoder().decode(WelcomeMessage.self, from: data!)
print(theData)
DispatchQueue.main.async {
self.messages = theData.receivedMessages
}
}
.resume()
}
}
The response to the request should return some JSON data that looks like:
{
"receivedMessages": [
{
"ackId": "UdfdsfdsfdsfdsfdgfhgfjJHGkjkjhKgjhgjFhgfDFgfdgDFGDFdfgFDGfd",
"message": {
"data": "//BASE-64 ENCODED STRING HERE",
"messageId": "4130086024457484",
"publishTime": "2022-02-16T15:03:49.372Z"
}
}
]
}
Error message as above, not sure why it's saying the data is not valid JSON?
Additional opinionated question...Should I just be using AlamoFire for this?
If the json response you show is correct, then in ReceivedMessage change ackID to ackId (note the small "d"),
or use
enum CodingKeys: String, CodingKey {
case ackID = "ackId"
case message
}
Similarly for messageID in Message.

Looping through JSON array | Swift 5

I need to perform an action on each instance of trialid in the following JSON array - How can I loop through each instance of trialid using forEach? The goal is to pass each instance of trialid to another function that only excepts one value of trialid at a time.
The following is the structure of the JSON array:
[
{
"name": "mobile",
"orderid": 1,
"trialid": 27
},
{
"name": "mobile",
"orderid": 1,
"trialid": 33
}
]
The following is the what I am currently trying - how can foreach be performed here to loop through each object:
var structure = [testStructure]()
func fetch() {
guard let url = URL(string: "test.com")
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "id=1".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
let nav = try JSONDecoder().decode(structure, from: data)
structure.forEach ..
}
catch {
print(error)
}
}.resume()
}
struct testStructure: Decodable {
let name: String?
let orderid: Int?
let trialid: Int?
}
First of all please start names of classes, structures and other types with a capital letter.
If the API JSON response contains an array you need to decode it to the array type like this:
let nav = try JSONDecoder().decode([TestStructure].self, from: data)
then you can iterate through your decoded array of structures (full code):
import UIKit
struct TestStructure: Decodable {
let name: String?
let orderid: Int?
let trialid: Int?
}
func fetch() {
guard let url = URL(string: "test.com")
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "id=1".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
let nav = try JSONDecoder().decode([TestStructure].self, from: data)
nav.forEach { testStructure in
// do whatever you want here
if let trialid = testStructure.trialid {
processingTrialid(trialid)
}
}
}
catch {
print(error)
}
}.resume()
}
func processingTrialid(_ trialid: Int) {
print("trialid: ", trialid)
}

How to troubleshoot API Call JSON in SwiftUI

I'm making an API call to a Rails server to fetch an array of objects and then display those objects in a SwiftUI view.
When I make this same API call in Postman, it works fine. I get the response.
When I make this same call in my SwiftUI project, I don't appear to be saving that response to my Models properly or I'm running into an error otherwise. My server appears to be sending the data fine. The view loads, but with a blank List and just the navigationTitle of "Your Projects"
Looking for guidance on how to check if my response array is storing data and how to troubleshoot. The view loads the data from this array and it appears to be empty.
I used quicktype.io to map the model structure out from the server provided JSON in Postman.
Here's the relevant portion of the Model:
import Foundation
struct ProjectFetchRequest: Decodable {
let request: [ProjectResponseObjectElement]
}
// MARK: - ProjectResponseObjectElement
struct ProjectResponseObjectElement: Codable, Identifiable {
let id = UUID()
let project: Project
let projectType: ProjectType
let inspirations: [JSONAny]
}
// MARK: - Project
struct Project: Codable {
let name: String
let id: Int
let projectType, timeframe, description: String
let currentProgress: Int
let zipcode, status, createdAt, visibility: String
let city, state: String
let title: String
let showURL: String
let thumbnailURL: String
let ownedByLoggedinUser, hasBids, isPublished: Bool
}
// MARK: - ProjectType
struct ProjectType: Codable {
let generalConstructions, landscapes: [GeneralConstruction]?
}
// MARK: - GeneralConstruction
struct GeneralConstruction: Codable {
let id: Int
}
typealias ProjectResponseObject = [ProjectResponseObjectElement]
Here's the API call:
import Foundation
final class Projectservice {
static let shared = Projectservice()
private init() {}
func fetchProjects(completed: #escaping (Result<[ProjectResponseObjectElement], AuthenticationError>) -> Void) {
guard let url = URL(string: "https://example.com/api/v1/projects") else {
completed(.failure(.custom(errorMessage:"URL unavailable")))
return
}
guard let Accesstoken = UserDefaults.standard.string(forKey: "access-token") else { return }
guard let client = UserDefaults.standard.string(forKey: "client") else { return }
guard let uid = UserDefaults.standard.string(forKey: "userEmail") else { return }
print(Accesstoken)
print(client)
print(uid)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(Accesstoken, forHTTPHeaderField: "access-token")
request.addValue(client, forHTTPHeaderField: "client")
request.addValue(uid, forHTTPHeaderField: "uid")
request.addValue("Bearer", forHTTPHeaderField: "Tokentype")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else { return }
guard let projectResponse = try? JSONDecoder().decode(ProjectFetchRequest.self, from: data) else { return }
completed(.success(projectResponse.request))
print(projectResponse)
}.resume()
}
}
Here's the view:
import SwiftUI
struct ProjectsView: View {
#State private var projectObjects: [ProjectResponseObjectElement] = []
var body: some View {
NavigationView{
List(projectObjects){ projectObject in
ProjectRowView(project: projectObject.project)
}
.navigationTitle("Your Projects")
.foregroundColor(.primary)
}.navigationViewStyle(StackNavigationViewStyle())
.onAppear {
fetchProjects()
}
}
func fetchProjects() {
Projectservice.shared.fetchProjects { result in
DispatchQueue.main.async {
switch result {
case .success(let projectObjects):
self.projectObjects = projectObjects
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
}
I needed to declare the top level array struct in the URLSession.
import Foundation
final class Projectservice {
static let shared = Projectservice()
private init() {}
func fetchProjects(completed: #escaping (Result<[ProjectResponseObjectElement], AuthenticationError>) -> Void) {
guard let url = URL(string: "https://example.com/api/v1/projects") else {
completed(.failure(.custom(errorMessage:"URL unavailable")))
return
}
guard let Accesstoken = UserDefaults.standard.string(forKey: "access-token") else { return }
guard let client = UserDefaults.standard.string(forKey: "client") else { return }
guard let uid = UserDefaults.standard.string(forKey: "userEmail") else { return }
print(Accesstoken)
print(client)
print(uid)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(Accesstoken, forHTTPHeaderField: "access-token")
request.addValue(client, forHTTPHeaderField: "client")
request.addValue(uid, forHTTPHeaderField: "uid")
request.addValue("Bearer", forHTTPHeaderField: "Tokentype")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else { return }
do {
let projectResponse = try JSONDecoder().decode([ProjectResponseObjectElement].self, from: data)
completed(.success(projectResponse))
} catch {
print(error)
}
}.resume()
}
}

Mutating Struct property with asynchronous function

I have the following Struct that I want to initialize, and then use its method query() to mutate its result property.
Query() sends and fetches JSON data, then decodes it to a String. When I declare query() as a mutating function, I receive the error "Escaping closure captures mutating 'self' parameter" in my URLSession.
What do I need to change?
The call:
var translation = Translate(string: "hello", base: "en", target: "de", result: "")
translation.query()
let translated = translation.result
The struct:
struct Translate {
let string: String, base: String, target: String
var result: String
mutating func query() {
let body: [String: String] = ["q": self.string, "source": self.base, "target": self.target]
let bodyData = try? JSONSerialization.data(withJSONObject: body)
guard let url = URL(string: "https://libretranslate.com/translate") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = bodyData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
DispatchQueue.main.async {
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
if responseJSON["translatedText"] != nil {
self.result = responseJSON["translatedText"] as! String
}
}
}
return
}
.resume()
}
}
Xcode error:
There are many issues in the code.
The most significant issue is that the URLRequest is asynchronous. Even if no error occurred result will be always empty.
You have to add a completion handler – it fixes the errors you got by the way – and it's highly recommended to handle all errors.
Instead of JSONSerialization the code uses JSONDe/Encoder
struct Translation : Decodable { let translatedText : String }
struct Translate {
let string: String, base: String, target: String
func query(completion: #escaping (Result<String,Error>) -> Void) {
let body: [String: String] = ["q": self.string, "source": self.base, "target": self.target]
do {
let bodyData = try JSONEncoder().encode(body)
let url = URL(string: "https://libretranslate.com/translate")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = bodyData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error { completion(.failure(error)); return }
completion( Result{ try JSONDecoder().decode(Translation.self, from: data!).translatedText} )
}
.resume()
} catch {
completion(.failure(error))
}
}
}
let translation = Translate(string: "hello", base: "en", target: "de")
translation.query() { result in
DispatchQueue.main.async {
switch result {
case .success(let translated): print(translated)
case .failure(let error): print(error)
}
}
}
Both exclamation marks (!) are safe.

Swift ui macos #Published nil or Int

I have the following variable, I would like it to take nil as an initial value and then an Int value.
#Published var status: Int = 0
To better understand place all the reference code:
struct ServerMessage: Decodable {
let token: String
}
class Http: ObservableObject {
#Published var status: Int = 0
#Published var authenticated = false
func req(url: String, httpMethod: String, body: [String: String]?) {
guard let url = URL(string: url) else { return }
let httpBody = try! JSONSerialization.data(withJSONObject: body ?? [])
var request = URLRequest(url: url)
request.httpMethod = httpMethod
request.httpBody = httpBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Error: \(String(describing: error))")
return
}
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 400: do {
print("Error: 400")
DispatchQueue.main.async {
self.status = 400
}
return
}
case 401: do {
print("Error: 401")
DispatchQueue.main.async {
self.status = 401
}
return
}
default: do {}
}
}
do {
if let data = data {
let results = try JSONDecoder().decode(ServerMessage.self, from: data)
DispatchQueue.main.async {
self.authenticated = true
}
print("Ok.", results)
} else {
print("No data.")
}
} catch {
print("Error:", error)
}
}.resume()
}
}
Use:
self.http.req(
url: "",
httpMethod: "POST",
body: ["email": "", "password": ""]
)
Make it optional (with all following corrections in place of usage)
#Published var status: Int? = nil // << I like explicit initialising
Update: possible variant of usage in View
Text("\(http.status ?? 0)") // << it is Int, so ?? "" is not valid
but probably it is more appropriate (due to no sense to show unknown status field)
if http.status != nil {
Text("\(http.status!)")
}