Rightness of waiting for a network call to complete - iOS - ios

I'm curious on the user experience for an user, while they wait for a network call to complete over cancelling the existing non deterministic request. Let me add more context. I'm prefetching data for my app that is later used. When the user hits the button, we use this data to load a screen. Instead of showing a spinner to the user and waiting on the network call to complete, we wanted to give them a better user experience.
class TestInteractor {
var currentTask: HTTPTask?
var content: SomeContent?
func getData(_ id: String, completion: Result<SomeContent, Error>) {
currentTask = URLSession.shared().dataTask(with: request) {
// check for no error
// set content here
}
}
var hasContent: Bool {
return content != nil
}
}
Here is the issue, if the prefetch is still in process (due to a bad network) should I let the user wait until this call completes or just cancel the task and restart a new call.
Canceling an existing network call can be implemented as below:
func getData(_ id: String) {
if let task = currentTask {
task.cancel()
currentTask = nil
}
// Continue with a new network call
}
Or should I add a new property to the TestInteractor and check if the network is still in progress and wait?
var isNetworkCallInProgress: Bool {
return currentTask?.state == running
}

There could be numerous reasons why a network request hasn’t completed yet; your server may be a bit overwhelmed; the client’s network speed may be a bit slow. It may be a waste to abort the work and start over. And whose to say that restarting the task is going to change any current impediment.
I’d say wait on the running task until it completes. If the pre-fetch completes before we need it, great, the pre-fetch saved time. But if it’s not yet done by the time we need it, if you let it finish, that’ll still save time rather than restarting it (the restarted task isn’t gonna magically be faster than the previous one just because we restarted it) so the pre-fetch was useful in this case too. So by allowing the request to complete, you’re maximizing the utility of the pre-fetch mechanism. Plus, if you choose to restart a task because pre-fetch couldn’t complete in time, what if your average user is actually faster than your average serving time for that request? Lol who knows, you might end up doubling your server load for the average case. Better that you have a design that is decoupled from things like that.

First, your app has a network activity indicator. Make a counter of how many network tasks you started, how many have finished, and turn the network activity indicator on or off when the count changes from 0 to 1 or from 1 to 0. That shows network activity very nicely.
Second, in your data model you will have items with correct data, items that are being loaded, and items that are not being loaded. Write code to display each kind of item. When a network request finishes, it updates your data model, and that redraws the corresponding item.

