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

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.

Related

SwiftUI 5.5 Timeout After X of Inactivity

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)
}
}

UI not updating (in Swift) during intensive function on main thread

I wondered if anyone could provide advice on how I can ‘force’ the UI to update during a particularly intensive function (on the main thread) in Swift.
To explain: I am trying to add an ‘import’ feature to my app, which would allow a user to import items from a backup file (could be anything from 1 - 1,000,000 records, say, depending on the size of their backup) which get saved to the app’s CodeData database. This function uses a ‘for in’ loop (to cycle through each record in the backup file), and with each ‘for’ in that loop, the function sends a message to a delegate (a ViewController) to update its UIProgressBar with the progress so the user can see the live progress on the screen. I would normally try to send this intensive function to a background thread, and separately update the UI on the main thread… but this isn't an option because creating those items in the CoreData context has to be done on the main thread (according to Swift’s errors/crashes when I initially tried to do it on a background thread), and I think this therefore is causing the UI to ‘freeze’ and not update live on screen.
A simplified version of the code would be:
class CoreDataManager {
var delegate: ProgressProtocol?
// (dummy) backup file array for purpose of this example, which could contain 100,000's of items
let backUp = [BackUpItem]()
// intensive function containing 'for in' loop
func processBackUpAndSaveData() {
let totalItems: Float = Float(backUp.count)
var step: Float = 0
for backUpItem in backUp {
// calculate Progress and tell delegate to update the UIProgressView
step += 1
let calculatedProgress = step / totalItems
delegate?.updateProgressBar(progress: calculatedProgress)
// Create the item in CoreData context (which must be done on main thread)
let savedItem = (context: context)
}
// loop is complete, so save the CoreData context
try! context.save()
}
}
// Meanwhile... in the delegate (ViewController) which updates the UIProgressView
class ViewController: UIViewController, ProgressProtocol {
let progressBar = UIProgressView()
// Delegate function which updates the progress bar
func updateProgressBar(progress: Float) {
// Print statement, which shows up correctly in the console during the intensive task
print("Progress being updated to \(progress)")
// Update to the progressBar is instructed, but isn't reflected on the simulator
progressBar.setProgress(progress, animated: false)
}
}
One important thing to note: the print statement in the above code runs fine / as expected, i.e. throughout the long ‘for in’ loop (which could take a minute or two), the console continuously shows all the print statements (showing the increasing progress values), so I know that the delegate ‘updateProgressBar’ function is definitely firing correctly, but the Progress Bar on the screen itself simply isn’t updating / doesn’t change… and I’m assuming it’s because the UI is frozen and hasn’t got ‘time’ (for want of a better word) to reflect the updated progress given the intensity of the main function running.
I am relatively new to coding, so apologies in advance if I ask for clarification on any responses as much of this is new to me. In case it is relevant, I am using Storyboards (as opposed to SwiftUI).
Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task.
You say "...Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task."
No. If you do time-consuming work synchronously on the main thread, you block the main thread, and UI updates will not take effect until your code returns.
You need to figure out how to run your code on a background thread. I haven't worked with CoreData in quite a while. I know it's possible to do CoreData queries on a background thread, but I no longer remember the details. That's what you're going to need to do.
As to your comment about print statements, that makes sense. The Xcode console is separate from your app's run loop, and is able to display output even if your code doesn't return. The app UI can't do that however.

How to handle first time launch experience when iCloud is required?

