Modal from home screen finished displayed when app launched from background - ios

Background
Out app has a feature to launch background using Significant-Change Location Service(https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/using_the_significant-change_location_service).
This an iOS feature that when the app detect significant change in location, the app launched on background. It launches directly to background state so the user won't notice that app has launched(unless writing code to notify to users. ie. sending a local push or etc.) however app is fully launched so didFinishLaunchingWithOptions method on App Delegate is called upon launch.
Situation
When our app is launched by Significant-Change Location, we send local push notification to the user notifying that how much distance the user has walked recently(we have implemented pedometer to our app). At this point, our app is launched on background.
A user may tap the local notification just received and then our app's lifecycle state will be changed to foreground and app's home screen will be displayed to the user(from user point of view app is LAUNCHED at this point).
On home view controller, we observe UIApplication.didBecomeActiveNotification and when detecting this notification, we make an API request. API response may have an award field(nullable) and when the award is not null, we show the award received modal.
// Observe app state changes to active
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(callRewardAPI), // calls api when detecting notification
name: UIApplication.didBecomeActiveNotification, // <- didBecomeActiveNotification
object: nil
)
func callRewardAPI {
usecase.callRewardAPI)()
}
// Show modal when received API response
extension: HomeViewController, HomeUseCaseOutput {
func didReceiveReward() {
let vc = RewardReceivedViewController()
vc.modalTransitionStyle = .crossDissolve
vc.modalPresentationStyle = .overFullScreen
self.present(vc, animated: true, completion: nil)
}
}
Problem
Expected
We expect to see the modal animation of RewardReceivedViewController.
Actual
When the app has finished launching(from user point of view, from application point of view it has been launched on background and moving to foreground), the modal had already been displayed.
We are quite sure that modal had finished displayed upon launch since view displayed on the zoom animation of our app launch(the iOS animation by apple that app's view zooms in to fill the device screen on app launch) shows the modal being displayed.
Something we are confused is that this does not happen all the time but rathe happens several times and then stopped happening(behaving as is should be) for several times. We checked this on same git commit.
Something we tried
We tried to simulate the problem by triggering modal display right after receiving UIApplication.didBecomeActiveNotification.
func callRewardAPI {
// Call didReceiveReward right away to show modal.
self.ouptut.didReceiveReward()
/*
self.rewardAPI.request { [weak self] result in
switch result {
case .success(let reward):
self.ouptut.didReceiveReward()
case .failure:
// error case
}
}
*/
}
Since it seems we cannot use Xcode debug breakpoint when simulating app launch from not running to background status, we set local push notification to each point of code to observer what is happening.
What we found out is that code to show modal is called after viewDidAppear. viewDidAppear is the last lifecycle method of the view controller when launching a view controller so we're stuck finding out the solution.

Related

Detect when app is closed, terminated but ignore when the phone is turned off

I want to know when someone closes my app (taps the home button) or terminates my app (double tap swipe up) however I do not want to know when the user is using my app and simply turns their phone off because when they turn their phone back on it will still be on my app.
I have tried using the applicationWillResignActive, applicationDidEnterBackground, applicationWillTerminate and registering it in my view controller
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
These either didn't tell me when my app wasn't the focus or if they did work they also told me when my app was still open just the phone was closed.
You cannot draw the distinction you are asking to draw. Whether the user clicks the Home button or turns off the screen, your app is deactivated and backgrounded and you are told so — and that is all you are told.
However, you do not need to draw this distinction. When your app is backgrounded, just do whatever is appropriate. You’ll be told when your app is foregrounded again, even if that is just because the user turned the screen back on.
So after some research the only possible way I found was:
func DidUserPressLockButton() -> Bool
{
let oldBrightness = UIScreen.main.brightness
UIScreen.main.brightness = oldBrightness + (oldBrightness <= 0.01 ? (0.01) : (-0.01))
return oldBrightness != UIScreen.main.brightness
}
func applicationDidEnterBackground(_ application: UIApplication)
{
if (DidUserPressLockButton())
{
//User pressed lock button
}
else
{
//user pressed home button
}}
This method works by seeing if the app can change the brightness however some say this will get rejected by apple because they don't want you to do which makes sense why there isn't a built in function for it however there are apps on the App Store that know how you left the app so there you go

pushViewController doesn't work when app is in background?

I recently refactored an app to use a UINavigationController and pushViewController(...) to transition back and forth between the main UIViewControllers of my app. Previously I was using present(...) with no issues. The app is a music player and has the appropriate background mode.
The transition to view controller B from view controller A happens after a 5 second countdown and thus can happen in the background if the user hits their home button or turns the screen off.
The issue is that the push, and all relevant events that are supposed to kick off when the view did appear, do not fire while the app is in the background, and immediately fire as soon as I turn the display back on or bring the app into the foreground.
Is there any way to force the push to happen even if the app isn't visible, or a different method to push and display the view controller?
Thank for any insight!
let nav = self.parent as! UINavigationController
print("Attempting to .pushViewController...")
nav.pushViewController(sessionVC, animated: false)
print("After .pushViewController...")

How to display lock sreen in task switcher and resume correctly

for the app I'm developing, I implemented a lock screen that allows the user to unlock the App by manual-pin or touch/Face-ID.
Everything is working OK during the normal use.
However, I need to show the lock screen when the app is resumed from background and even in the task switcher to avoid "peeking" at the content without having unlocked properly.
As recommended by Apple in this (old) article, I present the lock view controller in applicationDidEnterBackground:
func applicationDidEnterBackground(_ application: UIApplication) {
let lockVC = LoginViewController()
lockVC.loginType = LoginViewController.LoginType.resumeApp
if let topViewController = UIApplication.topViewController() {
topViewController.present(lockVC, animated: false, completion: nil)
}
}
where topViewControler is a useful extension to determnine the topmost view controller: Get top most UIViewController
.
The lockVC.loginType = ... is just to let the ViewController what type of login I need and customize its view a little bit
The results I get are a bit weird, both on simulator and real devices:
the task switcher preview for my app is completely black
when the app is resumed, the screen remains black as in preview and unresponsive. Only way to exit is to kill the app.
before obtaining the weird results above, I had to access all outlets as optional to avoid termination... that's fine for viewDidLoad stuff (I didn't expect the need for this when entering background, since the view had been loaded before - outlet wired) but the strange thing is that I had the same error for an IBAction in viewDidAppear (IBAction for touch-id button called automatically if touch/face-id is available - just a requirement).
I believe I'm missing something big here... but no other hints found.
No one of the ready-made lock screen solutions come with an example for the background/taskSwicth/resume case).
Note that the black/unresponsive screen seems to be same both if I use the mentioned extension for the topmost view controller to present or if I simply try to present it by
self.window?.rootViewController?.present(lockVC, animated: false)
(which I believe is wrong, but tried anyway)
Any help would be apreciated
I found a temporary workaround:
Display the lock screen when the app becomes active from background, as suggested by Dev_Tandel
Hide information in the task switcher by adding a blur effect on the current screen where the app is sent to background (applicationWillResignActive) and removing it when the app comes active again (applicationDidBecomeActive), as suggested here
As said, it's a temporary workaround that i wanted to share, but I don't like this 100%. It is required to show the lock screen in the task swtcher and Ive seen apps doing it (e.g. "oneSafe").
Still looking for help.
Solved, thanks to this post.
Apparently I was naive in trying to present a view controller with just its object instance.
If I use
let lockVC = topViewController.storyboard?.instantiateViewController(withIdentifier: "IDLoginViewController") as! LoginViewController
everything works, the lock screen is shown also in the task switcher, no need to blur!

