iOS/Swift - Lifecycle methods causing network request collisions - ios

In my application, I have a home screen. Anytime this screen is loaded, it needs to make a network request (currently using Alamofire 5.2) to fetch the latest data needed to display. I am running into this issue that I believe has to do with my implementation of view lifecycles, but I am not sure how to get around it to achieve my desired effect.
Scenario 1
override func viewDidLoad() {
NotificationCenter.default.addObserver(
self,
selector: #selector(wokeUp),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
#objc func wokeUp() {
pageLoaded()
}
func pageLoaded(){
// network requests made here
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
}
Here I am registering the observer within viewDidLoad. The reason I need this is many of our users will not close the application. We've found on more than a few occasions that they will let the phone sleep while the application is open, so we need to make this request when the phone is woken up and immediately back at this screen. the didBecomeActiveNotification seems to be what takes care of that.
Scenario 2
override func viewDidAppear(_ animated: Bool) {
pageLoaded() // same network request as example above
}
We also need to call this request within viewDidAppear, as there are quite a few flows where the user is brought back to this home page from another view in the application, and it's important that the request is made here as well (what the user does in the other flows has an impact of what shows up here, so we have to make sure it's updated).
The problem is that what I am finding is these two scenario will occasionally clash - our server essentially gets the same request twice, which is not ideal and causing issues. I've noticed the majority (if not all) of the problems occur when opening the application when it's no longer in memory (viewDidLoad gets called); the case of bringing the app from the background to foreground while it's still in memory is working as expected, but I have no idea what other implementation I could take to cover all of my bases here. Any insight would be appreciated.

Why not just add a simple boolean flag to your networking logic to make sure only 1 request gets fired. e.g.
class SomeViewController {
private var isFetching = false
...
func pageLoaded() {
guard isFetching == false else {
return
}
isFetching = true
// do some networking
// ....
// inside the callback / error cases
isFetching = false
}
}
depending on how big your app is, if you have many requests and/or the same request being fired on many screens. Move all your networking to another class and have this logic inside the network service rather than the viewController

Related

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.

Pushing A New WatchKit Controller Immediately After Popping Another Always Fails

OK. It's easy enough to do this in classic iOS, but WatchKit doesn't give any blocks/closures, and there isn't a choice between with/without animation.
I have a root controller that has a list of options. Touching one of the options (on either the watch or the phone) will close any currently open controller (popToRootController), then immediately, push a new one.
More or less, like so:
self.popToRootController()
self.pushController(withName: "IKANHAZCHEEZEBURGR", context: nil)
The problem is that there isn't enough time between the calls, and there's no lambda for me to execute a semaphore or push the controller.
If I step through with the debugger, it happens, no problem.
If I just hit "run," it no work.
This is what is known as a "heisenbug".
I guess I could do a one-shot timer, but that seems to be such a hideous hack that it may actually cause a disruption of The Force.
Any better ideas? What am I missing?
I know there's a TON of answers for iOS. They don't do me a whole lot of good, here.
Well, I succumbed to The Dark Side, and did the timer hack. It works. I need to give it around 0.4 seconds per open controller.
Here's an approximation of what I did:
self.popToRootController()
let _ = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: false)
func timerCallback(_ timer: Timer) {
if let timerIndex = timer.userInfo as? Int {
if 0 <= timerIndex {
DispatchQueue.main.async {self.pushController(withName: "IKANHAZCHEEZEBURGR", context: nil)}
}
}
}
UPDATE: I do want to mention that, even though this "solves" my issue, the issue that this issue is even an issue is an issue. My design was bad, and I am redesigning the basic navigation. I'll be using a page-based approach, instead of this hierarchical design.
On general principle, if I need to hack to make it work, I'm usually better off doing it a different way.

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!

Multi-threaded progress bar concurrency issue outside the main view controller

