SwiftUI 5.5 Timeout After X of Inactivity - ios

Unlike tracking a user's session via them logging in, I need to find a way to timeout the user going through a multi-page registration process after x amount of time of inactivity.
The registration process uses an observable class to store the various entered values in memory as the user is going through the process. Each page of the registration is within a NavigationView, with navigation links on each page that takes the user to the next screen.
I can't just timeout the entire registration if they are actively going through it because for some it may take a few minutes, whereas for someone who has a disability, it might take a lot longer. As long as there is some sort of activity I want to ensure no timeout.
However, if the user is on page 2 for example, and then gets a phone call (sending the app into the background), forgets about it until the next day (again, just an example), and then comes back to the app to keep going, I want to show an alert that upon the user tapping OK would clear out the values within the observable class and take them back to a certain page of the app.
While there are a lot of suggestions on something like this for UIKit (not really exactly for this, but....) I haven't seen anything to do this using the latest SwiftUI for iOS 15+.
I need the timer to start when the user begins the registration process, and not be interrupted if the app goes into the background. If the user quits the app, there's nothing I can do about that, but if the app remains open, in either the foreground OR the background, I need to time them out of the registration process, after x amount of time of inactivity.

#jnpdx Approach definitely works.
I can add an approach without Timer, using DispatchQueue, which also works when the app goes into background:
// global timer var for every view
var timerWorkItem: DispatchWorkItem?
struct ContentView: View {
#State private var message = "not started"
var body: some View {
VStack {
Text(message)
.font(.title)
Button("Start") {
message = "in process"
// set timer
timerWorkItem = DispatchWorkItem {
message = "timed out!"
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timerWorkItem!)
}
.padding()
Button("Do something") {
// cancel old item
timerWorkItem?.cancel()
// set new item
timerWorkItem = DispatchWorkItem {
message = "timed out!"
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timerWorkItem!)
}
.padding()
}
.buttonStyle(.bordered)
}
}

Related

How to programmatically dismiss quasar notification

