How do I add a body to an HTTP GET request made with Combine in Swift? - 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()
}

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.

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 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)!)

Swift - Reusable URL Request with Delegates

Hi I'm new to Swift and I am trying to create a reusable generic Download Manager for URL Request that can be reused throughout my project in different View Controllers or reused within the same VC for a different URL Request calls. The problem that I have is how do I pass the Data Type from the Request into the Download Manager and then return the Downloaded Data back to the VC with the corresponding Data Type. I am able to pass the Data Type in a call to downloadRequest but I can't figure out how to pass the Data Type back to the VC via a delegate DownloadManagerDelegate. Any help would be greatly appreciate it!
Generic Download Manager:
protocol DownloadManagerDelegate {
func didUpdateData<T: Codable>(modelType: T.Type, downloadedData: T.Type)
}
struct DownloadManager {
var delegate: DownloadManagerDelegate?
func downloadRequest<T: Codable>(modelType: T.Type, parameters: [String: Any]) {
guard let url = URL(string: "https://www.someAPI...") else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
guard let httpBodyWithParameters = try? JSONSerialization.data(withJSONObject: parameters, options: []) else
{
print("error")
return
}
request.httpBody = httpBodyWithParameters
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if error != nil {
print("error")
return
}
if let safeData = data {
if let downloadedData = parseDownloadedData(data: safeData) {
self.delegate?.didUpdateData(modelType: modelType, downloadedData: downloadedData)
}
}
}.resume()
func parseDownloadedData(data: Data) -> T?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(T.self, from: data)
return decodedData
} catch {
print(error)
return nil
}
}
}
Delegate in my VC:
override func viewDidLoad() {
super.viewDidLoad()
downloadManager.delegate = self
}
func didUpdateData(modelType: modelType,downloadedData:downloadedData){
DispatchQueue.main.async {
print(downloadedData)
}
}
To call download downloadRequest:
downloadManager.downloadrequest(modeType: Type1.self, parameters: parameters)
The Data Model is defined as a struct:
struct DataModel1: Codable {
let ItemID: String
}
Then in the same VC I call the same function downloadManager that will call a different API which should return data for a different Model Type (defined as Struct)
downloadManager.downloadRequest(modeType: Type2.self, parameters: parameters)
The Data Model is defined as a struct:
struct DataModel2: Codable {
let EmployeeeID: String
}
In the Swift times Protocol/Delegate smells a bit objective-c-ish.
I recommend a completion handler with the versatile Result type.
It returns the generic type non-optional on success and any error on failure.
The force unwrapping of data is safe because if error is nil then data has a value
struct DownloadManager {
func downloadRequest<T: Decodable>(modelType: T.Type, parameters: [String: Any], completion : #escaping (Result<T, Error>) -> Void) {
guard let url = URL(string: "https://www.someAPI...") else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
do {
let httpBodyWithParameters = try JSONSerialization.data(withJSONObject: parameters)
request.httpBody = httpBodyWithParameters
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(.failure(error))
} else {
completion( Result { try JSONDecoder().decode(T.self, from: data!)})
}
}.resume()
} catch {
completion(.failure(error))
}
}
}
And use it
downloadManager.downloadrequest(modeType: Type1.self, parameters: parameters) { result in
switch result {
case .success(let data): print(data)
case .failure(let error): print(error)
}
}