The transition from "fetching" to "displaying" JSON API data Swift Node.js - json

I understand how to "fetch" data from a JSON API (my local server, in fact), but how should I think about the pipeline from merely having the data to displaying it in views? What I intuitively want to do is "return" the data from the fetching function, though I know that's not the paradigm that the Swift URL functions operate with. My thought is that if I can "return" the data (as a struct) it will be easy to pass into a view for visualization.
Sample Code:
This is the structure of the fetched JSON and the kind of variable I want to pass into views.
struct User: Codable {
let userID: String
let userName: String
let firstName: String
let lastName: String
let fullName: String
}
My hope is that the printUser function can return instead of print a successful fetch.
func printUser() {
fetchUser { (res) in
switch res {
case .success(let user):
print(user.userName) // return here?
// I know it won't work, but that's what seems natural
case .failure(let err):
print("Failed to fetch user: ", err)
}
}
}
func fetchUser(completion: #escaping (Result<User, Error>) -> ()) {
let urlString = "http://localhost:4001/user"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
if let err = err {
completion(.failure(err))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data!)
completion(.success(user))
} catch let jsonError {
completion(.failure(jsonError))
}
}.resume()
}
Sample view that would take a user struct
struct DisplayUser: View {
var user: User
var body: some View {
Text(user.userID)
Text(user.userName)
Text(user.lastName)
Text(user.firstName)
Text(user.fullName)
}
}

The reason that you can't just "return" is that your fetchUser is asynchronous. That means that it might return relatively instantaneously or it may take a long time (or not finish at all). So, your program needs to be prepared to deal with that eventuality. Sure, it would be "be easy to pass into a view for visualization" as you put it, but unfortunately, it just doesn't fit the reality of the situation.
What you can do (in your example) is set the User as an Optional -- that way, if it hasn't been set, you can display some sort of loading view and if it has been set (ie your async function has returned a value), you can display it. That would look something like this:
class ViewModel : ObservableObject {
#Published var user : User? //could optionally store the entire Result here
func runFetch() {
fetchUser { (res) in
switch res {
case .success(let user):
DispatchQueue.main.async {
self.user = user
}
case .failure(let err):
print("Failed to fetch user: ", err)
//set an error message here? Another #Published variable?
}
}
}
func fetchUser(completion: #escaping (Result<User, Error>) -> ()) {
//...
}
}
struct DisplayUser: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
if let user = viewModel.user {
Text(user.userID)
Text(user.userName)
Text(user.lastName)
Text(user.firstName)
Text(user.fullName)
} else {
Text("Loading...")
}
}.onAppear {
viewModel.fetchUser()
}
}
}
Note: I'd probably refactor the async stuff to use Combine if this were my program, but it's a personal preference issue

Related

How do you call an Async Throws function that connects to a remote API with nested JSON properties?