Confused with quasar documentation. Not sure what that means
From docs: Notifications are meant to be dismissed only by the user, however for exceptional cases you can do it programmatically. Especially useful when you set indefinite timeout (0).
const dismiss = $q.notify({...})
...
dismiss()
For those who got confused with documentation as much as i did
$q.notify(...) returns you another function, you can leave it as is, call it with no parameters to hide the said notification, or call with some parameters to update the said notification
$q.notify({ message: 'I am a regular notification, will open and close as usual' })
const dismiss = $q.notify({ message: 'I will hide programmatically', timeout: 0 // Optionally set timeout to 0 to fully control the dismissing })
setTimeout(() => {
dismiss() // will hide the notification above
}, Math.random() * 5000)

Rightness of waiting for a network call to complete - 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.

Chat messages are being fetched every time I open the viewcontroller

I'm currently building a chat app and when I click on a user, it takes me to the chat log controller. Here I call a function fetchChatMessages() in viewdidload() that essentially fetches the conversation from firestore. Problem is, whenever I go to the previous controller and open the chat again, it again fetches the messages.
Not sure if it's fetching from cache or from the server itself. But I did write a print statement under the firestore fetch code that prints every time.
Now I'm new to swift so my question is, in other chat apps, you can see that messages seemed to be fetched just once from the server and after that you add a listener and update the collection view to display the new messages. In my case, it seems like everything is fetched over and over again. Even though I have added a listener and followed a highly acclaimed tutorial.
Also, I added a scroll to bottom code whenever messages are fetched, so every time a new message is fetched, the controller automatically scrolls to the bottom. But this happens every time I open the chat. I was trying to fix this bug where the controller keeps scrolling every time the view appears which made me wonder, am I contacting firebase again and again when the controller is opened?
override func viewDidLoad() {
super.viewDidLoad()
fetchCurrentUser()
fetchMessages()
setupLoadView()
}
//MARK: Fetch Messages
var listener: ListenerRegistration?
fileprivate func fetchMessages(){
print("Fetching Messages")
guard let cUid = Auth.auth().currentUser?.uid else {return}
let query = Firestore.firestore().collection("matches").document(cUid).collection(connect.uid).order(by: "Timestamp")
listener = query.addSnapshotListener { (querySnapshot, error) in
if let error = error{
print("There was an error fetching messages", error.localizedDescription)
return
}
querySnapshot?.documentChanges.forEach({ (change) in
if change.type == .added{
let dictionary = change.document.data()
self.items.append(.init(dictionary: dictionary))
print("FIRESTORE HAS BEEN CONTACTED FETCHING MESSAGES")
}
})
self.collectionView.reloadData()
self.collectionView.scrollToItem(at: [0, self.items.count - 1], at: .bottom, animated: true)
print("Fetched messages")
}
}
//MARK: View Disappears
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent{
listener?.remove()
}
}
I want the controller to remember its state. Like if I scroll the messages, exit the controller and enter it again, it stays at the scrolled position like in whatsapp.
The problem here is that your controller gets deallocated every time you leave the screen, because you probably push the controller on to the stack and pop it afterwards, this will erase all of the internal state of the controller. This behavior is indeed intended (viewDidLoad is called once the screen is loaded). You can solve this problem in several ways. An easy one would be to introduce a singleton service (a service which is shared across the app) which holds the state of the controller, so every time the controller is created it will ask the service for its state. Keep in mind this is not a very good solution, but it should be sufficient as starting point. If you need an example I will edit my answer accordingly later on.
I cannot give you a definite answer on this, because it really depends on what features the app and the server support, in the end I would have some kind of database service and chat services (these should not be accessed over the singleton pattern, but rather via dependency injection). The chat service would define some kind of policy when the data should be fetched, which means the controller should not be aware of this. The chat service would then store the messages via the database layer in some persistent store like user defaults, realm or core data for each chat. Every time the user enters an already fetched chat the chat service will check if persisted data is available if not it will fetch it from the server.

How can I increase the failure timeout of an apple Pay operation?

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.

Like feedback using firebase crash. Fast click like-remove like

I have an application like instagram. It has feedback page.
When user likes some post, I add this like and feedback (with its own key (.childByAutoId) for this like.
static func add(_ newLike: LikeItem) {
// add like id for user feedback implementation
var like = newLike
let likeRef = ref.child("/userslikes/" + newLike.userId + "/onposts/" + newLike.postId).childByAutoId()
like.key = likeRef.key
var updates: [String: Any?] = [
"/userslikes/" + like.userId + "/onposts/" + like.postId: like.toAnyObject(),
"/postslikes/" + like.postId + "/" + like.userId: like.toAnyObject()
]
if like.userId != like.postAddedByUserId { // dont add your own likes
var likeForFeedBack = like.toAnyObject()
likeForFeedBack["isViewed"] = false // when user will open feedback -> true
updates.updateValue(likeForFeedBack, forKey: "/feedback/" + like.postAddedByUserId + "/" + like.key)
}
ref.updateChildValues(updates)
}
It's ok. And also I have remove function. It is going to like node, getting this like and feedbackId from this like. And then I make multi-part update.
static func remove(with userId: String, _ post: PostItem) {
var updates: [String: Any?] = [
"/userslikes/" + userId + "/onposts/" + post.key: nil,
"/postslikes/" + post.key + "/" + userId: nil
]
// deleting from feedback node
getLikeFromUser(id: userId, postId: post.key) { like in
if like.userId != like.postAddedByUserId {
updates.updateValue(nil, forKey: "/feedback/" + like.postAddedByUserId + "/" + like.key)
}
ref.updateChildValues(updates)
}
}
static func getLikeFromUser(id: String, postId: String,
completion: #escaping (_ likeId: LikeItem) -> Void) {
let refToLike = ref.child("/userslikes/" + id + "/onposts/" + postId)
refToLike.observeSingleEvent(of: .value, with: { snapshot in
let like = LikeItem(snapshot: snapshot)
completion(like)
})
}
So, when user taps "remove like" I have some delay (It is fetching like entity to get feedback id at this time).
And the problem: If I'm spamming like-removeLike button (like - remove like - l - rl - l - rl etc.), sometimes my feedback node is duplicating (with different keys ofc. It has not removed old node) and sometimes it is not adding (in this situation it is crashing if I try to remove it in the future).
How to fix it?
My humble opinion, first of all this could be fix with UX limitations. User shouldn't be able to spam any button in application. Must be a delay between this events. Even you can add some max. switch between user decisions... wait a while and make it free again (maybe).
Like you said on your comment, it's very good idea and good UX wait user until finish write operation. This way you can eliminate bad UX.
You can use userinteractionenabled property of UIView.
When set to NO, touch, press, keyboard, and focus events
intended for the view are ignored and removed from the event queue.
When set to YES, events are delivered to the view normally. The
default value of this property is YES.
During an animation, user
interactions are temporarily disabled for all views involved in the
animation, regardless of the value in this property. You can disable
this behavior by specifying the
UIViewAnimationOptionAllowUserInteraction option when configuring the
animation.
Of course there are many alternatives, sky is the limit for UX scenario.
Also you can check Apple's user interface guidelines for loading:
https://developer.apple.com/ios/human-interface-guidelines/interaction/loading/
Show content as soon as possible. Don’t make people wait for content
to load before seeing the screen they're expecting. Show the screen
immediately, and use placeholder text, graphics, or animations to
identify where content isn't available yet. Replace these placeholder
elements as the content loads. Whenever possible, preload upcoming
content in the background, such as while an animation is playing or
the user is navigating a level or menu.
and indicators maybe:
https://developer.apple.com/ios/human-interface-guidelines/ui-controls/progress-indicators/
If it’s helpful, provide useful information while waiting for a task
to complete. Include a label above an activity indicator to give extra
context. Avoid vague terms like loading or authenticating because they
don’t usually add any value.
Another option
Like you said in your comment below there is another option to keep like/dislike until user lives the ViewController. But there is another UX problem that when user try to close modal or back to previous view controller they will wait until this background job finish. Another issue if user kills the application you have 1 change left to save data and it's AppDelegate's applicationWillTerminate. But it's bad practice to save data there because 5 seconds limit:
https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623111-applicationwillterminate
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
If the method does not return before time expires, the system may kill
the process altogether. For apps that do not support background
execution or are linked against iOS 3.x or earlier, this method is
always called when the user quits the app. For apps that support
background execution, this method is generally not called when the
user quits the app because the app simply moves to the background in
that case. However, this method may be called in situations where the
app is running in the background (not suspended) and the system needs
to terminate it for some reason. After calling this method, the app
also posts a UIApplicationWillTerminate notification to give
interested objects a chance to respond to the transition.
Hope it helps.

Resources