I am using CloudKit to store publicly available data and the new NSPersistentCloudKitContainer as part of my Core Data stack to store/sync private data.
When a user opens my app, they are in 1 of 4 states:
They are a new user with access to iCloud
They are a returning user with access to iCloud
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
States 1 and 2 represent my happy paths. If they are a new user, I'd like to seed the user's private store with some data before showing the initial view. If they are a returning user, I'd like to fetch data from Core Data to pass to the initial view.
Determining new/old user:
My plan is to use NSUbiquitousKeyValueStore. My concern with this is handling the case where they:
download the app -> are recorded as having launched the app before -> delete and reinstall/install the app on a new device
I assume NSUbiquitousKeyValueStore will take some time to receive updates so I need to wait until it has finished synchronizing before moving onto the initial view. Then there's the question of what happens if they don't have access to iCloud? How can NSUbiquitousKeyValueStore tell me if they are a returning user if it can't receive the updates?
Determining iCloud access:
Based on the research I've done, I can check if FileManager.default.ubiquityIdentityToken is nil to see if iCloud is available, but this will not tell me why. I would have to use CKContainer.default().accountStatus to learn why iCloud is not available. The issue is that is an asynchronous call and my app would have moved on before learning what their account status is.
I'm really scratching my head on this one. What is the best way to gracefully make sure all of these states are handled?
There's no "correct" answer here, but I don't see NSUbiquitiousKeyValueStore being a win in any way - like you said if they're not logged into iCloud or don't have network access it's not going to work for them anyway. I've got some sharing related stuff done using NSUbiquitiousKeyValueStore currently and wouldn't do it that way next time. I'm really hoping NSPersistentCloudKitContainer supports sharing in iOS 14 and I can just wipe out most of my CloudKit code in one fell swoop.
If your app isn't functional without cloud access then you can probably just put up a screen saying that, although in general that's not a very satisfying user experience. The way I do it is to think of the iCloud sync as truly asynchronous (which it is). So I allow the user to start using the app. Then you can make your call to accountStatus to see if it's available in the background. If it is, start a sync, if it's not, then wait until it is and then start the process.
So the user can use the app indefinitely standalone on the device, and at such time as they connect to the internet everything they've done on any other device gets merged into what they've done on this new device.
I struggled with this problem as well just recently. The solution I came up with was to query iCloud directly with CloudKit and see if it has been initialized. It's actually very simple:
public func checkRemoteData(completion: #escaping (Bool) -> ()) {
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Root'")
let query = CKQuery(recordType: .init("CD_Container"), predicate: predicate)
db.perform(query, inZoneWith: nil) { result, error in
if error == nil {
if let records = result, !records.isEmpty {
completion(true)
} else {
completion(false)
}
} else {
print(error as Any)
completion(false)
}
}
}
This code illustrates a more complex case, where you have instances of a Container entity with a derived model, in this case called Root. I had something similar, and could use the existence of a root as proof that the data had been set up.
See here for first hand documentation on how Core Data information is brought over to iCloud: https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data
to improve whistler's solution on point 3 and 4,
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
one should use UserDefaults as well, so that it covers offline users and to have better performance by skipping network connections when not needed, which is every time after the first time.
solution
func isFirstTimeUser() async -> Bool {
if UserDefaults.shared.bool(forKey: "hasSeenTutorial") { return false }
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Item'")
let query = CKQuery(recordType: "CD_Container", predicate: predicate)
do {
let items = (try await db.records(matching: query)).matchResults
return items.isEmpty
} catch {
return false
// this is for the answer's simplicity,
// but obviously you should handle errors accordingly.
}
}
func showTutorial() {
print("showing tutorial")
UserDefaults.shared.set(true, forKey: "hasSeenTutorial")
}
As it shows, after the first time user task showTutorial(), UserDefaults's bool value for key "hasSeenTutorial" is set to true, so no more calling expensive CK... after.
usage
if await isFirstTimeUser() {
showTutorial()
}

What's the right way to do an initial load of list data in Firebase and Swift?

Every firebase client example I see in Swift seems to oversimplify properly loading data from Firebase, and I've now looked through all the docs and a ton of code. I do admit that my application may be a bit of an edge case.
I have a situation where every time a view controller is loaded, I want to auto-post a message to the room "hey im here!" and additionally load what's on the server by a typical observation call.
I would think the flow would be:
1. View controller loads
2. Auto-post to room
3. Observe childAdded
Obviously the calls are asynchronous so there's no guarantee the order of things happening. I tried to simplify things by using a complete handler to wait for the autopost to come back but that loads the auto-posted message twice into my tableview.
AutoPoster.sayHi(self.host) { (error) in
let messageQuery = self.messageRef.queryLimited(toLast:25).queryOrdered(byChild: "sentAt")
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
DispatchQueue.main.async {
let m = Message(dict, key: snapshot.key)
if m.mediaType == "text" {
self.messages.append(m)
}
self.collectionView.reloadData()
}
}
})
}
Worth noting that this seems very inefficient for an initial load. I fixed that by using a trick with a timer that will basically only allow the collection view to reload maximum every .25s and will restart the timer every time new data comes in. A bit hacky but I guess the benefits of firebase justify the hack.
I've also tried to observe the value event once for an initial load and then only after that observe childAdded but I think that has issues as well since childAdded is called regardless.
While I'm tempted to post code for all of the loading methods I have tried (and happy to update the question with it), I'd rather not debug what seems to not be working and instead have someone help outline the recommended flow for a situation like this. Again, the goal is simply to auto-post to the room that I joined in the conversation, then load the initial data (my auto-post should be the most recent message), and then listen for incoming new messages.
Instead of
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
try replacing with
let childref = FIRDatabase.database().reference().child("ChildName")
childref.queryOrdered(byChild:"subChildName").observe(.value, with: { snapshot in

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!

Resources