Calling ObservableObject class initialiser to update List in SwiftUI - json

I have a List that is updated with a Fetch class, an ObservableObject. It has an init function. This is that Fetch class.
#Published private(set) var items: [ItemsResult] = []
init() {
let url = URL(string: "[redacted]")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let itemsData = data {
let decodedData = try JSONDecoder().decode([ItemsResult].self, from: itemsData)
DispatchQueue.global(qos: .utility).async {
DispatchQueue.main.async {
print("running task")
self.items = decodedData
}
}
print(self.items)
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
When the app is build, it correctly displays the data returned by the API and it matches the database. However when I tap / click on one in order to delete it, or use the textarea I've added to add a new item it doesn't update.
struct TickrApp: View {
#EnvironmentObject var fetch: Fetch
var body: some View {
NavigationView {
Form {
Section {
VStack(alignment: .center) {
Text("Welcome to Tickr")
}
}
Section {
List(fetch.items) { item in
CheckView(checked: item.done, title: item.content.replacingOccurrences(of:"_", with: " "))
}
}
AddItemView()
}.navigationBarTitle(Text("Tickr"))
}
}
}
The database is being updated as shown when I log the decodedData they respond, however in each I just call Fetch(). Requests are made the same in all three cases.
One of the calls, for text input.
func toggle() {
checked = !checked
let url = URL(string: "")!
var req = URLRequest(url: url)
req.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: req) { data, response, error in
guard let _ = data,
let response = response as? HTTPURLResponse,
error == nil else {
print("error", error ?? "Unknown error")
return
}
guard (200 ... 299) ~= response.statusCode else {
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
return
}
}
task.resume()
Fetch()
In order to update the list visually I need to completely quit the app / rerun it in order to have the new and/or deleted items show correctly. No errors show about background publishing changes or anything.

It appears that you're trying to call Fetch() to refresh your data. There are two things that are going to be a problem with this:
You're calling it outside of the dataTask completion handler. That means that it may get called before the write finishes
Calling Fetch() just creates a new instance of Fetch, when what you really want to do is update the results on your existing instance.
I'm assuming that your first code snipped is from Fetch. I'd change it to look more like this:
class Fetch: ObservableObject {
#Published private(set) var items: [ItemsResult] = []
init() {
performFetch()
}
func performFetch() {
//your existing fetch code that was in `init`
}
}
Then, in your AddItemView and CheckView, make sure you have this line:
#EnvironmentObject var fetch: Fetch
This will ensure you're using the same instance of Fetch so your list will reflect the same collection of results.
Once you're done with an operation like toggle(), call self.fetch.performFetch() to update your results. So, your last code snippet would turn into something like this:
let task = URLSession.shared.dataTask(with: req) { data, response, error in
//guard statements to check for errors
self.fetch.performFetch() //perform your refresh on the *existing* `Fetch` instance
}
A bigger refactor would involve moving your async code (like toggle) to a view model, instead of doing any of it in a View code. Also, look into using the URLSession Publishers using Combine, since you're using SwiftUI: https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine

Related

Can't access decoded JSON data (from API) outside the URLSession data task Function Scope

I am facing an issue when trying to retrieve and decode JSON data from an API. I am able to get the data from the API and decode it successfully, but I can't access it from outside the function scope. I am using the following function to get and decode data. The API returns an array of JSON objects.
func getJSON(completed: #escaping () -> ()) {
var jsonData = [API_data] () // A struct for retrieved data
let url = "URL Here" // I have the original URL here, which I can't share
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { [self]data,response,error in
guard let data = data, error == nil else{
print("error")
return
}
do{
jsonData = try JSONDecoder().decode([API_data].self,from:data)
} catch{
print(error)
}
print(jsonData[0].id) // This prints my data
})
print(jsonData[0].id) // This won't print my data
task.resume()
}
struct API_data: Codable {
let id : String
}
As a result of this, I cannot use this data anywhere in the application. Any help will be appreciated. I have also tried making jsonData a global variable, updating it in the function and returning it and then using it, still doesn't work.
Thanks for your help.
As you already have a completion handler use it and pass the received data
func getJSON(completed: #escaping ([API_data]) -> Void) {
let url = "URL Here" // I have the original URL here, which I can't shar
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _ , error in
if let error = error { print(error); return }
do {
completed(try JSONDecoder().decode([API_data].self,from:data!))
} catch{
print(error)
}
}
task.resume()
}
and use it
getJSON { apiData in
print(apiData[0].id) // This prints my data
}
Or more comfortable with the Result type
func getJSON(completed: #escaping (Result<[API_data],Error>) -> Void) {
let url = "URL Here" // I have the original URL here, which I can't shar
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _ , error in
if let error = error { completed(.failure(error)); return }
Result { try JSONDecoder().decode([API_data].self,from: data!) }
}
task.resume()
}
getJSON { result in
switch result {
case .success(let apiData): print(apiData[0].id) // This prints my data
case .failure(let error): print(error)
}
}
It is asynchronous, you might want to access it a bit later when it’s ready, e.g. by using the completion handler.
Try creating a completion handler from your API function and send the data with the completion. And try using it there.