To preface, I am self taught and I know there are tutorials and similar questions on how to do something like this but with the small contextual differences something just isn't clicking. So any help on a solution or a tutorial more closely related to what I am trying to do would be very helpful.
I am trying to access a remote API but am struggling with the call function to execute the Async Throws function. I am trying to get the information from the JSON to be stored in a way that I can access it later for a calculation but have no idea what to put in the Task.
Minimal Reproducible Example-
Code:
// Struct Declarations
struct Response: Decodable {
let data: [StockValues]
}
struct StockValues: Decodable {
let high: Decimal
let low: Decimal
let close: Decimal
let volume: Decimal
}
// Async Throws Function
class dataFetcher {
static func Fetch() async throws -> [StockValues] {
guard let url = URL(string: "http://api.marketstack.com/v1/eod/latest")
else {
throw APIError.invalidServerResponse
}
var request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(from: url)
let stockValues = try JSONDecoder().decode(Response.self, from: data)
return stockValues.data
}
}
// Call Async Throws Function
func CallFunction() {
Task {
let fetchedInfo = try await dataFetcher.Fetch()
Response.data = fetchedInfo // Error: Instance member 'data' cannot be used on type 'Response'; did you mean to use a value of this type instead?
}
}
This code assumes that you have a key for the API and that your Response is correct
//Your error refers to
//`Response` is a type it can't hold a value so `Response.data` is unacceptable
struct Response: Decodable {
let data: [StockValues]
}
actor MarketStackService {
//What you are attempting to do is something like this setup
//Something global that anything can access. Using `static` to hold a response is not approriate.
static func fetchLatest() async throws -> [StockValues] {
//Add API key
guard let url = URL(string: "https://api.marketstack.com/v1/eod/latest")
else {
throw APIError.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
let stockValues = try JSONDecoder().decode(Response.self, from: data)
return stockValues.data
}
}
enum APIError: LocalizedError{
case invalidURL
case error(Error)
var errorDescription: String?{
switch self {
case .error(let error):
return error.localizedDescription
default:
let string = String(describing: self)
return NSLocalizedString(string, comment: "AppError")
}
}
}
There are many ways of setting this is up but this is a basic class that works well with SwiftUI. It can hold the value from the API call and trigger changes to the body
#MainActor
class MarketStackViewModel: ObservableObject{
//Variables are lowercased
//A variable can hold a value. That is why you need a `class` or `struct`
#Published var data: [StockValues] = []
#Published var alert: (isPresented: Bool, error: APIError?) = (false, nil)
//`func` are lowercased
func getLatest() async {
do{
let fetchedInfo = try await MarketStackService.fetchLatest()
data = fetchedInfo
}catch{
alert = (true, .error(error))
}
}
}
The View is where the value would be displayed and where the call can be triggered.
//`class` and `struct` are uppercased
struct MarketStackView: View {
#StateObject var vm: MarketStackViewModel = .init()
var body: some View {
VStack{
if vm.data.isEmpty{
Text("Hello, World!")
.task {
//Runs the request as soon as the View displays.
await vm.getLatest()
}
}else{
Text(vm.data.description)
//This is another way of making the call
Button("Get latest") {
Task{
await vm.getLatest()
}
}
}
}.alert(isPresented: $vm.alert.isPresented, error: vm.alert.error) {
Button("Ok") {
vm.alert = (false, nil)
}
}
}
}
struct MarketStackView_Previews: PreviewProvider {
static var previews: some View {
MarketStackView()
}
}
If you paste this code into a .swift file it should work and you can see the flow. I suggest you try the Apple SwiftUI Tutorials. They might help clear some concepts.

Error at generic helper function for reading JSON from an URL

In one of my projects, I want to read JSON from several URLs in several Views into several structs so I decided to write a small, but generic helper function.
This function should be called e.g.
View 1:
let call1 = Bundle.main.decode(iobrokerSection.self, from: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")
View 2:
let call2 = Bundle.main.decodeURL(iobrokerweather.self, from: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Weather")
and so on.
For the first example the struct iobrokerSection is
struct iobrokerNumberDataPoint: Codable {
var val: Int
var ack: Bool
var ts: Int
var q: Int
var from: String
var user: String
var lc: Int
var _id: String
var type: String
}
And here is my helper function
extension Bundle {
func decodeURL<T: Decodable>(_ type: T.Type, from urlString: String) -> T {
guard let url = URL(string: urlString) else {
fatalError("Placeholder for a good error message")
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let loaded = try? JSONDecoder().decode(T.self, from: data!) else {
fatalError("Placeholder for a good error message")
}
}.resume()
return loaded
}
}
I think that I understand why I'm getting the compiler message "Cannot convert return expression of type Bool to return Type T" at "return loaded".
But I don't have any idea how to fix this.
May anyone give me a hint?
First it is Swift naming convention to name all your structures, classes and protocols starting with an uppercase letter. Second you can't wait for an asynchronous method to finish to return a value. You need to add a completion handler to your method. Third you don't need to use a URLRequest if your intent is only to get some data. You can just use an URL and pass a URL to your method instead of a string. Fourth don't force unwrap the returned data it might be nil. You need to safely unwrap your optional data and in case of error pass it to the completion handler. Your decode method should look like something like this:
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from url: URL, completion: #escaping (T?, Error?) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
completion(nil, error)
return
}
do {
try completion(JSONDecoder().decode(T.self, from: data), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
When calling this method you need to get the asynchronous result inside the resulting closure:
struct IOBrokerNumberDataPoint: Codable {
var val: Int
var ack: Bool
var ts: Int
var q: Int
var from: String
var user: String
var lc: Int
var id: String
var type: String
enum CodingKeys: String, CodingKey {
case val, ack, ts, q, from, user, lc, id = "_id", type
}
}
let url = URL(string: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")!
Bundle.main.decode(IOBrokerNumberDataPoint.self, from: url) { brokerNumberDataPoint, error in
guard let brokerNumberDataPoint = brokerNumberDataPoint else {
print("error", error ?? "")
return
}
print("brokerNumberDataPoint", brokerNumberDataPoint)
// use brokerNumberDataPoint here
}
Another option is to use Swift 5 Result generic enumeration.
extension Bundle {
func decode<T: Decodable>(from url: URL, completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
if let error = error { completion(.failure(error)) }
return
}
do {
try completion(.success(JSONDecoder().decode(T.self, from: data)))
} catch {
completion(.failure(error))
}
}.resume()
}
}
Usage:
let url = URL(string: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")!
Bundle.main.decode(from: url) { (result: Result<IOBrokerNumberDataPoint, Error>) in
switch result {
case let .success(brokerNumberDataPoint):
print("brokerNumberDataPoint", brokerNumberDataPoint)
// use brokerNumberDataPoint here
case let .failure(error):
print("error:", error)
}
}

Swift - Different output based on JSON response contents

This question will probably come off as very basic, and something that probably can be found through searching, but despite my efforts of searching StackOverflow and just google I can't find any up-to-date thread or post regarding how to handle the different responses of a REST API, and, as I've found out, having an up-to-date thread is important to save trouble down the road when errors occur. So, to jump into it, I have an API endpoint on my server for logging in. It responds, as one would assume, with either two cases given login credentials;
If the login information succeeds, it returns this JSON Object:
{
"user": {
"id": 1,
"type": "user",
"name": "username",
"api_token": "accesstokenhere"
},
"access_token": "accesstokenhere"
}
If it doesn't succeed, it gives this response
{
"message": "Invalid credentials"
}
Now I have the login screen for my app, upon pressing "log in", submit the information to the server and get this response back, which is not of issue and very well documented. I have the following code so far:
import SwiftUI
import Combine
import Foundation
public struct UserModel: Decodable {
let id: Int
let username: String
let age: Int
enum CodingKeys: String, CodingKey {
case id = "id"
case username = "name"
case age = "age"
}
}
public struct UserResponse: Decodable {
public let user: UserModel
public let accessToken: String
enum CodingKeys: String, CodingKey {
case user = "user"
case accessToken = "access_token"
}
}
public class UserFetcher: ObservableObject {
public let objectWillChange = PassthroughSubject<UserFetcher,Never>()
#Published var hasFinished: Bool = false {
didSet {
objectWillChange.send(self)
}
}
var user: UserResponse?
#Published var incorrectLogin: Bool = false {
didSet {
objectWillChange.send(self)
}
}
init(){
guard let url = URL(string: "https://mywebsite.com/api/login") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
do {
if let d = data {
let decodedRes = try JSONDecoder().decode(UserResponse.self, from: d)
DispatchQueue.main.async {
self.user = decodedRes
self.hasFinished = true
print("Dispatching")
}
} else {
print("No Data")
}
} catch {
print("Error")
}
}.resume()
}
}
I have taken this section in its entirety except for minor tweaks to fit the different object from another file I have for a similar task, albeit that it has no alternate responses and so I didn't have to handle any other types of data responses.
I'm still fairly new to swift, so I have basic understanding of do-try-catch syntax, but I don't how I would catch different response models or where to place them in my code to prevent any errors from happening.
Ideally, I would like it to toggle the incorrectLogin variable, which can be observed and trigger a popup saying incorrect login information, as all login screens do when you input incorrect credentials. If it doesn't, it should just toggle the hasFinished variable and leave incorrectLogin as false, and then I would use the user model to do all of the behind the scenes stuff.
Again, I'm still fairly new to swift, I'm sure there's probably security issues here or something else I'm overlooking, and please, let me know if that's the case.
A suitable solution is an enum with associated values.
Add a struct for the error case
public struct ErrorResponse: Decodable {
let message : String
}
and the enum
enum Response : Decodable {
case success(UserResponse)
case failure(ErrorResponse)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let userData = try container.decode(UserResponse.self)
self = .success(userData)
} catch DecodingError.typeMismatch {
let errorData = try container.decode(ErrorResponse.self)
self = .failure(errorData)
}
}
}
After decoding the data switch on the result and handle the cases
do {
if let d = data {
let result = try JSONDecoder().decode(Response.self, from: d)
switch result {
case .success(let userData):
DispatchQueue.main.async {
self.user = userData
self.hasFinished = true
print("Dispatching")
}
case .success(let errorData):
print(errorData.message)
// handle the error
}
} else {
print("No Data")
}
} catch {
print(error) // never print a meaningless literal string in a Decoding catch block
}

