Loading data from API pattern issue - ios

I am building an app that populates data in a collectionView. The data come from API calls. When the screen first loads I get the products and store them locally in my ViewController.
My question is when should I get the products again and how to handle screen changing. My data will change when the app is running (sensitive attributes like prices) , but I don't find ideal solution to make the API call each time viewWillAppear is being called.
Can anybody please tell me what is the best pattern to handle this situation. My first though was to check if [CustomObject].isEmpty on viewWillAppear and then make the call. Including a timer that check again every 10-15 minutes for example.
Thank you for your input.

I'm not sure what the data looks like and how your API in detail works, but you certainly don't have to call viewWillAppear when your API updates the data.
There are two possible solutions to be notified when your data is updated.
You can either use a notification that lets you know whether the API is providing some data. After the Data has been provided your notification then calls to update the collection view. You can also include in the objects or structs that contain the data from your API the "didSet" call. Every time the object or struct is being updated the didSet routine is called to update your collection view.
To update your collection view you simply call the method reloadData() and the collection view will update itself and query the data source that now contains the newly received data from your API.
Hope this helps.

There is no set pattern but it is advisable not to send repeated network requests to increase energy efficiency (link). You can check the time interval in ViewWillApear and send the network requests after certain gap or can use timer to send requests at time intervals. First method would be better as it sends request only when user is on that screen. You can try following code snippet to get the idea
class ViewController: UIViewController {
let time = "startTime"
let collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
update()
}
private func update() {
if let startDateTime = NSUserDefaults.standardUserDefaults().objectForKey(time) as? NSDate {
let interval = NSDate().timeIntervalSinceDate(startDateTime)
let elapsedTime = Int(interval)
if elapsedTime >= 3600 {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(startDateTime, forKey: time)
}
} else {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(NSDate(), forKey: time)
}
}
func makeNetworkRequest() {
//Network Request to fetch data and update collectionView
let urlPath = "http://MyServer.com/api/data.json"
guard let endpoint = NSURL(string: urlPath) else {
print("Error creating endpoint")
return
}
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
do {
guard let data = data else {
return
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else {
print("Error in json parsing")
return
}
self.collectionView.reloadData()
} catch let error as NSError {
print(error.debugDescription)
}
}.resume()
}

Related

UI Update Delegate Method Not Called

Working on a practice project (solution found here: https://github.com/appbrewery/ByteCoin-iOS13-Completed) where you swipe with a picker view to see the value of 1 Bitcoin in the selected currency.
Right now, I'm successfully retrieving and parsing the data from coinapi.io, but my delegate method to update the text labels isn't activating and I can't figure out why, even comparing it to the solution code. I'm not getting any errors and it runs fine, but this update method just isn't calling. Why not?
ViewController.swift
// CoinManager Delegate Extension Functionality
extension ViewController: CoinManagerDelegate {
func didUpdateCoin(currency: String, value: String) {
// Test to see if it's being called
print("didUpdateCoin Called")
DispatchQueue.main.async {
// Change the information presented to the user
self.currencyLabel.text = currency
self.bitcoinLabel.text = String(value)
}
}
// If it fails, print the error that occurred
func didFailWithError(error: Error) {
print(error)
}
}
This is where I'm calling it.
CoinManager.swift
var delegate: CoinManagerDelegate?
func performRequest(with urlString: String, currency: String) {
// If the url is valid
if let url = URL(string: urlString) {
// Create the URLSession to request the data
let session = URLSession(configuration: .default)
// Create the task and session with the url
let task = session.dataTask(with: url) { (data, response, error) in
// If there's an error
if error != nil {
// Call the error-handling function
self.delegate?.didFailWithError(error: error!)
// Return without any request being performed
return
}
// If the data is retrieved successfully
if let safeData = data {
// Parse the data
if let value = self.parseJSON(safeData) {
print("Got to just before didUpdateCoin")
// The value is being passed correctly
print(value)
// Not calling, just skipping passed didUpdateCoin
self.delegate?.didUpdateCoin(currency: currency, value: value)
print("Passed didUpdateCoin")
}
}
}
// Continue running
task.resume()
}
}
Output
Got to just before didUpdateCoin
9768.79
Passed didUpdateCoin
The problem is - you have not connected your delegate with second view where you want to update content
For make it works follow next steps:
1) Create Protocol
2) Create delegate variable with your protocol type, you can make it optional
3) Chose where you want to call method from your delegate
4) In related VC assign it itself
5) Subscribe to protocol
6) Add protocol stubs
7) Configure your functionality
For better understanding: https://www.youtube.com/watch?v=DBWu6TnhLeY

