SwiftUI application lifecycle - ios

In my first SwiftUI app, I have Remote Notifications and Background Processes enabled.
I did add an AppDelegate class, to support notification.
The notifications set the app badge to an appropriate value.
Since this app has these background modes enabled, several lifecycle events are not working:
applicationDidBecomeActive
applicationWillResignActive
applicationDidEnterBackground
applicationWillEnterForeground
Question: where/how do I reset the badge?

Here is how you can observe didBecomeActiveNotification:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
UIApplication.shared.applicationIconBadgeNumber = 0
}
}
}
}
You can observe other notifications in the same way.
Alternatively you can use an #EnvironmentObject to track the application state:
How can I use a method without any page transition or any reboot app

Related

Launch new window on iOS app using SwiftUI Lifecycle

The following code on WindowScene does indeed open a new window on macOS when a button is pressed in ContentView that opens an URL:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
WindowGroup("Viewer") {
Text("Viewer").padding()
}
.handlesExternalEvents(matching: ["*"])
}
}
However, when the same is applied to iOS, the app does not do anything when the openURL action is called.
The result from my research is that I can use the traditional requestSceneSessionActivation to open window manually. However, this will defeat the purpose of using the SwiftUI app lifecycle.
Any suggestions on a working method, without reverting back to the UIKit lifecycle?
After a very long research session, I finally found the property that does this in this blog post.
Turns out that requestSceneSessionActivation is necessary, but there's no need to use a SceneDelegate.
When creating the new session, set the targetContentIdentifier on the NSUserActivity object.
let activity = NSUserActivity(activityType: "newWindow")
activity.userInfo = ["some key":"some value"]
activity.targetContentIdentifier = "newWindow" // IMPORTANT
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
Finally, make sure the new WindowGroup can handle the event with the identifier:
WindowGroup(id: "newWindow") {
Text("New Window!").padding()
}
.handlesExternalEvents(matching: ["newWindow"])
This solution retains the SwiftUI Lifecycle, and also works on Catalyst.
Update for iPadOS 16/macOS Ventura:
You can now use the new openWindow environment property:
#Environment(\.openWindow) private var openWindow
...
openWindow(id: "newWindow")
Make sure the id passed to openWindow is the same as the id set in the WindowGroup or Window initializer.

Ipad iOS 14: icon badge flickering

I've found this strange behavior on ipad running ipados 14 (I've in both simulator and physical device installed 14.5, with 14.5.1 in the real one).
I need to show an icon badge with a number based on work made within the app, so there is no external push notification service involved in this.
I've set up a ViewModel that keep track of the number to show in the notification badge:
import Combine
import Foundation
class CounterViewModel: ObservableObject {
#Published private(set) var notificationCount = 42
func up() {
self.notificationCount += 1
}
func down() {
self.notificationCount -= 1
}
}
To publish this number on the notification badge on the app icon I'm using the .onAppear method of the contentView
struct ContentView: View {
#EnvironmentObject var counterViewModel: CounterViewModel
#State private var counterCancellable: AnyCancellable?
var body: some View {
Text("Hello World!")
.onAppear {
UNUserNotificationCenter.current().requestAuthorization(options: .badge) { _, error in
if error == nil {
self.counterCancellable = self.counterViewModel.$notificationCount.sink { count in
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = count
}
}
}
}
}
}
}
Finally, the EnvironmentObject is set up on the app
struct IBreviaryApp: App {
#ObservedObject private var counterViewModel = CounterViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.counterViewModel)
}
}
}
so I can share on all the view involved in this count mechanism.
The problem: on the first run of the app, and only on the first one, after accepting the alert about authorising the notifications, when the app is put on background the badge within the icon is shown, but suddenly disappear. In the dock does not even show.
If I click the app (both on dock or on the "desktop") the badge appear in the animation that open the app.
If I scroll between the pages of installed applications and return to the one where my app is installed, the badge now is fixed. If I open another app, so the dock show this new app in the recent used app list, the badge appear here also (and stays this time).
Restarting the ipad or reinstalling the app fixes the problem, but itself remain for the first time you run the app.
EDIT: repo here: https://github.com/artecoop/badgebug
To actually make the problem arise, I needed to change the count number from another function.
EDIT 2:
Here's a video: https://youtu.be/tPWYRm5xFXI
As you may already see on the repo, to mimic another interaction, I've added a 5 second delay and then set 42 in the counter.

Using BGTaskScheduler API with new iOS 14 App. How to register tasks without an AppDelegate?

I'm using the BGTaskScheduler API to register background tasks in my iOS 14 app which is using the new App as #Main instead of an AppDelegate. I thought we were to use scenePhase as below to mimic the previous function of didFinishLaunching in AppDelegate but the below causes a crash: *** Assertion failure in -[BGTaskScheduler _unsafe_registerForTaskWithIdentifier:usingQueue:launchHandler:], BGTaskScheduler.m:185 2020-12-01 12:53:40.645091-0500 newFitnessApp[13487:1952133] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'All launch handlers must be registered before application finishes launching' How to implement BGTaskScheduler?
#main
struct newFitnessAppApp: App {
#Environment(\.scenePhase) var scenePhase
//Launch Count (for requesting reviews and to show tutorial)
//var launchCount = UserDefaults.standard.integer(forKey: TrackerConstants.launchCountKey)
//var testMode = true
//#Environment(\.scenePhase) private var phase
let trackerDataStore = TrackerDataStore(workoutLoader: HealthKitWorkoutLoader())
var body: some Scene {
WindowGroup {
AppRootView().environmentObject(trackerDataStore)
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("App is active")
registerBackgroundTasks()
Try to do it in init
#main
struct newFitnessAppApp: App {
init() {
registerBackgroundTasks()
}
// ... other code
}
or use AppDelegate adapter to do this in delegate callback https://stackoverflow.com/a/62538373/12299030

How to Set up a SwiftUI App for iOS 14 but still default to AppDelegate if iOS 13?

I'd like to port my existing UIKit App to use the new App API for iOS 14. (but still utilize the AppDelegate methods via UIApplicationDelegateAdaptor, however I would still like to support iOS 13. Is there a way I can set up so that if iOS 14, use the App as #main but if iOS 13 use AppDelegate as #UIApplicationMain? Even the below code won't compile as my minimum build target is iOS 13:
#available(iOS 14.0, *)
#main
struct HockeyTrackerApp: App {
// inject into SwiftUI life-cycle via adaptor !!!
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
HomeView()
}
}
}

Checking back from the background

I have a webview on Android that always checks if there is internet coming back from the background checking if the connection status has changed if it is offline the application sends the user to a "reconnect and try again" screen using the code below:
protected void onResume() {
super.onResume();
mWebView.onResume();
if (isConnected(getApplicationContext())){
} else {
Intent i = new Intent(MainActivity.this, off.class);
startActivity(i);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
}
}
So far I have made a version for ios of this webview but I could not reproduce this check when the app returns from the background, how do I reproduce this "onresume" in ios swift? (the code that checks the connection state I already have)
In AppDelegate use the following method:
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
print("Enter foreground")
}
Subscribe for UIApplication.willEnterForegroundNotification and check the connection immediately after it's fired.

Resources