Swift Global Variables

I'm a beginner at Swift so let me know if this doesn't quite make sense, but i have a JSON file that i can access in swift and parse into an array, from there i can get a string from the array and store it in a var. I want to be able to access this variable globally but i'm not sure how to do it.
With the help of another user "rmaddy". I have this code:
struct Games: Decodable {
let videoLink: String
}
class BroadService {
static let sharedInstance = BroadService()
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
let jsonUrlString = "LINK IS HERE."
guard let url = URL(string: jsonUrlString) else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
}
}
I can then access it in another class from here:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
let game = games[indexPath]
let videoLink = game.videoLink
}
I want to be able to access the contents of "videoLink" globally, without having to use "BroadService.sharedInstance.fetchBroadcasts { (games) in" how would i go about doing this
You shouldn't use global variables, I don't think that's recommended in any language.
Now here you have what looks like a Singleton class (BroadService), that's good because it's a nice solution for what you're looking for.
Next all you need to do is add a property to that class. Let's say videoLink is a string, you can add a string property to BroadService, for example storedVideoLink as an optional String, and the next time you need to obtain that value after you have already fetched it, you can access it like so: BroadService.sharedInstance.storedVideoLink.
One more thing, to have BroadService work properly as a singleton, you should make its init private.
To sum up, here's what I'm suggesting:
class BroadService {
static let sharedInstance = BroadService()
var storedVideoLink: String?
private init() {} // to ensure only this class can init itself
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
// your code here
}
}
// somewhere else in your code:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
let game = games[indexPath]
let videoLink = game.videoLink
BroadService.sharedInstance.storedVideoLink = videoLink
}
}
// now you can access it from anywhere as
// BroadService.sharedInstance.storedVideoLink
This way it all stays cohesive in the same class. You can even add a getter method for storedVideoLink so you don't have to access it directly, and in this method you could state that if the string is nil then you fetch the data, store the link to the string, and then return the string.
You could create a file with a struct called something like Global and create a static var and set that inside your completion block once you have fetched the games.
Here is an example.
struct Global {
static var games:[Any]? = nil
static func setGames(games:[Any]) {
Global.games = games
}
}
Then you fetch the data once upon load of the app or somewhere before you use the Global and set that property:
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
Global.setGames(games: games)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
Please note that this will make the Global.games accessible from everywhere but it will also not be a constant so you should be careful not to override it.
This way Global.games will be accessible from anywhere.