ios simulator viewDidLoad, viewDidAppear not called after close or quit

My app has one view controller and one view. When I run my app on the simulator from xcode, the app loads and both viewDidLoad, viewDidAppear are called as expected.
When I go to the simulator home screen and then come back to the app,
I expect viewDidLoad to be called, but it is not.
When I QUIT the
app by following these directions, and restart the app fresh, I
expect both methods to be called, but neither are called.
If these events don't trigger those calls, then what will trigger those calls?
It's hard to believe that on a real device, viewDidLoad is only called once - the first time the app is loaded.
It says "load". It literally means that it is called when the view controller is loaded(instantiated). Also the "appear" is called when the view is appeared like when you push it or dismiss other view controller above it.
The thing you want is register the following notifications
static let UIApplicationWillEnterForeground: NSNotification.Name
static let UIApplicationDidEnterBackground: NSNotification.Name
static let UIApplicationDidBecomeActive: NSNotification.Name
In your view controller's viewDidLoad() add notification.
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.applicationDidBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
Then the following method will be called when the app becomes active again form the background.
func applicationDidBecomeActive() {
// Update your view controller
}
Case 2:
If you quit the app like the instruction, the debug session is terminated so the break point and log is not working. If you quit your app and want to check the break points or logs you need to hit run again in your Xcode.

Swift navigate to View from notification

I am making an iOS application. Where I have made that I get notification when near certain places. And that works fine. But I then want it to navigate to a certain view when I press the notification.
I am thinking just like when I get a sms. I press that notification for the sms and it then navigate me to the correct sms conversation.
When application receives a push notification, a method in UIApplicationDelegate is called. The notification needs to be handled differently depending on what state your app is in when it’s received:
If app wasn’t running and the user launches it by tapping the push
notification, the push notification is passed to your app in the
launchOptions of
application(_:didFinishLaunchingWithOptions:)
If app was running and in the foreground, the push notification
will not be shown this function will call immediately
application(_:didReceiveRemoteNotification:)
If app was running or suspended in the background and the user brings
it to the foreground by tapping the push notification, this function
will be called.
application(_:didReceiveRemoteNotification:)
So you can change the root view controller on these functions like this :-
if let notification = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey] as? [String: AnyObject] {
//Change your root view controller
}
This checks whether the value for UIApplicationLaunchOptionsRemoteNotificationKey exists in launchOptions. If it does, this will be the push notification payload you sent.
I wrote a simple class to navigate to any view controller in the view hierarchy from anywhere in one line of code by just passing the class type, so the code you'll write will be also decoupled from the view hierarchy itself, for instance:
Navigator.find(MyViewController.self)?.doSomethingSync()
Navigator.navigate(to: MyViewController.self)?.doSomethingSync()
..or you can execute methods asynchronously on the main thread also:
Navigator.navigate(to: MyViewController.self) { (MyViewControllerContainer, MyViewControllerInstance) in
MyViewControllerInstance?.doSomethingAsync()
}
Here the GitHub project link: https://github.com/oblq/Navigator

Resources