Better alternative to DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) for data refresh - swift

I am looking for a better alternative to DispatchQueue.main.asyncAfter(deadline: .now() + 0.5 to refresh my data.
My data is dynamic and changes based on user action, it's important that the data the user sees is always up to date.
I am using Firebase Realtime Database, I am wondering whether I can alter my service file to refetch data any time something changes. here is my service file:
class Service {
static let shared = Service()
let BASE_URL = "https://firebaseurl.com/jsondata.json"
let calendar = Calendar.current
func fetchClient(completion: #escaping ([Calls]) -> ()) {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error)
return
}
guard let data = data else {return}
do {
let myDecoder = JSONDecoder()
myDecoder.dateDecodingStrategy = .secondsSince1970
let calls = try myDecoder.decode([Calls?].self, from: data).filter({
self.calendar.isDateInToday($0?.dateTime ?? Date(timeIntervalSinceReferenceDate: -123456789.0))
})
completion(calls.filter{$0?.callmade != true}.compactMap{ $0 })
} catch let error {
print("Failed to create JSON with error: ", error)
}
}.resume()
}
}
Currently in my Main Controller I am using:
func fetchClient() {
Service.shared.fetchClient { (client) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.client = client
self.collectionView.reloadData()
self.fetchClient()
}
}
}

You can use Realtime Database to observer changes in data.
https://firebase.google.com/docs/database/ios/read-and-write#listen_for_value_events

Related

Asynchronous call to API

