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
}
})
}
Related
In the Android official documentation, it says the following about StateFlow vs Livedata:
LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not.
and they recommend to cancel eacy flow collection like this:
// Coroutine listening for UI states
private var uiStateJob: Job? = null
override fun onStart() {
super.onStart()
// Start collecting when the View is visible
uiStateJob = lifecycleScope.launch {
latestNewsViewModel.uiState.collect { uiState -> ... }
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
uiStateJob?.cancel()
super.onStop()
}
As far as I know about structured concurrency, when we cancel the parent job, all children jobs are cancelled automatically and flow terminal operator collect should not be an exception. In this case, we are using lifecycleScope that should cancel when the lifecycleOwner is destroyed. Then, why do we need to manually cancel flow collection in this case? what am I missing here?
When using lifecycleScope you do not need to cancel the jobs or scope yourself, as this scope is bound to the LifecycleOwner's lifecycle and gets cancelled when the lifecycle is detroyed. The flow is then cancelled too.
It changes as soon you are using your own CoroutineScope:
val coroutineScope = CoroutineScope(Dispatchers.Main)
override fun onCreate() {
// ...
coroutineScope.launch {
latestNewsViewModel.uiState.collect { uiState -> ... }
}
}
override fun onDestroy() {
// ...
coroutineScope.coroutineContext.cancelChildren()
}
In that case you would need to take care of cancelling the scope (or job's) yourself.
According to #ChristianB response, we don't need to cancel coroutines that are attached to a lifecycle. the collect() is running inside a coroutine and will cancel too, when coroutines cancels.
Then, why do we need to manually cancel flow collection in this case?
what am I missing here?
When we want to use coroutines with a custom lifecycle (not using fragment/activity lifecycle). For example, I've created a helper class that needed to do sth inside itself. It launches a coroutine and it was managing (cancel/join...) its job.
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
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.
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.
I have a custom subclass of UIDocument that I use to store the user's content for my app. I call -[UIDocument updateChangeCount:UIDocumentChangeDone] directly to track changes to the document. Saving and loading work fine, but the document never autosaves. Why would this be happening?
It turns out that the problem was that I wasn't calling -[UIDocument updateChangeCount:] from the main thread. Despite the fact that UIDocument isn't a UI element, it is still part of UIKit and so the usual caveats about always interacting with UIKit classes from the main thread still applies.
Wrapping the code in a dispatch to the main queue fixed the issue:
dispatch_async(dispatch_get_main_queue(), ^{
[doc updateChangeCount:UIDocumentChangeDone];
});
First, an update on Jayson's answer for Swift:
DispatchQueue.main.async {
doc.updateChangeCount(.done)
}
This works fine if you are just calling it in one or two places. However, if you have multiple calls, and a possibility of them being on background threads, then it may be beneficial to sub class UIDocument and override the updateChangeCount(:) function so that you enforce a main call. Otherwise, you become responsible for making the main call every time, which opens you up to a potential miss, leading to the document getting into the saveError state.
You would then have an override in your subclass like this:
override func updateChangeCount(_ change: UIDocumentChangeKind) {
DispatchQueue.main.async {
super.updateChangeCount(change)
}
}