Swift - Different output based on JSON response contents - json

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
}

Related

Errors while trying to create a list based on JSON in SwiftUI

I need my JSON data to be translated to a list as an output for an app I am creating.
I have found this tutorial, which seems to be using some old elements like "BindableObject":
https://www.youtube.com/watch?v=ri1A032zfLo
As far as I've checked several times, I went word for word in the NetworkingManager file being described from 1:52 in the video (just changing the names and the URL). My code:
import Foundation
import SwiftUI
import Combine
/*BindableObject was renamed to ObservableObject */
class NetworkingManager: ObservableObject{
var didChange: PassthroughSubject <NetworkingManager, Never>
var gitHubList = Root(items: []){
didSet{
didChange.send(self)
}
}
init() {
guard let url = URL(string: "https://api.github.com/search/repositories?q=CoreData&per_page=20") else { return }
URLSession.shared.dataTask(with: url) {(data, _, _) in guard let data = data else { return }
let gitHubList = try! JSONDecoder().decode(GitHubAPIlist.self, from: data)
DispatchQueue.main.async {
self.gitHubList = gitHubList
}
}.resume()
}
}
I get 2 errors:
'self' captured by a closure before all members were initialized
on the line with URLSession and
Return from initializer without initializing all stored properties
on the line with the } after the .resume() command
Are there some obsolete syntaxes in the code or am I missing something?
try something like this approach, to get you data from github. Works very well for me:
class NetworkingManager: ObservableObject{
#Published var gitHubList: [Item] = []
init() {
loadData()
}
func loadData() {
guard let url = URL(string: "https://api.github.com/search/repositories?q=CoreData&per_page=20") else { return }
URLSession.shared.dataTask(with: url) {(data, _, _) in
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(Root.self, from: data)
DispatchQueue.main.async {
self.gitHubList = response.items
}
} catch {
print("error: \(error)")
}
}.resume()
}
}
struct Root: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [Item]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct Item: Identifiable, Codable {
let keysURL, statusesURL, issuesURL: String
let id: Int
let url: String
let pullsURL: String
// ... more properties
enum CodingKeys: String, CodingKey {
case keysURL = "keys_url"
case statusesURL = "statuses_url"
case issuesURL = "issues_url"
case id
case url
case pullsURL = "pulls_url"
}
}
struct ContentView: View {
#StateObject var netManager = NetworkingManager()
var body: some View {
List {
ForEach(netManager.gitHubList) { item in
Text(item.url)
}
}
}
}
You declared the subject but didn't initialize it
var didChange = PassthroughSubject<NetworkingManager, Never>()
However actually you don't need the subject, mark gitHubList as #Published (and delete the subject).
And if you are only interested in the items declare
#Published var gitHubList = [Repository]()
and assign
DispatchQueue.main.async {
self.gitHubList = gitHubList.items
}
The #Published property wrapper will update the view.
In the view declare NetworkingManager as #StateObject
#StateObject private var networkManager = NetworkingManager()
and in the rendering area refer to
List(networkManager.gitHubList...
Notes:
Forget the tutorial , the video is more than two years old which are ages in terms of Swift(UI)'s evolution speed.
Ignoring errors and force unwrapping the result of the JSON decoder is very bad practice. Handle the potential URLSession error and catch and handle the potential decoding error.

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

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

Unable to read JSON from rest API

Trying to read this JSON Data:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
But my app keeps crashing and I'm struggling to understand why (still fairly new to swift). I think the data is stored in a dictionary and I'm just handling it incorrectly. Could someone please explain the correct way to decode this JSON and how I would show it on the view? I tried JSONSerialization in place of JSONDecoder but got the same results so not sure if that's the right direction.
Model:
struct Model: Codable{
var userId: Int? = nil
var id: Int? = nil
var title: String? = nil
var completed: Bool? = nil
enum CodingKeys: CodingKey{
case userId, id, title, completed
}
}
JSON Load Function:
func loadData(){
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
//create url request
var request = URLRequest(url: url)
//specify the method to use
request.httpMethod = "GET"
//set HTTP request Header, can set more than one.
request.setValue("application/json", forHTTPHeaderField: "Accept")
//send the request
let dataTask = URLSession.shared.dataTask(with: request){ data, response, error in
if let data = data{
if let users = try? JSONDecoder().decode(Model.self, from: data){
DispatchQueue.main.async {
self.dataInfo = users
}
}else{
print("Data Not Got")
}
}
if let response = response{
print("Response Got")
}
if let error = error{
print("\(error)")
}
}
dataTask.resume()
}
Swift UI View:
struct ContentView: View {
#State var dataInfo = Model()
var body: some View {
VStack{
Button("Go"){
loadData()
}
Text(dataInfo.title!)
}
By using ! after title, you're doing what is called a "force unwrap" -- telling the system that although the variable/property is declared as an Optional (in this case String?) that you're going to guarantee that it is not nil and there is in fact a value there. The problem is, before you've done the API call, that property is in fact nil, causing your program to crash.
Here's one way to change it (explanation follows):
struct Model: Codable{
var userId: Int
var id: Int
var title: String
var completed: Bool
}
struct ContentView: View {
#State var dataInfo : Model?
var body: some View {
VStack{
Button("Go"){
loadData()
}
if let dataInfo = dataInfo {
Text(dataInfo.title)
}
}
}
func loadData() {
// your previous code here
}
}
In this version, dataInfo is an Optional, and it gets set when the API call is made. Then, if let dataInfo = dataInfo does something called "optional binding," basically telling the system to only run the following code in the event that dataInfo isn't nil.
Finally, I've changed your Model to have non-Optional properties, since the API call you're using returns values for all of those fields. If you wanted to keep your previous model, you'd probably want to change my code to something like:
if let title = dataInfo?.title {
Text(title)
}
Check out the Swift Programming Language book for more information on Optionals and how to use them: https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html

Swift Issue Listing out a Dynamic Dictionary from JSON

I am trying to build an application which receives a JSON Object from an API endpoint, which then I want to list out in the view. I've watched a lot of videos on this topic, but in each video they use very simplistic JSON Objects as examples and therefore the code they write doesn't really seem to transfer over, giving me errors no matter how I try to format it. The code is as follows
import SwiftUI
import Combine
import Foundation
public struct ActivityModel: Codable, Identifiable {
public let id: Int
public let name: String
public let activity_desc: String?
}
public struct ActivitiesModel2: Codable {
public let location: String
public let popular: [String:ActivityModel]
}
public struct ActivitiesModel: Codable {
public let activities: ActivitiesModel2
}
public class ActivityFetcher: ObservableObject {
var activities: ActivitiesModel?
init(){
guard let url = URL(string: "https://mywebsite.com/api/loadapi") 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 decodedLists = try JSONDecoder().decode(ActivitiesModel.self, from: d)
DispatchQueue.main.async {
self.activities = decodedLists
}
} else {
print("No Data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct ActivityGuestView: View {
let networkingServiceGeneral = NetworkingServiceGeneral()
#ObservedObject var viewRouter: ViewRouter
#ObservedObject var fetcher = ActivityFetcher()
var body: some View {
// This is where my issues start
List(fetcher.activities?.activities.popular) { result in
VStack {
Text(result.name)
Text(result.activity_desc)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
This code, as I put it, gives me 5 errors. They are the following;
- Initializer 'init(_:rowContent:)' requires that '(key: String, value: ActivityModel)' conform to Identifiable
- Initializer 'init(_:rowContent:)' requires that '[String : ActivityModel]' conform to 'RandomAccessCollection'
-Value of optional type '[String : ActivityModel]?' must be unwrapped to a value of type '[String : ActivityModel]'
- Coalesce using '??' to provide a default when the optional value contains 'nil'
- Force-unwrap using '!' to abort execution if the optional value contains 'nil'
Some of these errors have options to fix it, but when I press fix it adds code but doesn't actually fix the error so I figured to just include them anyways. I'm still fairly new to Swift, but I know what some of it is asking, particularly the conforming to Identifiable, but it says that struct ActivitiesModel does not conform to identifiable when I try to add the tag, and the JSON Object doesn't have an ID for that section, so I can't ask the ID to make it identifiable.
Any help would be greatly appreciated, this has kind of been a wall right now.
EDIT: Here's the JSON
"activities": {
"location": "Dallas",
"popular": {
"10": {
"id": 38,
"name": "Adventure Landing Dallas",
"activity_desc": "Aquatic complex chain with additional land attractions including mini-golf, laser tag & go-karts.",
},
"12": {
"id": 40,
"name": "Jumpstreet",
"activity_desc": "None provided.",
},
}
}
}

reduce function is printing an empty dictionary [:]

I have reduced my dictionary keys successfully in this question as pseudo-code without a real json model. The goal which I accomplished in the previous question is to return only the keys that have matching values. So the output is a dictionary that looks something like this ["WoW": ["#jade", "#kalel"]. Exactly what I needed. Of course there could be other matches and I'd like to return those as well.
Now that I have a proper json model, the reduce function is printing out an empty dictionary [:]. Is it the type in .reduce(into: [String:[String]]() that is causing the issue?
All the data is printing so the json and model structure must be correct.
json
[
{
"id": "tokenID-tqkif48",
"name": "#jade",
"game": "WoW",
"age": "18"
},
{
"id": "tokenID-fvkif21",
"name": "#kalel",
"game": "WoW",
"age": "20"
}
]
UserModel
public typealias Users = [UserModel]
public struct UserModel: Codable {
public let name: String
public let game: String
// etc...
enum CodingKeys: String, CodingKey {
case name
case game
// etc...
Playground
guard let url = Bundle.main.url(forResource: "Users", withExtension: "json") else {
fatalError()
}
guard let data = try? Data(contentsOf: url) else {
fatalError()
}
let decoder = JSONDecoder()
do {
let response = try decoder.decode([UserModel].self, from: data)
for userModel in response {
let userDict: [String:String] = [ userModel.name:userModel.game ]
let reduction = Dictionary(grouping: userDict.keys) { userDict[$0] ?? "" }.reduce(into: [String:[String]](), { (result, element) in
if element.value.count > 1 {
result[element.key] = element.value
}
})
// error catch etc
}
Your code is too complicated. You can group the array by game simply with
let response = try decoder.decode([UserModel].self, from: data)
let reduction = Dictionary(grouping: response, by: {$0.game}).mapValues{ usermodel in usermodel.map{ $0.name}}
UPDATE I may be mistaking what you want to get. There's another code below and please check the results and choose one you want.
If you want to use reduce(into:updateAccumulatingResult:), you can write something like this.
do {
let response = try decoder.decode([UserModel].self, from: data)
let userArray: [(name: String, game: String)] = response.map {($0.name, $0.game)}
let reduction = userArray.reduce(into: [String:[String]]()) {result, element in
if !element.game.isEmpty {
result[element.name, default: []].append(element.game)
}
}
print(reduction)
} catch {
print(error)
}
If you prefer an initializer of Dictionary, this may work:
do {
let response = try decoder.decode([UserModel].self, from: data)
let userArray: [(name: String, games: [String])] = response.map {
($0.name, $0.game.isEmpty ? [] : [$0.game])
}
let reduction = Dictionary(userArray) {old, new in old + new}
print(reduction)
} catch {
print(error)
}
Both output:
["#jade": ["WoW"], "#kalel": ["WoW"]]
Anyway, your way of combining loop, Dictionary(grouping:) and reduce(into:) in addition of userDict.keys is making things too complex than they should be.
ADDITION When you want to get a Dictionary with keys as games:
do {
let response = try decoder.decode([UserModel].self, from: data)
let userArray: [(game: String, name: String)] = response.compactMap {
$0.game.isEmpty ? nil : ($0.game, $0.name)
}
let reduction = userArray.reduce(into: [String:[String]]()) {result, element in
result[element.game, default: []].append(element.name)
}
print(reduction)
} catch {
print(error)
}
Or:
do {
let response = try decoder.decode([UserModel].self, from: data)
let userArray: [(game: String, names: [String])] = response.compactMap {
$0.game.isEmpty ? nil : ($0.game, [$0.name])
}
let reduction = Dictionary(userArray) {old, new in old + new}
print(reduction)
} catch {
print(error)
}
Output:
["WoW": ["#jade", "#kalel"]]