How to troubleshoot API Call JSON in SwiftUI - swift

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()
}
}

Related

Variable Not Passing In POST parameter in 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
}
}

Swift iOS App - No Console Messages Cannot Make URLSession Call to GraphQL Enpoint

I've been trying to get a list of entities from a graphql endpoint but I can't figure it out. Also, the console in my Xcode v13.4 isn't showing anything even though I have some print() statements in the code, so that's not helping - I've found where it is at the bottom of the window but it's always blank.
My View to get the data is below, the DetailView is the link following a link from the main ContentView. the loadData function content, I got the code from Postman after testing the graphql call.
import SwiftUI
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
//array of properties
struct Response: Codable {
var data: Properties
}
struct Properties: Codable {
var properties: Nodes
}
struct Nodes: Codable {
var nodes: [Result]
}
struct Result: Codable {
var propertyId: Int
var title: String
}
struct DetailView: View {
#State private var results = [Result]()
var body: some View {
List(results, id: \.propertyId) { property in
VStack(alignment: .leading) {
Text(property.title)
.font(.headline)
}
}
.task{
await loadData()
}
}
func loadData() async {
let semaphore = DispatchSemaphore (value: 0)
let parameters = "{\"query\":\"{\\n properties {\\n nodes {\\n title(format: RENDERED)\\n propertyId\\n }\\n }\\n}\",\"variables\":{}}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "http://DOMAIN/graphql")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
do {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.data.properties.nodes
}
semaphore.signal()
}
task.resume()
semaphore.wait()
} catch {
print("Invalid data")
}
}
}
The output of the graphql call is
{
"data": {
"properties": {
"nodes": [
{
"title": "MY TITLE",
"propertyId": 00001
}
]
}
}
}
EDIT
Swapped try? for try! as suggested
if let decodedResponse = try! JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.data.properties.nodes
}
error: Initializer for conditional binding must have Optional type, not 'Response'
I managed to get it all working with the below...
import SwiftUI
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
//array of properties
struct Response: Codable {
var data: Properties
}
struct Properties: Codable {
var properties: Nodes
}
struct Nodes: Codable {
var nodes: [Result]
}
struct Result: Codable {
var propertyId: Int
var title: String
}
struct DetailView: View {
#State private var results = [Result]()
#State private var test = "one"
var body: some View {
List(results, id: \.propertyId) { property in
VStack(alignment: .leading) {
Text(property.title)
.font(.headline)
}
}.task{
await loadData()
}
}
func loadData() async {
let semaphore = DispatchSemaphore (value: 0)
let parameters = "{\"query\":\"{\\n properties {\\n nodes {\\n title(format: RENDERED)\\n propertyId\\n }\\n }\\n}\",\"variables\":{}}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://DOMAIN/graphql")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
do {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
let decodedResponse = try! JSONDecoder().decode(Response.self, from: data)
results = decodedResponse.data.properties.nodes
semaphore.signal()
}
task.resume()
semaphore.wait()
} catch {
print("Invalid data \(error)")
}
}
}
Much to learn :)

how can I init struct: Codable without default value

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?

Swift used Codable, but the type is not correct

I know that Codable = Decodable & Encodable but when calling json from xcode,
Codable was given as a struct, but an error saying
Argument type'login.Type' does not conform to expected type'Encodable' appears.
json code
struct login: Codable {
var userId: String?
var userPw: String?
class func LoginBoard(_ completeHandler: #escaping (login) -> Void) {
let loginboard: String = MAIN_URL + "/member/login"
guard let url = URL(string: loginboard) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")
urlRequest.httpBody = try? JSONEncoder().encode(login) // ERROR [Argument type 'login.Type' does not conform to expected type 'Encodable']
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) in
guard error == nil else {
print("error calling Post on /todos/1")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
let decoder = JSONDecoder.init()
let LoginList = try decoder.decode(login.self, from: responseData)
completeHandler(LoginList)
}
catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
There is no error in try decoder.decode
but only in urlRequest.httpBody = try? JSONEncoder().encode(login) what is the problem?
You need to have something like this to set the values.
let loginvalues = login(userId: "john", userPw: "adfadfa")
urlRequest.httpBody = try? JSONEncoder().encode(loginvalues)
If you place this inside a play ground and run it you will see that you get the json data.
struct Login: Codable {
var userId: String?
var userPw: String?
}
let loginvalues = Login(userId: "john", userPw: "adfadfa")
let test = try? JSONEncoder().encode(loginvalues)
print(String(data: test!, encoding: .utf8)!)

How do I add a body to an HTTP GET request made with Combine in Swift?

I am making an HTTP GET request using Combine and I don't know how to add a body. I know that it is not ok to have a body in a GET request, but I really need to test some things. I am using Xcode 11.4 and iOS 13.4. Stack Overflow doesn’t let me post this question unless i write some more information, but I can’t think of any more pieces of information that you might need for this. Here is my code:
import Foundation
import Combine
//MARK: - Object to retrieve from JSON
struct Doctor: Codable, Identifiable {
let id = UUID()
let patients: [Patients]
}
struct Patients: Codable, Identifiable {
let id: String
let name: String
let phone: String
enum CodingKeys: String, CodingKey {
case id = "_id"
case name
case phone
}
}
class Network {
enum Error: LocalizedError {
case invalidResponse
case addressUnreachable(URL)
var errorDescription: String? {
switch self {
case .invalidResponse:
return "The server responded with garbage."
case .addressUnreachable(let url):
return "\(url.absoluteString) is unreachable."
}
}
}
let urlRequest = URL(string: URL)!
let networkQueue = DispatchQueue(label: "Networking",
qos: .default,
attributes: .concurrent)
func downloadPatients() -> AnyPublisher<Doctor, Error> {
URLSession.shared
.dataTaskPublisher(for: urlRequest)
.receive(on: networkQueue)
.map(\.data)
.decode(type: Doctor.self, decoder: JSONDecoder())
.mapError { (error) -> Network.Error in
switch error {
case is URLError:
return Error.addressUnreachable(self.urlRequest)
default:
return Error.invalidResponse
}
}
.eraseToAnyPublisher()
}
}
let networkRequest = Network()
func loadPatients() {
cancelable = networkRequest.downloadPatients()
.sink(
receiveCompletion: {
receiveValue: { doctor in
self.localPatients = doctor.patients
self.isShowing = false
}
)
}
Thank you!
I'm not sure if can be done with dataTaskPublish for a Get. But You can customize your request with this kind of call:
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.httpBody = body
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
// Handle the error here
}
guard let data = data else { return }
let resData = try! JSONDecoder().decode(ServerMessage.self, from: data)
}.resume()
}