I've found so many solutions for progress bar update within the same thread and view controller, however they seemed to be not similar cases as mine.
In my application, the main view controller calls loadIntoCoreData()(implemented in class MyLoadingService) which asynchronously loads data into core data by another thread. This function has to continuously update the loading percentage (which is written in NSUserDefaults.standardUserDefaults()) to the main thread so that it could be shown on the progress bar in main view controller. I had ever used a while loop in MainViewController to continuously fetch the current percentage value, like below:
class MainViewController {
override func viewDidLoad() {
MyLoadingService.loadIntoCoreData() { result in
NSUserDefaults.standardUserDefaults().setBool(false, forKey: "isLoading")
// do something to update the view
}
self.performSelectorInBackground("updateLoadingProgress", withObject: nil)
}
func updatingLoadingProgress() {
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setBool(true, forKey: "isLoading")
// here I use a while loop to listen to the progress value
while(prefs.boolForKey("isLoading")) {
// update progress bar on main thread
self.performSelectorOnMainThread("showLoadingProcess", withObject: nil, waitUntilDone: true)
}
prefs.setValue(Float(0), forKey: "loadingProcess")
}
func showLoadingProcess() {
let prefs = NSUserDefaults.standardUserDefaults()
if let percentage = prefs.valueForKey("loadingProcess") {
self.progressView.setProgress(percentage.floatValue, animated: true)
}
}
}
And in the class of function loadIntoCoreData:
class MyLoadingService {
let context = (UIApplication.sharedApplication()delegate as! AppDelegate).managedObjectContext!
func loadIntoCoreData(source: [MyModel]) {
var counter = 0
for s in source {
//load into core data using the class context
NSOperationQueue.mainQueue.addOperationWithBlock({
// updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults()
// and synchronize it on main queue
})
counter++
}
}
}
The above code can successfully run the progress bar, however it often encounter BAD_ACCESS or some other exceptions(like "Cannot update object that was never inserted") due to the conflicts on core data context (thought it seems that managedObjectContext isn't touched by the main thread). Therefore, instead of using a while loop listening on the main thread, I consider using NSOperationQueue.performSelectorOnMainThread to acknowledge the main thread after each entry. Therefore I put my view controller as an argument sender into loadCoreData and call performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true) but failed with error "unrecognized selector sent to class 'XXXXXXXX'". So I would like to ask if is it possible to update an UI object between threads? Or, how to modify my previous solution so that the core data context conflicts could be solved? Any solutions are appreciated.
class MyLoadingService {
func loadIntoCoreData(sender: MainViewController, source: [MyModel]) {
var counter = 0
for s in source {
//load into core data using the class context
NSOperationQueue.mainQueue.addOperationWithBlock({
// updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults()
// and synchronize it on main queue
})
NSOperationQueue.performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true)
counter++
}
}
func updateProgressBar(sender: MainViewController) {
sender.progressView.setProgress(percentage, animated: true)
}
}
class MainViewController {
override func viewDidLoad() {
MyLoadingService.loadIntoCoreData(self) { result in
// do something to update the view
}
}
}
First, you are abusing NSUserDefaults in horrible ways. The documentation describes it as this...
The NSUserDefaults class provides a programmatic interface for
interacting with the defaults system. The defaults system allows an
application to customize its behavior to match a user’s preferences.
For example, you can allow users to determine what units of
measurement your application displays or how often documents are
automatically saved. Applications record such preferences by assigning
values to a set of parameters in a user’s defaults database. The
parameters are referred to as defaults since they’re commonly used to
determine an application’s default state at startup or the way it acts
by default.
You are using it to store a global variable.
Furthermore, you are completely abusing the user's CPU in your loop where you continuously are checking the value in the user defaults, and clipping off a selector to the main thread. "Abuse of the CPU" doesn't even come close to describing what this code is doing.
You should use NSProgress for reporting progress. There is a WWDC 2015 presentation dedicated exclusively to using NSProgress.
On to your core data usage.
Unfortunately, since you intentionally redacted all of the core data code, it's impossible to say what is going wrong.
However, based on what I see, you are probably trying to use that managed object context from your app delegate (which is probably still created with the deprecated confinement policy) from a background thread, which is a cardinal sin of the highest order as far as core data is concerned.
If you want to import data as a long running operation, use a private context, and execute the operations in the background. Use NSProgress to communicate progress to anyone wanting to listen.
EDIT
Thanks for the advice on my core data context usage. I digged into all
the contexts in my code and re-organized the contexts inside, the
conflict problem does not happen anymore. As for NSProgress , it's a
pity that the WWDC presentation focus on the feature on iOS 9 (while
my app must compact on iOS 8 devices). However, even though I use
NSProgress, I should still tell the main thread how many data the core
data (on another thread) already has, right? How does the thread on
NSProgress know the loading progress on my core data thread? –
whitney13625
You can still use NSProgress for iOS8, then only real difference is that you can't explicitly add children, but the implicit way still works, and that video explains it as well.
You really should watch the whole video and forget about the iOS9 part, except to know that you must add children implicitly instead of explicitly.
Also, this pre-iOS9 blog post should clear up any questions you have about it.

Is it possible to determine when iOS widget hides?

Is there any way to catch the moment when a user hides the notification panel with a widget? I want to save some information into the database at that moment (I want it to be similar to applicationDidEnterBackground:). Any other ideas about how to save data at the last moment would also be appreciated.
Usually, your widget would be a UIViewController instance, conforming to the NCWidgetProviding protocol.
That means, that you can take advantage of UIViewController's functionality and execute your code in
- (void)viewWillDisappear:(BOOL)animated;
or
- (void)viewDidDisappear:(BOOL)animated;
I tested it and it worked.
#Andrew is correct that the normal UIViewController lifecycle methods will be called when your widget goes off screen, but your controller will also be deallocated shortly thereafter, and its process suspended. So if you need to do some I/O, you have no guarantee it will complete.
The recommended way to keep your extension's process alive is to request a task assertion using performExpiringActivityWithReason:usingBlock:.
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSProcessInfo.processInfo().performExpiringActivityWithReason("because", usingBlock: { (expired) -> Void in
if expired {
NSLog("expired")
} else {
// save state off to database
}
})
}

Resources