You can give the user a choice, you could add a refresh button to reset the call or let them wait for it.
If you want to ask them if it's working, you could just push an alert asking them if they want to refresh the call while running the prefetch in the background.
let alert = UIAlertController(title: "Message Title", message: "body text", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "button", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
This is the code for an alert. I would personally check and see if the process is taking long and then push out the alert asking the to either refresh or wait.

Related

How to limit number of requests to a database

I have an app with a settings page where the settings of each user are stored in a MySQL database. I was wondering what is the best way to update the database for every setting the user changes while sending the minimal number of requests as I'm worried that it will crash if it sends too many( it has happened before).
I was thinking about setting a timer for ~5 seconds when the user first changes a setting, and then reset the timer to 5 seconds again if another setting is changed. Once that timer is finished it will send a request to the server to update all the settings at once. It would also constantly store the new values locally to the app, so if the user closes the app before the 5 seconds are up it will send the request once/if the app loads up again.
Is this viable/what's the best way to go about this?
You need to make some logic functions in your app, so i will try make an pseudo codes below. Hope it will give you an idea. I don`t know the MySQL details but i am trying to explain native Swift way.
First of all you should fetch data partly, I mean if you try to fetch all data at the same time your app can work very slow.. That is why we are doing pagination in the app.
As well as pagination you want to refresh the data fresh 5 seconds so i will use Timer object, which will trigger every 5 seconds the API function and catch data based on pagination. Check your below codes and implement step by step to your project. Happy Coding.
var timer: Timer?
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "loadNewDataAutomatically" with the interval of 5 seconds
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.loadNewDataAutomatically), userInfo: nil, repeats: true)
}
#objc func loadNewDataAutomatically(_ pageNumber: Int, _ pageSize: Int, onSuccess: ((Bool) -> Void)?, onError: ((Error) -> Void)?){
// Call your api here
// Send true in onSuccess in case new data exists, sending false will disable pagination
// If page number is first, reset the list
if pageNumber == 1 { self.list = [YourDataModel]() }
// else append the data to list
self.list.append(apiResponseList)
// If Api responds with error
onError?(apiError)
// Else end success with flag true if more data available
let moreDataAvailable = !apiResponseList.isEmpty
onSuccess?(moreDataAvailable)
}
Assuming that the Database (MySQL) is on a server
You can try using WorkManager for this requirement.
When the user changes their settings, save them locally (which you are already doing)
enqueue a Unique Periodic Work Request using WorkManager & set up the time at what interval should the response be sent to the server.
Minimum time interval is around 15 min. but not guaranteed at exactly 15 minutes,
the system fires it when it seems fit, also according to the Constraints that you set on your Work Request.
You can also use a OneTimeWorkRequest, if you don't need periodic, with an Initial Delay of whatever time you need.
Edit: This question was later edited and ios, swift tags were added where previously it was for android.
If anyone comes here searching for something similar for Android, this would work.

How to test Webview is loaded or not without staticTexts in XCTest in swift

first time I try to write unit test case. Here am blocking with the following scenario.
When I tap a button, it navigates to a next screen and webview will load. I need to wait for webview loading.
I tried to wait till the staticTexts is visible by below code,
let rewards = self.app.staticTexts[“Rewards”]
let exists = NSPredicate(format: “exists == 1”)
expectationForPredicate(exists, evaluatedWithObject: rewards, handler: nil)
waitForExpectationsWithTimeout(10, handler: nil)
XCTAssert(rewards.exists)
XCTAssert(app.staticTexts[“Rewards Overview”].exists)
But In my scenario, when I tap the button it will navigate to next screen and webview will starts to load. But the webview content is always dynamic one. So I need to wait till the webviewDidFinishLoad.
You can use waitForExistence(timeout:) to return a boolean if an element enters existence within the given timeout - I'd recommend using this for your use case.
https://developer.apple.com/documentation/xctest/xcuielement/2879412-waitforexistence
I would recommend writing your own function for waiting, since with waitForExistence(timeout:), you can only wait for existence, but it ignores other states of element(.isHittable, .isEnabled etc) or simply wait for any other boolean.
Plus waitForExistence(timeout:) is painfully slow, if you have a lot of tests with a lot of waits (we stopped using it after we wrote our own waiting). It is caused by the system method waiting for additional 1-2s before returning true, if the object exists.
This might help you, however its not Swift, its ObjC: https://pspdfkit.com/blog/2016/running-ui-tests-with-ludicrous-speed/

Swift iOS -DispatchWorkItem is still running even though it's getting Cancelled and Set to Nil

I use GCD's DispatchWorkItem to keep track of my data that's being sent to firebase.
The first thing I do is declare 2 class properties of type DispatchWorkItem and then when I'm ready to send the data to firebase I initialize them with values.
The first property is named errorTask. When initialized it cancels the firebaseTask and sets it to nil then prints "errorTask fired". It has a DispatchAsync Timer that will call it in 0.0000000001 seconds if the errorTask isn't cancelled before then.
The second property is named firebaseTask. When initialized it contains a function that sends the data to firebase. If the firebase callback is successful then errorTask is cancelled and set to nil and then a print statement "firebase callback was reached" prints. I also check to see if the firebaseTask was cancelled.
The problem is the code inside the errorTask always runs before the firebaseTask callback is reached. The errorTask code cancels the firebaseTask and sets it to nil but for some reason the firebaseTask still runs. I can't figure out why?
The print statements support the fact that the errorTask runs first because
"errorTask fired" always gets printed before "firebase callback was reached".
How come the firebaseTask isn't getting cancelled and set to nil even though the errorTask makes those things happen?
Inside my actual app what happens is if a user is sending some data to Firebase an activity indicator appears. Once the firebase callback is reached then the activity indicator is dismissed and an alert is shown to the user saying it was successful. However if the activity indicator doesn't have a timer on it and the callback is never reached then it will spin forever. The DispatchAsyc after has a timer set for 15 secs and if the callback isn't reached an error label would show. 9 out of 10 times it always works .
send data to FB
show activity indicator
callback reached so cancel errorTask, set it to nil, and dismiss activity indicator
show success alert.
But every once in while
it would take longer then 15 secs
firebaseTask is cancelled and set to nil, and the activity indicator would get dismissed
the error label would show
the success alert would still appear
The errorTask code block dismisses the actiInd, shows the errorLabel, and cancels the firebaseTask and sets it to nil. Once the firebaseTask is cancelled and set to nil I assumed everything inside of it would stop also because the callback was never reached. This may be the cause of my confusion. It seems as if even though the firebaseTask is cancelled and set to nil, someRef?.updateChildValues(... is somehow still running and I need to cancel that also.
My code:
var errorTask:DispatchWorkItem?
var firebaseTask:DispatchWorkItem?
#IBAction func buttonPush(_ sender: UIButton) {
// 1. initialize the errorTask to cancel the firebaseTask and set it to nil
errorTask = DispatchWorkItem{ [weak self] in
self?.firebaseTask?.cancel()
self?.firebaseTask = nil
print("errorTask fired")
// present alert that there is a problem
}
// 2. if the errorTask isn't cancelled in 0.0000000001 seconds then run the code inside of it
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0000000001, execute: self.errorTask!)
// 3. initialize the firebaseTask with the function to send the data to firebase
firebaseTask = DispatchWorkItem{ [weak self] in
// 4. Check to see the if firebaseTask was cancelled and if it wasn't then run the code
if self?.firebaseTask?.isCancelled != true{
self?.sendDataToFirebase()
}
// I also tried it WITHOUT using "if firebaseTask?.isCancelled... but the same thing happens
}
// 5. immediately perform the firebaseTask
firebaseTask?.perform()
}
func sendDataToFirebase(){
let someRef = Database.database().reference().child("someRef")
someRef?.updateChildValues(myDict(), withCompletionBlock: {
(error, ref) in
// 6. if the callback to firebase is successful then cancel the errorTask and set it to nil
self.errorTask?.cancel()
self.errorTask? = nil
print("firebase callback was reached")
})
}
This cancel routine is not doing what I suspect you think it is. When you cancel a DispatchWorkItem, it performs no preemptive cancellation. It certainly has no bearing on the updateChildValues call. All it does is perform a thread-safe setting of the isCancelled property, which if you were manually iterating through a loop, you could periodically check and exit prematurely if you see that the task was canceled.
As a result, the checking of isCancelled at the start of the task isn't terribly useful pattern, because if the task has not yet been created, there is nothing to cancel. Or if the task has been created and added to a queue, and canceled before the queue had a chance to start, it will obviously just be canceled but never started, you'll never get to your isCancelled test. And if the task has started, it's likely gotten past the isCancelled test before cancel was called.
Bottom line, attempts to time the cancel request so that they are received precisely after the task has started but before it has gotten to the isCancelled test is going to be an exercise in futility. You have a race that will be almost impossible to time perfectly. Besides, even if you did happen to time this perfectly, this merely demonstrates how ineffective this whole process is (only 1 in a million cancel requests will do what you intended).
Generally, if you had asynchronous task that you wanted to cancel, you'd wrap it in an asynchronous custom Operation subclass, and implement a cancel method that stops the underlying task. Operation queues simply offer more graceful patterns for canceling asynchronous tasks than dispatch queues do. But all of this presumes that the underlying asynchronous task offers a mechanism for canceling it and I don't know if Firebase even offers a meaningful mechanism to do that. I certainly haven't seen it contemplated in any of their examples. So all of this may be moot.
I'd suggest you step away from the specific code pattern in your question and describe what you are trying to accomplish. Let's not dwell on your particular attempted solution to your broader problem, but rather let's understand what the broader goal is, and then we can talk about how to tackle that.
As an aside, there are other technical issues in your example.
Specifically, I'm assuming you're running this on the main queue. So task.perform() runs it on the current queue immediately. But your DispatchQueue.main.asyncAfter(...) can only be run when whatever is running on the main queue is done. So, even though you specified a delay of 0.0000000001 seconds, it actually won't run until the main queue is available (namely, after your perform is done running on the main queue and you're well past the isCancelled test).
If you want to test this race between running the task and canceling the task, you need to perform the cancel on a different thread. For example, you could try:
weak var task: DispatchWorkItem?
let item = DispatchWorkItem {
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.00001) {
task?.cancel()
}
task = item
DispatchQueue.main.async {
item.perform()
}
Now you can play with various delays and see the different behavior between a delay of 0.1 seconds and one of 0.0000000001 seconds. And you'll want to make sure the app has reached quiescence before you try this test (e.g. do it on a button press event, not in viewDidLoad).
But again, this will merely illustrate the futility of the whole exercise. You're going to have a really hard time catching the task between the time it started and before it checked the isCancelled property. If you really want to manifest the cancel logic in some repeatable manner, we're going to have to artificially make this happen:
weak var task: DispatchWorkItem?
let queue = DispatchQueue(label: "com.domain.app.queue") // create a queue for our test, as we never want to block the main thread
let semaphore = DispatchSemaphore(value: 0)
let item = DispatchWorkItem {
// You'd never do this in a real app, but let's introduce a delay
// long enough to catch the `cancel` between the time the task started.
//
// You could sleep for some interval, or we can introduce a semphore
// to have it not proceed until we send a signal.
print("starting")
semaphore.wait() // wait for a signal before proceeding
// now let's test if it is cancelled or not
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
task?.cancel()
semaphore.signal()
}
task = item
queue.async {
item.perform()
}
Now, you'd never do this, but it just illustrates that isCancelled does work.
Frankly, you'd never use isCancelled like this. You would generally use the isCancelled process if doing some long process where you can periodically check the isCancelled status and exit if it is true. But that's not the case in your situation.
The conclusion of all of this is that checking isCancelled at the start of a task is unlikely to ever achieve what you had hoped for.

How to implement a search queue

I am new in swift3.0 I am implementing a custom search box. I wish to know how can i make a search queue such that on text change in searchbox i need to perform search operation with new text and if there is an existing search operation going on cancel that. I also want to include threshold ontextchanged. So that search operation does not get fired very frequently
Your question is somehow general, but let me tell you how I accomplished this in Swift 3 and AFNetworking (this assumes you wish to search for the data on the server).
I hold a reference of the networking manager in the properties of the view controller:
//The network requests manager. Stored here because this view controller extensively uses AFNetworking to perform live search updates when the input box changes.
var manager = AFHTTPRequestOperationManager()
Afterwards, using UISearchController I check to see if there is any text entered in the search box at all and, if it is, I want to make sure there aren't any other ongoing AFNetworking tasks from now by closing any of them which are still running:
//Called when the something is typed in the search bar.
func updateSearchResults (for searchController: UISearchController) {
if !SCString.isStringValid(searchController.searchBar.text) {
searchController.searchResultsController?.view.isHidden = false
tableView.reloadData()
return
}
data.searchText = searchController.searchBar.text!
/**
Highly important racing issue solution. We cancel any current request going on because we don't want to have the list updated after some time, when we already started another request for a new text. Example:
- Request 1 started at 12:00:01
- We clear the containers because Request 2 has to start
- Request 2 started at 12:00:02
- Request 1 finished at 12:00:04. We update the containers because data arrived
- Request 2 finished at 12:00:05. We update the containers because data arrived
- Now we have data from both 1 and 2, something really not desired.
*/
manager.session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) in
dataTasks.forEach { $0.cancel() }
}
/**
Reloads the list view because we have to remove the last search results.
*/
reloadListView()
}
In the end, I also check in the failure closure if the code of the error is not NSURLErrorCancelled. Because, if that happened, I don't display any error message or toast.
//The operation might be cancelled by us on purpose. In this case, we don't want to interfere with the ongoing logic flow.
if (operation?.error as! NSError).code == NSURLErrorCancelled {
return
}
self.retrieveResultListFailureNetwork()
Hope it helps!

How to sign out user automatically after a certain period of time?

At the Time of Signing In to my Application, I am getting Time for which the current user login is valid["liveTime"] from the BackEnd API for a particular user. And I am saving this liveTime(parameter) to the NSUserDefaults.
My requirement is to show the Sign In page again when liveTime will become 0. And If that particular user kills the app and if liveTime is greater than 0, It will show the Default page after Sign In happens.
Please share your experience how to do or what is the best practices to resolve this kind of problems. Please share the code snippets if someone has already done it.
Maybe this can help you.
func afterDelay(seconds: Double, closure: () -> ()) {
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_main_queue(), closure)
}
Call the function
self.afterDelay(4) {
// Write what you want to happen after 4 sec. You can increase the number
self.dismissViewControllerAnimated(true, completion: nil)
}
The solution is to keep track in NSUserDefaults of your last login timestamp and the last maximum live time retrieved from the server (unless it must be fetched at that moment [thing that would be inappropriate because of the lag that communicating with a server brings]).
Then, in your initial controller's viewDidLoad or in your AppDelegate (the choice is yours)check if that time is after the maximum allowed and proceed accordingly.
UPDATE:
Best Practices:
1.- Add some kind of waiting page, to leave the user there waiting until you retrieve that data from the server.
2.- After receiving that Data, check for the timestamp saved in your NSUserDefaults and proceed either to login or welcome pages
3.- If the user logs in, then update the timestamp in NSUserDefaults

Resources