I have a three year old barcode scanning ios app that has been making a synchronous call to an api with no problems until I was forced to use a slighly slower api. The latest version of XCode offer this advice: Synchronous URL loading of https://get-thumb.herokuapp.com/getThumb.php?objectid=20019 should not occur on this application's main thread as it may lead to UI unresponsiveness. Please switch to an asynchronous networking API such as URLSession.
The code below asychronusly loads an ArtObject with string data including a path to another api. How can I get the data from the second, getThumb api asynchronously and load it into the UI on the main thread?
func downLoadJson (forObject: String) {
let myURLStr = urlObj + forObject
print (myURLStr)
guard let downLoadURL = URL(string: myURLStr) else {return}
URLSession.shared.dataTask(with: downLoadURL) { (data, urlResponse, error) in
guard let data = data, error == nil, urlResponse != nil else {
print ("something is wrong in URLSessions call")
return
}
do {
let decoder = JSONDecoder()
let anObject = try decoder.decode(ArtObject.self, from: data)
/*
print("object Name is \(anObject.ObjectName)")
print("Creators is \(anObject.Creators)")
print("Medium is \(anObject.Medium)")
print("Titles is \(anObject.Titles)")
print("LabelUUID is \(anObject.LabelUUID)")
*/
gArtObject = anObject // this populates the global gArtObject with the local anObject so that the vals are avial in other funcs
var displayText = "ObjName: " + anObject.ObjectName
displayText += "\nCreator(s): " + anObject.Creators
displayText += "\nMedium: " + anObject.Medium
displayText += "\nTitle(s): " + anObject.Titles
displayText += "\nObjectNumber: " + String(anObject.ObjectNumber)
displayText += "\nComponentNumber: " + anObject.compNumber
//--- UI update must happen on main queue THIS IS THE PROBLEM
DispatchQueue.main.async {
self.objectDetails.text = displayText
self.imageVW.setImageFromURl(stringImageUrl: anObject.imageSmall)
}
} catch {
print("something wrong after download step")
print(error.localizedDescription)
}
}.resume()
extension UIImageView{
func setImageFromURl(stringImageUrl url: String){
if let url = NSURL(string: url) {
if let data = NSData(contentsOf: url as URL) {
self.image = UIImage(data: data as Data)
}
}
}
}
You can achieve this the same way you are loading your JSON.
In the setImageFromURl function remove the synchronous call to Data and use:
if let url = URL(string: url) {
URLSession.shared.dataTask(with: url) { (data, urlResponse, error) in
if let data = data {
DispatchQueue.main.async{
self.image = UIImage(data: data)
}
}
}.resume()
}

In SwiftUI, how can return a function only when an API request is finished? [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
I'm trying to learn a bit about making API calls in SwiftUI.
I have a function called loadData which runs via the onAppear modifier.
The goal of that function is to see if I currently have data in CoreData.
If there is no data in CoreData, then I'd like to call another function that makes the API call to get the data, but only return the fetched data.
With the example I have below, the getCurrentSol function returns before the async portion is finished. Resulting in no data being returned. What is the appropriate way for me to return the data?
As you can see, I did try a while(true) "trick". But for whatever reason, my results variable never even updates with the fetched data, even though the decodedData variable does contain the proper results.
}.onAppear(perform: loadData)
}
func loadData() {
print("data: \(storedData) ")
print("data.count: \(storedData.count)")
if(storedData.count == 0){
let fetchedData = getCurrentSol()
let currentSol = fetchedData.sol
print("fetchedData: \(fetchedData)")
print("currentSol: \(currentSol)")
}
}
func getCurrentSol() -> CuriosityRoverModel {
var results = CuriosityRoverModel(sol: 0, low: 0, high: 0, opacity: "Sunny", sunrise: "00:00", sunset: "00:00", month: "Month 0")
let urlString = "https://api.maas2.apollorion.com"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) {data, response, error in
DispatchQueue.main.async {
if let data = data {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(CuriosityRoverModel.self, from: data)
//This recieves the proper data, but it doesn't get written to the results var
print("decodedData: \(decodedData)")
results = decodedData
} catch {
print("Error: ", error)
}
}
}
}.resume()
// I thought this would be a way to wait for the data
// but results never gets updated so it ends up running endlessly
while(true){
if(results.sol > 0){
return results
}
}
//return results // This would just return the "empty" results var from above before the data is actually retrieved
}
}
There are many ways to achieve what you want. This is one approach, using a closure:
....
.onAppear(perform: loadData)
}
func loadData() {
print("data: \(storedData) ")
print("data.count: \(storedData.count)")
if (storedData.count == 0) {
getCurrentSol() { results in // <--- here
if let fetchedData = results {
let currentSol = fetchedData.sol
print("fetchedData: \(fetchedData)")
print("currentSol: \(currentSol)")
}
}
}
}
// use a completion closure to "return" your results when done, not before
func getCurrentSol(completion: #escaping (CuriosityRoverModel?) -> Void) {
let urlString = "https://api.maas2.apollorion.com"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) {data, response, error in
DispatchQueue.main.async {
if let data = data {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(CuriosityRoverModel.self, from: data)
print("decodedData: \(decodedData)")
completion(decodedData) // <--- here, return the results
} catch {
print("Error: ", error) // need to deal with errors
completion(nil) // <--- here, should return the error
}
}
}
}.resume()
}
}

How to call dataTask method several times with a counter?