Swift how to reuse my JSON HTTP Request header

I am making an application which makes a lot of requests from an API. So I don't want to copy and past the code over and over. I was wondering how I can reuse my code in a some more efficient way? Maybe with extensions?
This is my code know:
func apiRequest() {
let config = URLSessionConfiguration.default
let username = "****"
let password = "****"
let loginString = String(format: "%#:%#", username, password)
let userPasswordData = loginString.data(using: String.Encoding.utf8)
let base64EncodedCredential = userPasswordData?.base64EncodedString()
let authString = "Basic " + (base64EncodedCredential)!
print(authString)
config.httpAdditionalHeaders = ["Authorization" : authString]
let session = URLSession(configuration: config)
var running = false
let urlProjects = NSURL(string: "https://start.jamespro.nl/v4/api/json/projects/?limit=10")
let task = session.dataTask(with: urlProjects! as URL) {
( data, response, error) in
if let taskHeader = response as? HTTPURLResponse {
print(taskHeader.statusCode)
}
if error != nil {
print("There is an error!!!")
print(error)
} else {
if let content = data {
do {
let dictionary = try JSONSerialization.jsonObject(with: content) as! [String:Any]
print(dictionary)
if let items = dictionary["items"] as? [[String:Any]] {
for item in items {
if let description = item["Description"] as? String {
self.projectNaam.append(description)
}
if let id = item["Id"] as? String {
self.projectId.append(id)
}
if let companyId = item["CompanyId"] as? String {
self.companyId.append(companyId)
}
}
}
self.apiRequestCompani()
}
catch {
print("Error: Could not get any data")
}
}
}
running = false
}
running = true
task.resume()
while running {
print("waiting...")
sleep(1)
}
}
Yes, you can use Extensions to create a BaseViewController and extend that where you want to use your code over and over again. Then you should abstract all dynamic data over input parameters to that method.
import UIKit
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
func getApiRequest (Parameters) {
//API Request
}
And then in your view controller you just extend BaseViewController
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Call method in baseviewcontroller
getApiRequest(parameters)
//Call method in self
self.getApiRequest(parameters)
}
override func getApiRequest(Parameters) {
//IF you need to override default configuration
}
So I don't want to copy and past the code over and over.
Absolutely right, no one aiming to get duplicated code; That's the issue of massive view controller. This issue appears since the view controller layer in your application handles most of the responsibilities, such as: getting data from the network, how data should be represented, deliver the formatted data to the view layer, etc...
There are many approaches for solving such an issue (using an appropriate architectural pattern for your application), for simplicity, I would recommend to apply the MVC-N (or MVCNetworking) approach into your app, it is almost the same usual MVC, with a separated files (managers), represent a new layer for handling -for instance- the integration with the external APIs.
Applying the MVN-N should not be that complex, nevertheless it needs to be described well (which might be too abroad to be descried in the answer), I would suggest to check the above mentioned apple example, also watching this video should be useful.