Swift - TableView datasource from URLSession task

I have to populate a TableView with some data fetched with an URLSession task. The source is an XML file, so i parse it into the task. The result of parsing, is an array that i pass to another function that populate another array used by TableView Delegates.
My problem is that TableView Delegates are called before task ends, so tha table is empty when i start the app, unless a data reloading (so i know that parsing and task work fine).
Here is viewDidLoad function. listOfApps is my TableView
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
checkInstalledApps(apps: <ARRAY POPULATED>)
listOfApps.delegate = self
listOfApps.dataSource = self
}
}
fetchData is the function where i fetch the XML file and parse it
func fetchData() {
let myUrl = URL(string: "<POST REQUEST>");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"
let postString = "firstName=James&lastName=Bond";
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.parser = XMLParser(data: data!)
self.parser.delegate = self
}
task.resume()
}
while checkInstalledApps is the function where i compose the array used by TableView Delegates.
func checkInstalledApps(apps: NSMutableArray){
....
installedApps.add(...)
installedApps.add(...)
....
}
So, for example, to set the number of rows i count installedApps elements
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (installedApps.count == 0) {
noApp = true
return 1
}
return installedApps.count
}
that are 0. Obviously, if i reload data, it's all ok.
My problem is the async call: first of that i used an XML accessible via GET request, so i can use XMLParser(contentsOf: myUrl) and the time is not a problem. Maybe if the XML will grow up, also in this way i will have some trouble, but now i've to use a POST request
I've tried with DispatchGroup, with a
group.enter() before super.viewDidLoad
group.leave() after task.resume()
group.wait() after checkInstalledApps()
where group is let group = DispatchGroup(), but nothing.
So, how can i tell to the tableview delegate to wait the task response and the next function?
thanks in advance
I would forget about DispatchGroup, and change a way of thinking here (you don't want to freeze the UI until the response is here).
I believe you can leave the fetchData implementation as it is.
In XMLParserDelegate.parserDidEndDocument(_:) you will be notified that the XML has been parsed. In that method call checkInstalledApps to populate the model data. After that simply call listOfApps.reloadData() to tell the tableView to reload with the new data.
You want to call both checkInstalledApps and listOfApps.reloadData() on the main thread (using DispatchQueue.main.async {}).
Also keep listOfApps.delegate = self and listOfApps.dataSource = self in viewDidLoad as it is now.
The cleaner way is to use an empty state view / activityIndicator / loader / progress hud (whatever you want), informing the user that the app is fetching/loading datas,
After the fetch is done, just reload your tableview and remove the empty state view / loader
Your problem is caused by the fact that you currently have no way in knowing when the URLSession task ended. The reloadData() call occurs almost instantly after submitting the request, thus you see the empty table, and a later table reload is needed, though the new reload should be no sooner that the task ending.
Here's a simplified diagram of what happens:
Completion blocks provide here an easy-to-implement solution. Below you can find a very simplistic (and likely incomplete as I don't have all the details regarding the actual xml parsing) solution:
func fetchData(completion: #escaping () -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.parser = XMLParser(data: data!)
self.parser.delegate = self
completion()
}
override func viewDidLoad() {
super.viewDidLoad()
fetchData() { [weak self] in self?.tableView.reloadData() }
}
Basically the completion block will complete the xml data return chain.

Handing an async call to Firestore

I have the following method inside my database.swift file:
func GetTestData(arg: Bool, completion: ([Tweet]) -> ()) {
let db = Firestore.firestore()
var tweets = [Tweet]()
db.collection("tweets").getDocuments() {
querySnapshot, error in
if let error = error {
print("unable to retrieve documents \(error.localizedDescription)")
} else{
print("Found documebts")
tweets = querySnapshot!.documents.flatMap({Tweet(dictionary: $0.data())})
}
}
completion(tweets)
}
This method connects to Firestore retrieve data from a given collection, convert it into an array and then passes it back, I call this function with the following (located within my table view controller):
func BlahTest() {
let database = Database()
print("going to get documents")
database.GetTestData(arg: true) { (tweets) in
self.tweets = tweets
self.tableView.reloadData()
}
print("after calling function")
}
The issue I have is when I run this my code is out of sync, and by that I mean print("after calling function") is called before print("Found documebts") which tells me it's not waiting for the async call to Firestore to finish, now I'm new to iOS development so would someone be willing to help me understand how I go about handling this?
Thanks in advance.
You are using closure block in your GetTestData() method. Anything that should be done after execution of this method must be done inside completion:
{
(tweets) in
self.tweets = tweets
self.tableView.reloadData()
// Do rest of stuff here.
}
Following are some resource about implementing async/await in swift like other languages:
1. Async semantics proposal for Swift
2. AwaitKit : The ES8 Async/Await control flow for Swift
3. Concurrency in Swift: One approach
4. Managing async code in Swift
Hope this helps :)

swift shouldPerformSegueWithIdentifier get data async

Im making a very basic app which has a search field to get data that is passed to a tableview.
What I want to do is run an Async task to get the data and if the data is succesfully fetched go to the next view, during the loading the screen must not freeze thats why the async part is needed.
When the user pressed the searchbutton I run the following code to get data in my
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
method.
var valid = true
let searchValue = searchField.text
let session = NSURLSession.sharedSession()
let url = NSURL(string: "https://someapi.com/search?query=" + searchValue!)
let task = session.dataTaskWithURL(url!, completionHandler: {(data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
if let theData = data {
dispatch_async(dispatch_get_main_queue(), {
//for the example a print is enough, later this will be replaced with a json parser
print(NSString(data: theData, encoding: NSUTF8StringEncoding) as! String)
})
}
else
{
valid = false;
print("something went wrong");
}
})
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
task.resume()
return valid;
I removed some code that checks for connection/changes texts to show the app is loading data to make the code more readable.
This is the part where I have the problem, it comes after all the checks and im sure at this part I have connection etc.
What happens is it returns true (I can see this because the view is loaded), but also logs "Something went wrong" from the else statement.
I Understand this is because the return valid (at the last line) returns valid before valid is set to false.
How can I only return true (which changes the view) if the data is succesfully fetched and dont show the next view if something went wrong?
Because you want the data fetching to be async you cannot return a value, because returning a value is sync (the current thread has to wait until the function returns and then use the value). What you want instead is to use a callback. So when the data is fetched you can do an action. For this you could use closures so your method would be:
func shouldPerformSegue(identifier: String, sender: AnyObject?, completion:(success:Bool) -> ());
And just call completion(true) or completion(false) in your session.dataTaskWithURL block depending on if it was successful or not, and when you call your function you give a block for completion in which you can perform the segue or not based on the success parameter. This means you cannot override that method to do what you need, you must implement your own mechanism.

Background API POST that handle failure

In my iOS app users complete transactions which I need to post back to the server. I've created a function to do this:
static let configurationParam = NSURLSessionConfiguration.defaultSessionConfiguration()
static var manager = Alamofire.Manager(configuration: configurationParam)
func postItemToServer(itemToPost:DemoItem) {
let webServiceCallUrl = "..."
var itemApiModel:[String: AnyObject] = [
"ItemId": 123,
"ItemName": itemToPost.Name!,
//...
]
ApiManager.manager.request(.POST, webServiceCallUrl, parameters: itemApiModel, encoding: .JSON)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
print("post success")
case .Failure:
print("SERVER RESPONSE: \(response.response?.statusCode)")
}
}
}
Currently I call this once a transaction is complete:
//...
if(transactionCompleted!) {
let apiManager = ApiManager()
apiManager.postItemToServer(self.item)
self.senderViewController!.performSegueWithIdentifier("TransactionCompletedSegue", sender: self)
}
//...
Where DemoItem is a CoreData object.
This all works as expected. However I need the ability to retry the POST request if it fails. For example if the network connection is down at the point of trying post to the server I need to automatically post the data once it becomes active again - at which point there may be several DemoItem's which need to be synced.
I'm new to Swift. In a similar Xamarin app I had a status column in my SQLite database which I set to 'AwaitingSync'. I then had an async timer that ran every 30 seconds, queried the DB for any items which had status='AwaitingSync' and then tried to post them if they existed. If it succeed it updated the status in the DB. I could implement something along the same lines here - but I was never really happy with that implementation as I had a DB query every 30 seconds even if nothing had changed.
Finally, it needs to be still work if the app is terminated. For example any items which weren't synced before the app is killed should sync once the app is resumed. What's the best way to approach this?
Edit
Based on Tom's answer I've created the following:
class SyncHelper {
let serialQueue = dispatch_queue_create("com.mycompany.syncqueue", DISPATCH_QUEUE_SERIAL)
let managedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
func StartSync() {
//Run on serial queue so it can't be called twice at once
dispatch_async(serialQueue, {
//See if there are any items pending to sync
if let itemsToSync = self.GetItemsToSync() {
//Sync all pending items
for itemToSync in itemsToSync {
self.SyncItemToServer(itemToSync)
}
}
})
}
private func GetItemsToSync() -> [DemoItem]? {
var result:[DemoItem]?
do {
let fetchRequest = NSFetchRequest(entityName: "DemoItem")
fetchRequest.predicate = NSPredicate(format: "awaitingSync = true", argumentArray: nil)
result = try managedContext.executeFetchRequest(fetchRequest) as? [DemoItem]
} catch {
//Handle error...
}
return result
}
private func SyncItemToServer(itemToSync:DemoItem) {
let apiManager = ApiManager()
//Try to post to the server
apiManager.postItemToServer(itemToSync:DemoItem, completionHandler: { (error) -> Void in
if let _ = error {
//An error has occurred - nothing need to happen as it will be picked up when the network is restored
print("Sync failed")
} else {
print("Sync success")
itemToSync.awaitingSync = false
do {
try self.managedContext.save()
} catch {
//Handle error...
}
}
})
}
}
I then call this when ever a transaction is completed:
//...
if(transactionCompleted!) {
let syncHelper = SyncHelper()
syncHelper.StartSync()
}
//...
And then finally I've used Reachability.swift to start the sync every time the network connection resumes:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reachability:Reachability?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//...
//Setup the sync for when the network connection resumes
do {
reachability = try Reachability.reachabilityForInternetConnection()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "reachabilityChanged:",
name: ReachabilityChangedNotification,
object: reachability)
try reachability!.startNotifier()
} catch {
print("Unable to create Reachability")
}
return true
}
func reachabilityChanged(note: NSNotification) {
let reachability = note.object as! Reachability
if reachability.isReachable() {
print("Network reachable")
let syncHelper = SyncHelper()
syncHelper.StartSync()
} else {
print("Not reachable")
}
}
}
This all seems to be working. Is this approach ok and have I missed anything which would improve it? The only gap I can see is if the network connectivity is active however the server throws an error for some reason - I guess I could then add a button for the user to retry any pending items.
Firstly, if your concern is whether the network connection is working, you shouldn't be polling at intervals. You should be using iOS's network reachability API to get notified when the network status changes. Apple provides a simple implementation of this and there are numerous alternative implementations online.
Since a sync status value should be a boolean flag, it's not as if a fetch request is a heavy-duty operation, especially if you use reachability. Not only should the fetch request be fast, you can update the flag after the fact in a single step-- use NSBatchUpdateRequest to set the flag to false on every instance you just sent to the server.
If you want to get the sync status out of the persistent store (not a bad idea since it's metadata), you'll need to maintain your own list of unsynced objects. The best way to do this is by tracking the objectID of the managed objects awaiting sync. That would be something like:
Get the objectID of a newly changed managed object
Convert that to an NSURL using NSManagedObjectID's URIRepresentation() method.
Put the NSURL on a list that you save somewhere, so it'll persist.
You can save the list in a file, in user defaults, or in the persistent store's own metadata.
When it's time to sync, you'd do something like:
Get an NSURL from your list
Convert that into an NSManagedObjectID using managedObjectIDForURIRepresentation(url:NSURL) (which is on NSPersistentStoreCoordinator)
Get the managed object for that ID objectWithID: on NSManagedObjectContext.
Sync that object's data.
Then on a successful sync, remove entries from the list.

Resources