I'm currently developing an application using SwiftUI.
I want to call dataTask method several times with while method, a flag, and a counter.
But my code doesn't work...
How could solve this problem?
Here is my code:
func makeCallWithCounter(){
var counter = 0
var flag = false
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.sample = try JSONDecoder().decode([Sample].self, from: responseData)
counter += 1
if counter > 4 {
flag = true
}
}catch{
print("Error: did not decode")
return
}
}
}
while flag == false {
task.resume()
}
}
UPDATED
func makeCallWithCounter(){
var day = 1
var date = "2020-22-\(day)"
var totalTemperature = 0
var counter = 0
var flag = false
// Set up the URL request
let endpoint: String = "https://sample.com/api/info/?date=\(date)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// parse the result as JSON, since that's what the API provides
DispatchQueue.main.async {
do{ self.sample = try JSONDecoder().decode([Sample].self, from: responseData)
day += 1
totalTemperature += self.sample.temperature
if day > 4 {
flag = true
}
}catch{
print("Error: did not decode")
return
}
}
}
while flag == false {
task.resume()
}
print(totalTemperature)
}
Xcode:Version 12.0.1
As I wrote in the comments you need a loop and DispatchGroup. On the other hand you don't need flag and counter and actually not even the URLRequest
I removed the redundant code and there is still a serious error: The line
totalTemperature += sample.temperature
cannot work if sample is an array. The question contains not enough information to be able to fix that.
func makeCallWithCounter() {
var totalTemperature = 0
let group = DispatchGroup()
for day in 1...4 {
// Set up the URL request
let endpoint = "https://sample.com/api/info/?date=2020-22-\(day)"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
continue
}
// make the request
group.enter()
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
defer { group.leave() }
if let error = error { print(error); return }
// parse the result as JSON, since that's what the API provides
do {
let sample = try JSONDecoder().decode([Sample].self, from: data!)
totalTemperature += sample.temperature
} catch {
print(error)
}
}
task.resume()
}
group.notify(queue: .main) {
print(totalTemperature)
}
}

API Exclude data if date is in the past

I am trying to exclude data from my API response if the nested list 'calls' contains past properties
Include this data in response:
[
{
"addressLineOne":"Test",
"addressLineTwo":"Test2",
"calls":{
"dateTime":1597932000, // a date in the future
},
]
Exclude this data:
[
{
"addressLineOne":"Test",
"addressLineTwo":"Test2",
"calls":{
"dateTime":1596193200 // a date in the past
},
]
I am using JSON decoder to make my api calls:
class Service {
static let shared = Service()
let BASE_URL = "url.com/JsonData"
func fetchClient(completion: #escaping ([Client]) -> ()) {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error.localizedDescription)
return
}
guard let data = data else {return}
do {
let clients = try JSONDecoder().decode([Client].self, from: data)
completion(clients)
} catch let error {
print("Failed to create JSON with error: ", error.localizedDescription)
}
}.resume()
}
}
Any direction would be much appreciated
Managed to solve it by adding a filter and using the built-in Calendar function to check the date:
class Service {
static let shared = Service()
let BASE_URL = "url.com/JsonData"
let calendar = Calendar.current
func fetchClient(completion: #escaping ([Client]) -> ()) {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error.localizedDescription)
return
}
guard let data = data else {return}
do {
let myDecoder = JSONDecoder()
myDecoder.dateDecodingStrategy = .secondsSince1970 // formats date
let clients = try myDecoder.decode([Client].self, from: data)
completion(clients.filter { self.calendar.isDateInToday($0.calls.dateTime) // filters dates upon completion
})
} catch let error {
print("Failed to create JSON with error: ", error.localizedDescription)
}
}.resume()
}
}
In my solution the API call completes before filtration, which is less-than-ideal as it means all data is downloaded prior to filtering, ideally i'd like the data to be filtered prior to download anyone who can point me in the right direction on achieving this is welcomed.
Also this solution only checks if the date is today, not if the date is in the future.

Data tasks outside ViewController

I'm gonna start with I'm currently learning swift + iOS so I'm by no means an experienced developer or one for that matter.
My goal is to separate any network calls that are currently done in my view controller to a dedicated class outside of it.
In this view controller i have a IBAction with the following code inside of it:
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
What I want, or I think I want to achieve is something like this:
let apiData = "somehow get it from outside"
Then when apiData has info stored in it, execute this next bit of code:
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
How would I achieve this? Thank you.
You can try
class API {
static func userLoginWith(email:String,password:String,completion:#escaping(_ token:String?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else { completion(nil) ; return }
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(apiData.data?.token)
} catch {
print("Decoder error: ",error")
completion(nil)
}
}.resume()
}
}
Inside the VC
API.userLoginWith(email:<##>,password:<##>) { (token) in
if let token = token {
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
}
}