URLSession.shared.dataTaskPublisher - how to convert received value to string? - ios

Being a Swift newbie I am trying to download and parse a CSV file using the code:
URLSession.shared.dataTaskPublisher(for: url)
.tryMap(handleOutput)
.sink { completion in
} receiveValue: { csvWords in
let lines = csvWords.split(separator: "\n")
for line in lines {
let columns = line.split(separator: ",")
for column in columns {
print("column: \(column)")
}
}
however I get the syntax error:
Cannot convert value of type 'String' to expected argument type 'Data.Element' (aka 'UInt8')
Please help me to understand, what is happening.
What value is returned by the receiveValue, isn't it the remote file content as string?
UPDATE:
Here is the missing method
func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
guard
// as? means "this might be nil"
let response = output.response as? HTTPURLResponse,
response.statusCode >= 200,
response.statusCode < 300
else {
throw URLError(.badServerResponse)
}
return output.data
}

Judging from your error message, handleOutput would appear to be publishing Data. If you want to call string functions, you would map that to String, e.g.,
var cancellable: AnyCancellable?
func foo(_ url: URL) {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.tryMap(handleOutput)
.compactMap { String(data: $0, encoding: .utf8) }
.sink { completion in
...
} receiveValue: { string in
...
}
}
Or, if you want, you could throw an error if it can't be converted to a string:
var cancellable: AnyCancellable?
func foo(_ url: URL) {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.tryMap(handleOutput)
.tryMap { data -> String in
guard let string = String(data: data, encoding: .utf8) else {
throw URLError(.badServerResponse)
}
return string
}
.sink { completion in
...
} receiveValue: { string in
...
}
}
In your revised question, you shared handleOutput. You could simply change that to produce your String for you:
func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> String {
guard
let response = output.response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode,
let string = String(data: output.data, encoding: .utf8)
else {
throw URLError(.badServerResponse)
}
return string
}
And then you don't need that extra compactMap/tryMap:
func foo(_ url: URL) {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.tryMap(handleOutput)
.sink { completion in
...
} receiveValue: { string in
...
}
}

Related

Return value from Closure within a function [duplicate]

