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!)")
}
Related
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
}
}
I'm not sure what I'm doing wrong, I've been at it for an hour, I'm trying to print 1 or 2 lines from
https://api.covalenthq.com/v1/1/address/0x8f299f2908c9Cd71e723E7059Ac52eaea3638b2E/balances_v2/?&key=ckey_4eeea29a22c14701a9844f01151
but I get an error. I tried to debug it and it seems like my model and it's call is wrong, what's going on?
import SwiftUI
struct FetchingAPI: View {
#State private var results = [Result]()
var body: some View {
Text("Fetching API")
.onAppear(perform: loadData)
}
var semaphore = DispatchSemaphore (value: 0)
func loadData() {
guard let url = URL(string: "https://api.covalenthq.com/v1/1/address/0x8f299f2908c9Cd71e723E7059Ac52eaea3638b2E/balances_v2/?&key=ckey_4eeea29a22c14701a9844f01151") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url, timeoutInterval: Double.infinity)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
print(String(data: data, encoding: .utf8)!)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// AFTER HERE THE DATA IS [] NON EXISTENT
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
}
// everything is good, so we can exit
return
}
} else {
print(String(describing: error))
semaphore.signal()
return
}
semaphore.signal()
}
task.resume()
semaphore.wait()
}
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var data: Int
var address: String
var quote_currency: String
}
}
EDIT: Adding error message after adding suggested try catch:
I updated the code to be:
do {
if let data = data {
// print(String(data: data, encoding: .utf8)!)
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
}
// everything is good, so we can exit
return
}
} catch {
print("Unexpected error: \(error).")
semaphore.signal()
return
}
ERROR Message:
Unexpected error: keyNotFound(CodingKeys(stringValue: "updatedAt", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"updatedAt\", intValue: nil) (\"updatedAt\").", underlyingError: nil)).
Here's how I solved it without so much code:
struct ContentView: View {
#State var address:String = ""
var body: some View {
VStack(){
Text("address: \(address)")
Button(action: {
makeApiRequest()
}, label: {
Text("make api request")
})
}
}
func makeApiRequest(){
let url = URLRequest(url: URL(string: "https://api.covalenthq.com/v1/1/address/0x8f299f2908c9Cd71e723E7059Ac52eaea3638b2E/balances_v2/?&key=ckey_4eeea29a22c14701a9844f01151")!)
URLSession.shared.dataTask(with: url) { data, responce, error in
if let data = data {
if let decodeResponce = try? JSONDecoder().decode(apiResponce.self, from: data){
address = decodeResponce.data.address
}
}
}.resume()
print(url)
}
}
struct apiResponce: Codable {
let data:apiData
let error:Bool
}
struct apiData: Codable {
let address:String
let quote_currency:String
}
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()
}
}
I want to get Json from API, and use Codable protocol, but when I init published var, I get an error.
struct Search: Codable {
let result: [String]
}
class SearchViewModel: ObservableObject {
#Published var data = Search()
func loadData(search: String) {
var urlComps = URLComponents(string: getUrl)
let queryItems = [URLQueryItem(name: "result", value: search)]
urlComps!.queryItems = queryItems
let url = urlComps!.url!.absoluteString
guard let Url = URL(string: url) else { return }
URLSession.shared.dataTask(with: Url) { (data, res, err) in
do {
if let data = data {
let decoder = JSONDecoder()
let result = try decoder.decode(Search.self, from: data)
DispatchQueue.main.async {
self.data = result
}
} else {
print("there's no Data😭")
}
} catch (let error) {
print("Error!")
print(error.localizedDescription)
}
}.resume()
}
}
Change
#Published var data:Search?
I'm having an issue decoding an API response.
So we have a NetworkManager class which we use to decode APIs. I have a simple GET endpoint that I need to retrieve a list of airports from. Here is the endpoint:
static let airports = Endpoint(url: "/test/airports")
Endpoint is defined as follows:
public struct Endpoint : Equatable {
public init(url: String? = nil, pattern: String? = nil, methods: [Test.HTTPMethod] = [.get], type: Test.EncodingType = .json)
}
Then in our network manager we have:
public func call<R: Decodable>(_ endpoint: Endpoint,
with args: [String: String]? = nil,
using method: HTTPMethod = .get,
expecting response: R.Type?,
completion: APIResponse<R>) {
call(endpoint, with: args, parameters: Nothing(),
using: method, posting: Nothing(), expecting: response, completion: completion)
}
My Airport model is as follows:
struct Airport: Codable {
let id: String
let name: String
let iata3: String
let icao4: String
let countryCode: String
}
And then I'm calling the endpoint like:
private func getAirportsList() {
API.client.call(.airports, expecting: [Airport].self) { (result, airports) in
print(airports)
}
}
Now I'm using Charles to proxy and I am getting the response I expect:
[{
"id": "5f92b0269c983567fc4b9683",
"name": "Amsterdam Schiphol",
"iata3": "AMS",
"icao4": "EHAM",
"countryCode": "NL"
}, {
"id": "5f92b0269c983567fc4b9685",
"name": "Bahrain International",
"iata3": "BAH",
"icao4": "OBBI",
"countryCode": "BH"
}, {
"id": "5f92b0269c983567fc4b968b",
"name": "Bankstown",
"iata3": "BWU",
"icao4": "YSBK",
"countryCode": "AU"
}]
But in my getAirports() method, airports is nil. I'm really struggling to see why. Clearly the endpoint is being hit correctly but my decoding is failing.
Edit:
Full method:
private func call<P: Encodable, B: Encodable, R: Decodable>(_ endpoint: Endpoint,
with args: [String: String]? = nil,
parameters params: P?,
using method: HTTPMethod = .get,
posting body: B?,
expecting responseType: R.Type?,
completion: APIResponse<R>) {
// Prepare our URL components
guard var urlComponents = URLComponents(string: baseURL.absoluteString) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
guard let endpointPath = endpoint.url(with: args) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
urlComponents.path = urlComponents.path.appending(endpointPath)
// Apply our parameters
applyParameters: if let parameters = try? params.asDictionary() {
if parameters.count == 0 {
break applyParameters
}
var queryItems = [URLQueryItem]()
for (key, value) in parameters {
if let value = value as? String {
let queryItem = URLQueryItem(name: key, value: value)
queryItems.append(queryItem)
}
}
urlComponents.queryItems = queryItems
}
// Try to build the URL, bad request if we can't
guard let urlString = urlComponents.url?.absoluteString.removingPercentEncoding,
var url = URL(string: urlString) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
if let uuid = UIDevice.current.identifierForVendor?.uuidString, endpoint.pattern == "/logging/v1/device/<device_id>" {
let us = "http://192.168.6.128:3000/logging/v1/device/\(uuid)"
guard let u = URL(string: us) else { return }
url = u
}
// Can we call this method on this endpoint? If not, lets not try to continue
guard endpoint.httpMethods.contains(method) else {
completion?(.failure(nil, NetworkError(reason: .methodNotAllowed)), nil)
return
}
// Apply debug cookie
if let debugCookie = debugCookie {
HTTPCookieStorage.shared.setCookies(
HTTPCookie.cookies(
withResponseHeaderFields: ["Set-Cookie": debugCookie],
for:url
), for: url, mainDocumentURL: url)
}
// Build our request
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if let headers = headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
// If we are posting, safely retrieve the body and try to assign it to our request
if !(body is NothingProtocol) {
guard let body = body else {
completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
return
}
do {
let result = try encode(body: body, type: endpoint.encodingType)
request.httpBody = result.data
request.setValue(result.headerValue, forHTTPHeaderField: "Content-Type")
} catch {
completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
return
}
}
// Build our response handler
let task = session.dataTask(with: request as URLRequest) { (rawData, response, error) in
// Print some logs to help track requests
var debugOutput = "URL\n\(url)\n\n"
if !(params is Nothing.Type) {
debugOutput.append(contentsOf: "PARAMETERS\n\(params.asJSONString() ?? "No Parameters")\n\n")
}
if !(body is Nothing.Type) {
debugOutput.append(contentsOf: "BODY\n\(body.asJSONString() ?? "No Body")\n\n")
}
if let responseData = rawData {
debugOutput.append(contentsOf: "RESPONSE\n\(String(data: responseData, encoding: .utf8) ?? "No Response Content")")
}
Logging.client.record(debugOutput, domain: .network, level: .debug)
guard let httpResponse = response as? HTTPURLResponse else {
guard error == nil else {
completion?(.failure(nil, NetworkError(reason: .unwrappingResponse)), nil)
return
}
completion?(.failure(nil, NetworkError(reason: .invalidResponseType)), nil)
return
}
let statusCode = httpResponse.statusCode
// We have an error, return it
guard error == nil, NetworkManager.successStatusRange.contains(statusCode) else {
var output: Any?
if let data = rawData {
output = (try? JSONSerialization.jsonObject(with: data,
options: .allowFragments)) ?? "Unable to connect"
Logging.client.record("Response: \(String(data: data, encoding: .utf8) ?? "No error data")", domain: .network)
}
completion?(.failure(statusCode, NetworkError(reason: .requestFailed, json: output)), nil)
return
}
// Safely cast the responseType we are expecting
guard let responseType = responseType else {
completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
return
}
// If we are expecting nothing, return now (since we will have nothing!)
if responseType is Nothing.Type {
completion?(.success(statusCode), nil)
return
}
guard let data = rawData else {
assertionFailure("Could not cast data from payload when we passed pre-cast checks")
return
}
// Decode the JSON and cast to our expected response type
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let responseObject = try decoder.decode(responseType, from: data)
completion?(.success(statusCode), responseObject)
return
} catch let error {
let content = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
Logging.client.record("Failed to build codable from JSON: \(String(describing: content))\n\nError: \(error)", domain: .network, level: .error)
assertionFailure("Failed to build codable from JSON: \(error)")
completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
return
}
}
// Submit our request
task.resume()
}