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.
Related
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.
How do I create my program that will only allow users to post every 30 minutes using swift? This delay time only works if the app is running, besides that it will not work or restart the time running every time the app launches. I need a way to have them wait for only 30 minutes. My code right now is:
DispatchQueue.global(qos: .background) .async {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1800)) {
UserDefaults.standard.set(false, forKey: "PostTimeLimit")
}
}
Any tips or solutions would be very helpful. Thanks!
Instead of storing whether or not the user is allowed to post, store their last post time and compare it to the current time.
In the current implementation, my payment takes a long time in some cases. Often users have an error like "Apple pay not completed". The question says that in iOS 11 this happens after 15-20 seconds, can I increase this time, if so, how ? If the payment has time to process during this time, the payment in apple pay is successful.
Unfortunately this is not possible from what i know and what i found ,
the onpaymentauthorized method has to be called within 30 seconds , if not the payment is declined . Refer to this , in most cases you only have as much as 30 seconds .
https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778020-onpaymentauthorized
While changing the failure timeout is impossible, there is still a workaround to avoid the "Apple pay not completed" message for cases when, for example, your API call processing the payment has extended timeout. (At the moment of writing this answer, on iOS 13, the ApplePay dialog would timeout itself in around 30 secs).
The trick is to set a one-time timer, which would dismiss the ApplePay dialog just before it decides to give up. Of course your app must always give user proper feedback when the purchase process ends (was it success or failure), 'cause ApplePay dialog won't be able to show anything after you dismiss it.
Example timer:
_ = Timer.scheduledTimer(withTimeInterval: 25, repeats: false) { _ in
guard self.applePayBeingProcessed == true else { return }
if let applePayVC = AppUtil.shared.topMostController() as? PKPaymentAuthorizationViewController {
self.applePayHasTimeouted = true
applePayVC.dismiss(animated: true)
}
}
*applePayBeingProcessed is set to true in paymentAuthorizationViewController(_:didAuthorizePayment:handler:) and set back to false right after calling handler(PKPaymentAuthorizationResult(status: paymentStatus, errors: [error])) - so that the routine called by timer would be skipped when ApplePay dismissed in a normal way via paymentAuthorizationViewControllerDidFinish(_:)
** applePayHasTimeouted is later checked inside a completion of your payment processing API call, if true it means we need to perform actions, that are normally supposed to be performed inside paymentAuthorizationViewControllerDidFinish(_:) (because the latter will never be called after manually closing ApplePay dialog)
*** topMostController() method finds the controller from the top of hierarchy. How to do this is out of scope of current question, there lots of ways to do this, my favorite one is in this answer.
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
my firebase data structure looks like the following
user
|__{user_id}
|__userMatch
|__{userMatchId}
|__createdAt: <UNIX time in milliseconds>
I'm trying to listen for the child added event under userMatch since a particular given time. Here's my swift code:
func listenForNewUserMatches(since: NSDate) -> UInt? {
NSLog("listenForNewUserMatches since: \(since)")
var handle:UInt?
let userMatchRef = usersRef.childByAppendingPath("\(user.objectId!)/userMatch")
var query = userMatchRef.queryOrderedByChild("createdAt");
query = query.queryStartingAtValue(since.timeIntervalSince1970 * 1000)
handle = query.observeEventType(FEventType.ChildAdded, withBlock: { snapshot in
let userMatchId = snapshot.key
NSLog("New firebase UserMatch created \(userMatchId)")
}, withCancelBlock: { error in
NSLog("Error listening for new userMatches: \(error)")
})
return handle
}
What's happening is that the event call back is called only once. Subsequent data insertion under userMatch didn't trigger the call. Sort of behaves like observeSingleEventOfType
I have the following data inserted into firebase under user/{some-id}/userMatch:
QGgmQnDLUB
createdAt: 1448934387867
bMfJH1bzNs
createdAt: 1448934354943
Here are the logs:
2015-11-30 17:32:38.632 listenForNewUserMatches since:2015-12-01 01:32:37 +0000
2015-11-30 17:45:55.163 New firebase UserMatch created bMfJH1bzNs
The call back was fired for bMfJH1bzNs but not for QGgmQnDLUB which was added at a later time. It's very consistent: after opening the app, it only fires for the first event. Not sure what I'm doing wrong here.
Update: Actually the behavior is not very consistent. Sometimes the call back is not fired at all, not even once. But since I persist the since time I should use when calling listenForNewUserMatches function. If I kill the app and restart the app, the callback will get fired (listenForNewUserMatches is called upon app start), for the childAdded event before I killed the app. This happens very consistently (callback always called upon kill-restart the app for events that happened prior to killing the app).
Update 2: Don't know why, but if I add queryLimitedToLast to the query, it works all the time now. I mean, by changing userMatchRef.queryOrderedByChild("createdAt") to userMatchRef.queryOrderedByChild("createdAt").queryLimitedToLast(10), it's working now. 10 is just an arbitrary number I chose.
I think the issue comes from the nature of time based data.
You created a query that says: "Get me all the matches that happened after now." This should work when the app is running and new data comes in like bMfJH1bzNs. But older data like QGgmQnDLUB won't show up.
Then when you run again, the since.timeIntervalSince1970 has changed to a later date. Now neither of the objects before will show up in your query.
When you changed your query to use queryLimitedToLast you avoided this issue because you're no longer querying based on time. Now your query says: "Get me the last ten children at this location."
As long as there is data at that location you'll always receive data in the callback.
So you either need to ensure that since.timeIntervalSince1970 is always earlier than the data you expect to come back, or use queryLimitedToLast.