I have created a utility class in my Swift project that handles all the REST requests and responses. I have built a simple REST API so I can test my code. I have created a class method that needs to return an NSArray but because the API call is async I need to return from the method inside the async call. The problem is the async returns void.
If I were doing this in Node I would use JS promises but I can't figure out a solution that works in Swift.
import Foundation
class Bookshop {
class func getGenres() -> NSArray {
println("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
println(urlPath)
let url: NSURL = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
var resultsArray:NSArray!
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if(error) {
println(error.localizedDescription)
}
var err: NSError?
var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
if(err != nil) {
println("JSON Error \(err!.localizedDescription)")
}
//NSLog("jsonResults %#", jsonResult)
let results: NSArray = jsonResult["genres"] as NSArray
NSLog("jsonResults %#", results)
resultsArray = results
return resultsArray // error [anyObject] is not a subType of 'Void'
})
task.resume()
//return "Hello World!"
// I want to return the NSArray...
}
}
You can pass callback, and call callback inside async call
something like:
class func getGenres(completionHandler: (genres: NSArray) -> ()) {
...
let task = session.dataTaskWithURL(url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
and then call this method:
override func viewDidLoad() {
Bookshop.getGenres {
genres in
println("View Controller: \(genres)")
}
}
Introduced in Swift 5.5 (iOS 15, macOS 12), we would now use the async-await pattern:
func fetchGenres() async throws -> [Genre] {
…
let (data, _) = try await URLSession.shared.dataTask(for: request)
return try JSONDecoder().decode([Genre].self, from: data)
}
And we would call it like:
let genres = try await fetchGenres()
The async-await syntax is far more concise and natural than the traditional completion handler pattern outlined in my original answer, below.
For more information, see Meet async/await in Swift.
The historic pattern is to use completion handlers closure.
For example, we would often use Result:
func fetchGenres(completion: #escaping (Result<[Genre], Error>) -> Void) {
...
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
// parse response here
let results = ...
DispatchQueue.main.async {
completion(.success(results))
}
}.resume()
}
And you’d call it like so:
fetchGenres { results in
switch results {
case .failure(let error):
print(error.localizedDescription)
case .success(let genres):
// use `genres` here, e.g. update model and UI
}
}
// but don’t try to use `genres` here, as the above runs asynchronously
Note, above I’m dispatching the completion handler back to the main queue to simplify model and UI updates. Some developers take exception to this practice and either use whatever queue URLSession used or use their own queue (requiring the caller to manually synchronize the results themselves).
But that’s not material here. The key issue is the use of completion handler to specify the block of code to be run when the asynchronous request is done.
Note, above I retired the use of NSArray (we don’t use those bridged Objective-C types any more). I assume that we had a Genre type and we presumably used JSONDecoder, rather than JSONSerialization, to decode it. But this question didn’t have enough information about the underlying JSON to get into the details here, so I omitted that to avoid clouding the core issue, the use of closures as completion handlers.
Swiftz already offers Future, which is the basic building block of a Promise. A Future is a Promise that cannot fail (all terms here are based on the Scala interpretation, where a Promise is a Monad).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Hopefully will expand to a full Scala-style Promise eventually (I may write it myself at some point; I'm sure other PRs would be welcome; it's not that difficult with Future already in place).
In your particular case, I would probably create a Result<[Book]> (based on Alexandros Salazar's version of Result). Then your method signature would be:
class func fetchGenres() -> Future<Result<[Book]>> {
Notes
I do not recommend prefixing functions with get in Swift. It will break certain kinds of interoperability with ObjC.
I recommend parsing all the way down to a Book object before returning your results as a Future. There are several ways this system can fail, and it's much more convenient if you check for all of those things before wrapping them up into a Future. Getting to [Book] is much better for the rest of your Swift code than handing around an NSArray.
Swift 4.0
For async Request-Response you can use completion handler. See below I have modified the solution with completion handle paradigm.
func getGenres(_ completion: #escaping (NSArray) -> ()) {
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult["genres"] as! NSArray
print(results)
completion(results)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
You can call this function as below:
getGenres { (array) in
// Do operation with array
}
Swift 3 version of #Alexey Globchastyy's answer:
class func getGenres(completionHandler: #escaping (genres: NSArray) -> ()) {
...
let task = session.dataTask(with:url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
Swift 5.5, async/wait-based solution
The original test URL provided by the original poster is no longer functional, so I had to change things a bit. This solution is based on a jokes API I found. That API returns a single joke, but I return it as an array of String ([String]), to keep it as consistent as possible with the original post.
class Bookshop {
class func getGenres() async -> [String] {
print("Hello inside getGenres")
let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
typealias Continuation = CheckedContinuation<[String], Never>
let genres = await withCheckedContinuation { (continuation: Continuation) in
let task = session.dataTask(with: url) { data, response, error in
print("Task completed")
var result: [String] = []
defer {
continuation.resume(returning: result)
}
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
print("jsonResult is \(jsonResult)")
if let joke = (jsonResult as? [String: String])?["joke"] {
result = [joke]
}
} catch {
print("JSON Error \(error.localizedDescription)")
print("data was \(String(describing: String(data: data, encoding: .utf8)))")
return
}
}
task.resume()
}
return genres
}
}
async {
let final = await Bookshop.getGenres()
print("Final is \(final)")
}
The withCheckedContinuation is how you made the Swift async function actually run in a separate task/thread.
I hope you're not still stuck on this, but the short answer is that you can't do this in Swift.
An alternative approach would be to return a callback that will provide the data you need as soon as it is ready.
There are 3 ways of creating call back functions namely:
1. Completion handler
2. Notification
3. Delegates
Completion Handler
Inside set of block is executed and returned when source is available, Handler will wait until response comes so that UI can be updated after.
Notification
Bunch of information is triggered over all the app, Listner can retrieve n make use of that info. Async way of getting info through out the project.
Delegates
Set of methods will get triggered when delegate is been called, Source must be provided via methods itself
Swift 5.5:
TL;DR: Swift 5.5 is not yet released(at the time of writing). To use swift 5.5, download swift toolchain development snapshot from here and add compiler flag -Xfrontend -enable-experimental-concurrency. Read more here
This can be achieved easily with async/await feature.
To do so, you should mark your function as async then do the operation inside withUnsafeThrowingContinuation block like following.
class Bookshop {
class func getGenres() async throws -> NSArray {
print("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
return try await withUnsafeThrowingContinuation { continuation in
let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
print("Task completed")
if(error != nil) {
print(error!.localizedDescription)
continuation.resume(throwing: error!)
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
let results: NSArray = jsonResult!["genres"] as! NSArray
continuation.resume(returning: results)
} catch {
continuation.resume(throwing: error)
}
})
task.resume()
}
}
}
And you can call this function like
#asyncHandler
func check() {
do {
let genres = try await Bookshop.getGenres()
print("Result: \(genres)")
} catch {
print("Error: \(error)")
}
}
Keep in mind that, when calling Bookshop.getGenres method, the caller method should be either async or marked as #asyncHandler
self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) in
self.endNetworkActivity()
var responseError: Error? = error
// handle http response status
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode > 299 , httpResponse.statusCode != 422 {
responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
}
}
var apiResponse: Response
if let _ = responseError {
apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
self.logError(apiResponse.error!, request: request)
// Handle if access token is invalid
if let nsError: NSError = responseError as NSError? , nsError.code == 401 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Unautorized access
// User logout
return
}
}
else if let nsError: NSError = responseError as NSError? , nsError.code == 503 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Down time
// Server is currently down due to some maintenance
return
}
}
} else {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
self.logResponse(data!, forRequest: request)
}
self.removeRequestedURL(request.url!)
DispatchQueue.main.async(execute: { () -> Void in
completionHandler(apiResponse)
})
}).resume()
There are mainly 3 ways of achieving callback in swift
Closures/Completion handler
Delegates
Notifications
Observers can also be used to get notified once the async task has been completed.
There are some very generic requirements that would like every good API Manager to satisfy:
will implement a protocol-oriented API Client.
APIClient Initial Interface
protocol APIClient {
func send(_ request: APIRequest,
completion: #escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Now Please check complete api structure
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: #escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}
This is a small use case that might be helpful:-
func testUrlSession(urlStr:String, completionHandler: #escaping ((String) -> Void)) {
let url = URL(string: urlStr)!
let task = URLSession.shared.dataTask(with: url){(data, response, error) in
guard let data = data else { return }
if let strContent = String(data: data, encoding: .utf8) {
completionHandler(strContent)
}
}
task.resume()
}
While calling the function:-
testUrlSession(urlStr: "YOUR-URL") { (value) in
print("Your string value ::- \(value)")
}

Combine conversion from model to viewModel gives error "Type of expression is ambiguous without more context when running .eraseToAnyPublisher()

I am trying to figure out why the last .eraseToAnyPublisher() is giving the aforementioned error, to me it seems all the types are well specified, aren't they?
static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
// prepare URL
let urlString = "\(baseURL)/search/users?q=\(query)"
guard let url = URL(string: urlString) else {
return Just([]).eraseToAnyPublisher()
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.flatMap({ users in
var userViewModels = [UserViewModel]()
users.forEach { user in
userViewModels.append(contentsOf: UserViewModel(with: user))
}
return userViewModels
})
.catch { err -> Just<[UserViewModel]> in
print(err)
return Just([])
}
.eraseToAnyPublisher() // <-- HERE IS THE ERROR
return publisher
}
Unfortunately with those complex Combine pipelines sometimes compliler errors are displayed on the wrong line. In your case there are two problems, but not where the compiler is pointing.
One being use of flatMap instead of map.
This part of the pipeline:
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
returns Publisher<[User], Error>.
Next you want to transforming that into Publisher<[UserViewModel], Error> for which you need a function map:
func map<T>(_ transform: #escaping (Output) -> T) -> Publishers.Map<Upstream, T>
which transforms one type of Output into another type of Output not flatMap:
func flatMap<T, P>(maxPublishers: Subscribers.Demand = .unlimited, _ transform: #escaping (Self.Output) -> P) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure
which transforms Output into a new Publisher
The second problem is with append(contentsOf;) which expects a Sequence of elements, in case of single elements you should use append(), but even simpler would be just to map the [User] to [UserViewModel]:
{ users in
users.map { user in
UserViewModel(with: user)
}
}
so the whole function should work with those changes:
static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
// prepare URL
let urlString = "\(baseURL)/search/users?q=\(query)"
guard let url = URL(string: urlString) else {
return Just([]).eraseToAnyPublisher()
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.map { users in
users.map { user in
UserViewModel(with: user)
}
}
.catch { err -> Just<[UserViewModel]> in
print(err)
return Just([])
}
.eraseToAnyPublisher()
return publisher
}

FlatMap with Generic ReturnType using Combine

I'm building a network API.
I'm new to Combine and I'm having some troubles with it, I'm trying to chain publish network requests, in this case I'm forming an URLRequest publisher and dispatching it on another publisher, the problem is that I cant make the flatMap work on the second publisher.
First I assemble the URLRequest with the Auth token:
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
return Deferred {
Future<URLRequest, NetworkRequestError> { promise in
if var urlComponents = URLComponents(string: baseURL) {
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
if let finalURL = urlComponents.url {
if let user = Auth.auth().currentUser {
print("##### final url -> \(finalURL)")
// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
user.getIDToken(completion: { (token, error) in
if let fbToken = token {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(headers ?? [:], uniquingKeysWith: { (first, _) in first })
print("##### API TOKEN() SUCCESS: \(defaultHeaders)")
promise(.success(request))
}
if let fbError = error {
print("##### API TOKEN() ERROR: \(fbError)")
promise(.failure(NetworkRequestError.decodingError))
}
})
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
}
}.eraseToAnyPublisher()
}
Then I'm trying to dispatch a request (publisher) and return another publisher, the problem is that the .flatMap is not getting called:
struct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
print("##### --------> \(request)")
//typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
return request.asURLRequest(baseURL: baseURL)
.flatMap { request in
//NOT GETTING CALLED
self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
The final publisher that is not being called is the following:
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
Running the code:
struct ReadUser: Request {
typealias ReturnType = UserData
var path: String
var method: HTTPMethod = .get
init(_ id: String) {
path = "users/\(id)"
}
}
let apiClient = APIClient(baseURL: BASE_URL)
var cancellables = [AnyCancellable]()
apiClient.dispatch(ReadUser(Auth.auth().currentUser?.uid ?? ""))
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
})
.store(in: &cancellables)
I took your code and boiled it down to just the Combine parts. I could not reproduce the issue you are describing. I'll post that code below. I recommend you start simplifying your code a bit at a time to see if that helps. Factoring out the Auth and Facebook token code seems like a good candidate to start with. Another good debugging technique might be to put in more explicit type declarations to make sure your closures are taking and returning what you expect. (just the other day I had a map that I thought I was applying to an Array when I was really mapping over Optional).
Here's the playground:
import UIKit
import Combine
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, Error> {
return Deferred {
Future<URLRequest, Error> { promise in
promise(.success(URLRequest(url: URL(string: "https://www.apple.com")!)))
}
}.eraseToAnyPublisher()
}
struct APIClient {
var networkDispatcher: NetworkDispatcher!
init(networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.networkDispatcher = networkDispatcher
}
func dispatch() -> AnyPublisher<Data, Error> {
return asURLRequest(baseURL: "Boo!")
.flatMap { (request: URLRequest) -> AnyPublisher<Data, Error> in
print("Request Received. \(String(describing: request))")
return self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
}
func httpError(_ code: Int) -> Error {
return NSError(domain: "Bad Things", code: -1, userInfo: nil)
}
func handleError(_ error: Error) -> Error {
debugPrint(error)
return error
}
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func dispatch(request: URLRequest) -> AnyPublisher<Data, Error> {
return urlSession
.dataTaskPublisher(for: request)
.tryMap({ data, response in
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
.mapError { error in
handleError(error)
}
.eraseToAnyPublisher()
}
}
let apiClient = APIClient()
var cancellables = [AnyCancellable]()
apiClient.dispatch()
.print()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
debugPrint(result)
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
debugPrint(value)
})
.store(in: &cancellables)
I refactored your code. Breaking down the offending method into several functions. I could not find any problem. Below is my refactoring. You will notice that I broke all the code that constructs things into their own functions so they can be easily tested without dealing with the effect (I don't even have to mock the effect to test the logic.)
extension Request {
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
guard let user = Auth.auth().currentUser else {
return Fail(error: NetworkRequestError.missingUser)
.eraseToAnyPublisher()
}
return user.idTokenPublisher()
.catch { error in
Fail(error: NetworkRequestError.badToken(error))
}
.tryMap { token in
makeRequest(
finalURL: try finalURL(baseURL: baseURL),
fbToken: token
)
}
.eraseToAnyPublisher()
}
func finalURL(baseURL: String) throws -> URL {
guard var urlComponents = URLComponents(string: baseURL) else {
throw NetworkRequestError.malformedURLComponents
}
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
guard let result = urlComponents.url else {
throw NetworkRequestError.malformedURLComponents
}
return result
}
func makeRequest(finalURL: URL, fbToken: String) -> URLRequest {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(
headers ?? [:],
uniquingKeysWith: { (first, _) in first }
)
return request
}
}
extension User {
func idTokenPublisher() -> AnyPublisher<String, Error> {
Deferred {
Future { promise in
getIDToken(completion: { token, error in
if let token = token {
promise(.success(token))
}
else {
promise(.failure(error ?? UnknownError()))
}
})
}
}
.eraseToAnyPublisher()
}
}
struct UnknownError: Error { }

JSON Decode Function From File Versus Web

SwiftUI is supposed to simplify things- I am bit frustrated as I have been working on the URLSession+JSONDecoder for weeks, I really need some help!
I have a function to load JSON data from a file in Swift and it works as expected. I copy/pasted the function and updated it to get the data via an API, however I receive a compile time error: "Unexpected non-void return value in void function". Is my approach wrong to use a function for JSON over the web?
JSON response:
{
"T":"CSU",
"v":468303,
"vw":1.2838,
"o":1.31,
"c":1.24,
"h":1.38,
"l":1.2001,
"t":1607374800000,
"n":994
}
struct Root2: Codable {
var T: String
var v: Double
var vw: Double
var o: String
var c: String
var h: Double
var l: Double
var t: Double
}
This file-based function works as expected:
let symbolData: [Root2] = load("symbolData.json")
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
For the web version, I receive compile time error: "Unexpected non-void return value in void function".
Line: return try decoder.decode(T.self, from: data)
func loadURL<T: Decodable>() -> T {
guard let url = URL(string: """)
else {
fatalError("Invalid URL in main bundle.")
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
do {
if let data = data {
let stringData = String(decoding: data, as: UTF8.self)
print("1 Fetched: \(url)")
print("2 Response: \(stringData)")
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}
catch {
fatalError("Couldn't parse as :\n\(error)")
}
}.resume()
}
Working version after Leo's help!
class Manager: ObservableObject {
#Published var symbols: [Symbol] = []
func loadURL<T: Decodable>(using decoder: JSONDecoder = .msSince1970, completion: #escaping ((T?, Error?) -> Void)) {
let url = URL(string: """)!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
print("ops")
completion(nil, error)
return
}
print("1 Fetched: \(url)")
print("2 Response:", String(data: data, encoding: .utf8) ?? "")
_ = Data("""
[
{
"open": {
"price": 124.02,
"time": 1657105851499
},
"close": {
"price": 124.96,
"time": 1618647822184
},
"high": 124.64,
"low": 124.65,
"volume": 75665274,
"symbol": "AAPL"
}
]
""".utf8)
do {
completion(try decoder.decode(T.self, from: data), nil)
//completion(try decoder.decode(T.self, from: tempDataForTesting), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
extension JSONDecoder {
static let msSince1970: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
return decoder
}()
}
You can't wait for an asynchronous task to finish and return the result. What you need is a completion handler. You would need also to explicitly set the resulting type if you don't pass the resulting type to your decode method and you need to call resume to start your url session data task:
import SwiftUI
struct ContentView: View {
#ObservedObject var manager = Manager()
#State var string: String = "Hello, world!"
var body: some View {
Text(manager.symbol)
.padding()
.onAppear {
manager.load(symbol: manager.symbol) { (symbols: [Symbol]?, error: Error?) in
guard let symbols = symbols else {
print("error:", error ?? "")
string = "JSON could not be parsed"
return
}
for symbol in symbols {
print(symbol.open.price)
print(symbol.open.time)
print(symbol.close.price)
print(symbol.close.time)
print(symbol.high)
print(symbol.low)
print(symbol.volume)
print(symbol.symbol)
DispatchQueue.main.async {
manager.symbols = symbols
}
}
string = "JSON was successufly parsed"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Manager: ObservableObject {
#Published var symbols: [Symbol] = []
#Published var symbol: String = "IBM"
func load<T: Decodable>(symbol: String, using decoder: JSONDecoder = .msSince1970, completion: #escaping ((T?, Error?) -> Void)) {
guard let url = URLComponents(symbol: symbol).url else {
completion(nil, URL.Error.invalidURL)
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
print("ops")
completion(nil, error)
return
}
print("1 Fetched: \(url)")
print("2 Symbol: \(symbol)")
print("3 Response:", String(data: data, encoding: .utf8) ?? "")
do {
completion(try decoder.decode(T.self, from: data), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
struct Symbol: Codable {
let open, close: Price
let high, low: Double
let volume: Int
let symbol: String
}
struct Price: Codable {
let price: Double
let time: Date
}
extension JSONDecoder {
static let msSince1970: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
return decoder
}()
}
extension URLComponents {
init(scheme: String = "https",
host: String = "sandbox.iexapis.com",
path: String = "/stable/stock/market/ohlc",
symbol: String,
token: String = "YOUR_API_TOKEN") {
self.init()
self.scheme = scheme
self.host = host
self.path = path
self.queryItems = [URLQueryItem(name: "symbols", value: symbol),
URLQueryItem(name: "token", value: token)]
}
}
extension URL {
enum Error: String, Swift.Error {
case invalidURL = "Invalid URL"
}
}
This will print
1 Fetched: https://sandbox.iexapis.com/stable/stock/market/ohlc?symbols=IBM&token=YOUR_API_TOKEN
2 Symbol: IBM
3 Response: [{"open":{"price":128.9,"time":1636600302693},"close":{"price":131.44,"time":1662259300134},"high":132.517,"low":130.074,"volume":3403359,"symbol":"IBM"}]
128.9
2021-11-11 03:11:42 +0000
131.44
2022-09-04 02:41:40 +0000
132.517
130.074
3403359
IBM

SwiftUI: API URL fetch failed

I'm trying to retrieve data from an API url
This is what the implementation guide reads, so the URL should match this format:
The request for information in JSON format is submitted as a GET
operation to the endpoint:
http://digit-eyes.com/gtin/v2_0/?upc_code=x&app_key=x&signature=x&language=x&field_names=x
This is my function that fetches the data from the JSON and decodes it from JSON.
I've replaced the signature and API Key with x.
Signature is generated by combining the app_key and the barcode forming a hashed value.
func loadData() {
guard let url = URL(string: "https://www.digit-eyes.com/gtin/v2_0/?upcCode=5901905880016&language=en&app_key=x&signature=x&language=en&field_names=description,brand,ingredients,image,upc_code") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, responce, error in
if let data = data {
if let decodedRepsonce = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async{
self.results = decodedRepsonce.results
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") //This is the error I get
}.resume()
}
Here's what I get when I paste the URL into safari
I've tested the URL with: "https://itunes.apple.com/search?term=radiohead&entity=song" and it works. A noticeable difference is that this link downloads a JSON file, my URL doesn't.
I store the JSON into an array Results:
struct Result: Codable {
var description: String
var brand: String
var ingredients: String
var image: String
var upc_code: Int
}
Which is then displayed in the body:
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
self.indicator.padding()
List(self.results, id: \.upc_code) { item in
VStack(alignment: .leading) {
Text(item.brand)
.font(.headline)
Text(item.description)
}
}
}
EDIT
Dealing with nulls from the JSON data
To call loadData, I have an .onAppear on a VStack in the body.
.onAppear {
//let signiture = self.scannedCode.barcode.hashedValue("Ls75O8z1q9Ep9Kz0")
self.loadData(url: "https://www.digit-eyes.com/gtin/v2_0/?upcCode=5901905880016&language=en&app_key=/9nOS+obsRF5&signature=DiKl4lURenoNe53I0a/i3kiAkQQ=&language=en&field_names=description,ingredients,brand,image") { error, result in
if let err = error {
print(err)
}
}
}
}
This is in a struct outside of the body
func loadData(url: String, completion: #escaping (Error?, Result?) -> Void) {
if let url = URL(string: url) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {return}
do {
let decoder = JSONDecoder()
let result: Result = try decoder.decode(Result.self, from: data)
completion(nil, result)
}
catch let e {
print(e)
completion(e, nil)
}
}
task.resume()
}
}
}
I'm now getting:
valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "brand", intValue: nil)], debugDescription: "Expected String value but found null instead.", underlyingError: nil))
In the JSON object, the brand name isn't always found, so it's sometimes null. I don't know how I can resume the decoder if a null is found.
Try this code to call in the body:
func loadData(url: String, completion: #escaping (Error?, Result?) -> Void) {
if let url = URL(string: url) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {return}
do {
let decoder = JSONDecoder()
let result: Result = try decoder.decode(Result.self, from: data)
completion(nil, result)
}
catch let e {
print(e)
completion(e, nil)
}
}
task.resume()
}
}
loadData(url: "https://google.com") { error, result in
if let err = error {
print(err)
}
}
Try to modify the struct as follows and add the other variables. I also noticed that upc_code is a String.
struct Result: Codable {
var description: String?
var brand: String?
var ingredients: String?
var image: String?
var upc_code: String?
var return_message: String?
var return_code: String?
}

Resources