How to use data decoded from JSON in SwiftUI

Problem
The data is successfully decoded from Reddit’s api and put into the variable theUser inside the .onAppear {…}, but when I try to use it I keep getting nil values.
Code
struct ContentView: View {
#State var didAppear = false
#State var theUser = getNilUser()
var body: some View {
Text("User Profile")
Text(theUser.data.subreddit.display_name_prefixed ?? "No name found")
.onAppear(perform: {
theUser = getUser(withName: "markregg")
})
}
func getUser(withName username: String) -> user {
if !didAppear {
if let url = URL(string: "https://www.reddit.com/user/\(username)/about.json") {
do {
let data = try Data(contentsOf: url)
do {
let decodedUser = try JSONDecoder().decode(user.self, from: data)
return decodedUser
} catch {
print(error)
}
} catch {
print(error)
}
}
didAppear = true
}
return getNilUser()
}
}
Edit: I’m very new to this so if I made a stupid mistake I’m sorry
You can't get JSON data like that. You have to execute a HTTP request using the URLSession class in order to get the data and parse it.
let username = "markregg"
let url = URL(string: "https://www.reddit.com/user/\(username)/about.json")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print(error)
} else if let data = data {
do {
let decodedUser = try JSONDecoder().decode(User.self, from: data)
} catch {
print(error)
}
} else {
print("Request failed")
}
}.resume()
Another thing to note is that URLSession methods are asynchronous. In simple terms it means they take some time to execute and return the result. So you can't use a simple return statement like return decodedUser to get the result right away. Look into how to use URLSession more.

JSON networking request not entering URLSession.shared.dataTask

I am having problems finding out why my dataTask returns an empty result.
While going through My NetworkingManager class it appeared that it never enters the URLSession.shared.dataTask. Does anyone know why?
Her is my NetworkingManager which is being used in the ContentView of the app:
class NetworkingManager: ObservableObject {
var didChange = PassthroughSubject<NetworkingManager, Never>()
var showList = ShowResultsAPI(results: []) {
didSet {
didChange.send(self)
}
}
init() {
guard let url = URL(string: "www.json.url") else {
return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else {
return }
let showList = try! JSONDecoder().decode(ShowResultsAPI.self, from: data)
DispatchQueue.main.async {
self.showList = showList
}
}.resume()
}
}
In my opinion your coding looks correct.
Keep in mind that the request is asynch. When your debugging the URLSession.shared.dataTask you will recognize that at first the debugger is skipping the dataTask. When the URLSession receives a response it will enter the URLSession.shared.dataTask again.
I would recommend to set a breakpoint in the line with your guard statement. Then debug the process again and see if the debugger enters the process.
It would also be interesting to observe the response and the error in the completion handler to see if there are errors occuring.
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let data = data
else {
print("ResponseProblem")
print(response)
return
}
add return value in guard : return "example"
change your code to this :
}
}.resume()
sleep(2)
}
Adding a .sleep(2) at the end of the init() helped to process the JSON.
Edited:
It needed an asynchronous task which implies having a sleep or as #vadian suggested a better suited delay() from combine.

retrieved values position(order) changed every time in swift3

I retrieved values from Json, i print retrieved values order changed(position changed) every time. please check my below code once:
override func viewDidLoad() {
super.viewDidLoad()
var myIds = [104016, 104010, 104014, 104018, 104000, 104038, 104015, 104011, 104015, 104010, 104010, 104010, 104003, 104003, 104011]
for arr in 0 ..< myIds.count-1 {
let url = URL(string: "http://.........\(myIds[arr])")
print("myIds[\(arr)]:\(myIds[arr])")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil
{
print ("ERROR")
}
else
{
if let content = data
{
do
{
//Array
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
var i = myJson["Job_title_name"]!
var ist = ""
// print("i=\(i)")
ist = String(describing: i!)
print("myIds[\(arr)]=\(ist)")
}
catch
{
}
}
}
}
task.resume()
}
}
**output:**
myIds[0]:104016
myIds[1]:104010
myIds[2]:104014
myIds[3]:104018
myIds[4]:104000
myIds[5]:104038
myIds[6]:104015
myIds[7]:104011
myIds[8]:104015
myIds[9]:104010
myIds[10]:104010
myIds[11]:104010
myIds[12]:104003
myIds[13]:104003
myIds[0]=WebLogic Admin
myIds[3]=OracleDevloper
myIds[2]=Node Js Developer
myIds[1]=Angular Developer
myIds[6]=HTML&CSS Developer
myIds[5]=Senior Manager
myIds[7]=CRM Developer
myIds[4]=Windows Manager
myIds[8]=HTML&CSS Developer
myIds[9]=Angular Developer
myIds[11]=Angular Developer
myIds[10]=Angular Developer
myIds[12]=java developer
myIds[13]=java developer
This output is changed every time, when stop and run the app. but i'm unable to find the problem. please check my code once.
Required output:
myIds[0]:104016
myIds[1]:104010
myIds[2]:104014
myIds[3]:104018
myIds[4]:104000
myIds[5]:104038
myIds[6]:104015
myIds[7]:104011
myIds[8]:104015
myIds[9]:104010
myIds[10]:104010
myIds[11]:104010
myIds[12]:104003
myIds[13]:104003
myIds[0]=WebLogic Admin
myIds[1]=Angular Developer
myIds[2]=Node Js Developer
myIds[3]=OracleDevloper
myIds[4]=Windows Manager
myIds[5]=Senior Manager
myIds[6]=HTML&CSS Developer
myIds[7]=CRM Developer
myIds[8]=HTML&CSS Developer
myIds[9]=Angular Developer
myIds[10]=Angular Developer
myIds[11]=Angular Developer
myIds[12]=java developer
myIds[13]=java developer
i want the above output. In the above code whats my mistake.
URLSession.shared.dataTask() usually call API in parallel execution so no matter if calling request order will same as response order.
If you want to call request and get response in same order then you should create NSOperation queue and make dependency on it.
Network requests run asynchronously, so this is the expected behaviour. The requests don't finish in the same order as they were started.
If you need your requests to run sequentially, you can run them using DispatchGroups, but this will lead to slower performance due to the fact that your asynchronous requests are executed sequentially instead of in parallel. A better solution is to store the results in a data structure, where you can identify the objects based on a unique identifier other than their index. For your current problem, the best solution is to store the results in a dictionary, where the key is the id and the value is the value from the network request.
Concurrent solution using a Dictionary to store the output values:
override func viewDidLoad() {
super.viewDidLoad()
var myIds = [104016, 104010, 104014, 104018, 104000, 104038, 104015, 104011, 104015, 104010, 104010, 104010, 104003, 104003, 104011]
var jobTitles = [Int:String]()
let group = DispatchGroup()
for arr in 0 ..< myIds.count-1 {
let url = URL(string: "http://.........\(myIds[arr])")
print("myIds[\(arr)]:\(myIds[arr])")
group.enter()
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil{
print ("ERROR")
} else {
if let content = data{
do {
guard let myJson = try JSONSerialization.jsonObject(with: content, options: []) as? [String:Any] else {return}
guard let jobTitle = myJson["Job_title_name"] as? String else {return}
jobTitles[myIds[arr]] = jobTitle
group.leave()
} catch {
}
}
}
}
task.resume()
}
}
}
group.notify(queue: DispatchQueue.main, execute: {
print(jobTitles)
})
Some general advice: don't force unwrap values from a server response and don't use String(describing:) to create a String. Use optional binding or default values to safely unwrap the optionals and either cast your values to String if they are String values or just use String(value) for values from which Strings can be directly initialized (such as Int).

Unexpected non-void return value in void function - JSON Data from dataTask(with: URL) - (Swift 3.0)

I'm building an iOS app in Swift 3 that's supposed to communicate with a JSON Rest Api that I'm also building myself. The app will get all sorts of content from the Api, but for now all I need it to do is check the availability of the Api through a handshake function.
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error)
} else {
if let urlContent = data {
do {
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
if jsonResult["response"] as! String == "Welcome, come in!" {
print("************ RESPONSE IS: ************")
print(jsonResult)
return
} else {
return
}
} catch {
print("************ JSON SERIALIZATION ERROR ************")
}
}
}
}
task.resume()
This is the dataTask I've set up and it runs just fine (When I print the jsonResult, I get the "Welcome!" message as expected. The problem is that I want my handshake function to return true or false (So that I can give an alert if the case is false.) When I try to set up a return true or false within the if-statement, I get the error: Unexpected non-void return value in void function.
My question is: How do I return the data out of the dataTask so that I can perform checks with it within my handshake function? I'm very new to Swift so all help is appreciated :)
Below is the entire class:
import Foundation
class RestApiManager: NSObject {
var apiAvailability:Bool?
func handshake() -> Bool {
let url = URL(string: "https://api.restaurapp.nl/handshake.php")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error)
} else {
if let urlContent = data {
do {
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
if jsonResult["response"] as! String == "Welcome, come in!" {
print("************ RESPONSE IS: ************")
print(jsonResult)
return true
} else {
return false
}
} catch {
print("************ JSON SERIALIZATION ERROR ************")
}
}
}
}
task.resume()
}
}
Because you're using an asynchronous API, you can't return the bool from your handshake function. If you want to show an alert in the false case, you would replace the return false with something like:
DispatchQueue.main.async {
self.showAlert()
}
Technically you could make the handshake function pause until the network stuff was done, and return the bool, but that defeats the purpose of being asynchronous, and it would freeze your app's UI during the network activity, so I doubt that's what you want to do.