Determine background status with SwiftUI - swiftui

When using SwiftUI with iOS 13+, the traditional means of determining background state no longer work. For example:
AppDelegate methods applicationDidEnterBackground(_ application: UIApplication) and applicationDidBecomeActive(_ application: UIApplication) do not get called.
Notifications didEnterBackgroundNotification, willEnterForegroundNotification, didBecomeActiveNotification and willResignActiveNotification do not get sent.
As an alternative, there are UIWindowSceneDelegate callbacks: sceneDidBecomeActive(_ scene: UIScene), sceneWillResignActive(_ scene: UIScene), sceneWillEnterForeground(_ scene: UIScene), sceneDidEnterBackground(_ scene: UIScene)
The problem with these replacements is that they are specific to one of multiple scenes that are entering and leaving the foreground. They do not provide a simple and clean way to determine if the entire app is in the foreground or background.
Determining app foreground/background status is important for reasons that have nothing to do with the user interface. Some iOS features fail silently when the app is not in the foreground (wildcard bluetooth scanning and iBeacon transmission are two examples.) I often develop iOS frameworks that have no user interface whatsoever, so I need a way to determine app background/foreground state that does not rely on pasting a bunch of boilerplate code in the UIWindowSceneDelegate -- it is not reasonable for me to ask somebody using my framework to do that.
Are there any straightforward ways to determine the apps's foreground/background status on iOS 13 with SwiftUI?

You can use the UIApplication notifications in SwiftUI as well:
didEnterBackgroundNotification
willEnterForegroundNotification
didBecomeActiveNotification
willResignActiveNotification
Here is an example:
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in
// active
}
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { _ in
// inactive
}

I believe the correct way to handle events in SwiftUI is to use the onReceive method.
Text("Hello, World!")
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
// Action
}

Related

SwiftUI Is there a way to override the default back button in the navigation view toolbar without needing to add a modifier to every secondary view?

I know that on the view you can add the following modifiers to modify the navigation bar to create a custom back button.
SomeView{ ... }
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
mode.wrappedValue.dismiss()
} label: {
Image(systemName: "chevron.backward")
}
}
However, I do not want to go through my entire app to every secondary view and add these modifiers. I've considered creating a reusable header component, but at the moment, I'm just wondering if there was a way to override the system default for the back button to impact the entire app.
Unfortunately In SwiftUI there is not. You could however, override UINavigationController but it is not recommended as APIs can change.
Look at List for example, we used to set the UITableView appearance background color to .clear to customize List's background, but in iOS 16 this solution works no more.
Extended View and create a func where you put your code in it, then use that function wherever you need!

How not to let Home button quit your tvOS app in SwiftUI

In UIKit we can use GCEventViewController to intercept the game controllers from propagating Home button presses to the responders (and have them quit our app) by setting controllerUserInteractionEnabled = false
SwiftUI Apps do not use ViewControllers, so, except for resurrecting one to embed the whole app in it, we can't use the above solution to avoid the player leaving out game / app by accident by pressing the wrong button (not can we use buttonB)
Does anyone know a solution to this conundrum? Did Apple already implement a new way to intercept high-level events so we can deal with them internally?
Thanks!
You have to have a focusable view (like a button) and then you can use onExitCommand in the containing view to simply ignore the exit event, or do something different.
struct ContentView: View {
var body: some View {
VStack {
Button("Hello, world!") {
}
}.onExitCommand {
// Do nothing
}
}
}

Can't catch bluetooth headset button click event in swiftui 2.0

I am trying to simply execute code on a click of a bluetooth headset button in a SwiftUI 2.0 app, but after trying many different codes, nothing have worked... Does someone have solved this issue?
Based on apple docs and some answer I found on StackOverflow (https://stackoverflow.com/a/58249502/13207818), I tried this simple code
import SwiftUI
import MediaPlayer
struct ContentView: View {
init() {
MPRemoteCommandCenter.shared().pauseCommand.isEnabled = true
MPRemoteCommandCenter.shared().pauseCommand.addTarget(handler: { (event) in
print("Pause")
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().playCommand.isEnabled = true
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { (event) in
print("Play")
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget (handler: { (event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus in
// middle button (toggle/pause) is clicked
print("event:", event.command)
return .success
})
}
var body: some View {
Text("Hello World")
}
}
Of course Enabling Background Audio as per Apple doc
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Even tried to activate my app audio session:
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, mode: .default, options: [.duckOthers, .allowBluetooth, .allowBluetoothA2DP])
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
print("audioSession is Active")
} catch {
print("audioSession properties weren't set because of an error.")
print(error)
}
But everything failed...
Would someone know what I am doing wrong or would have faced such issue with swiftUI 2.0?
Thank in advance for your support
In general you shouldn’t do actions in the initializers of views. Since they represent the state of the UI, not the actual UI they could be broken down and created again whenever SwiftUI thinks it needs to.
Im not at my pc but You can probably get a Publisher for the pause button which you can bind to a view with onReceive
Finally, I got a solution for my issue.
I don't know how it works really behind but the audio focus wasn't on my app. So I've just played a silent sound for a second and I could play properly with my play/pause button. I know that it's not a proper solution, but it works!
This reminds me of a similar bug on the galaxy s8...
If I find a better one, I'll keep you posted.

SwiftUI SecureField: How to achieve the same character obscuring behaviour as in UIKit?

My problem is SecureField in SwiftUI doesn’t display characters input by the user for any time at all, it just directly shows the '•' symbol for each character as it's typed - whereas in UIKit, UITextField (with isSecureTextEntry = true) shows the latest character for a second before hiding it behind '•'.
UX testers at my company have requested I bring back the "old behaviour" - but this behaviour doesn't seem part of any public API.
Interestingly this goes for UITextField custom classes injected into SwiftUI using UIViewRepresentable too - they behave in the "SwiftUI way" described above. So there's some contextual behaviour modification going on in SwiftUI for all secure UITextField behaviour? I'd have to completely rewrite my SwiftUI form into a full UIViewController to get back the behaviour (modally pushed UIViewControllers with secure UITextFields do exhibit the desired behaviour.)
Is this a sort of sideline bug in SwiftUI? I see the same thing for SwiftUI in both iOS13 and 14. Anyone seen a workaround or solution?
-EDIT-
After #Asperi's great explanation below, I noticed that my UITextField custom classes injected into SwiftUI using UIViewRepresentable were forcing this behaviour by unnecessarily setting the text binding in the updateUIView call. Using a Coordinator only to deal with text logic fixed the problem for me when using this method.
The observed effect is due to immediate apply to bound string state and immediate react/rebuild of view.
To bring desired behavior beck we need to postpone somehow state update and thus give a chance for SecuredField/UITextField to update self without synchronisation with state.
Here is a demo of possible direction (it is not ideal, but a way to go). Tested with Xcode 12.1 / iOS 14.1.
struct DemoSecureFieldView: View {
#State private var password = "demo"
var textBinding: Binding<String> {
Binding(get: { password },
set: { value in
// more logic can be added to delay _only_ if new symbol added,
// and force apply if next symbol came fast
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
password = value
}
}
)
}
var body: some View {
VStack {
SecureField("Placeholder", text: textBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}.background(Color.pink)
}
}

How to use custom view transitions in SwiftUI when navigating?

In UIKit-based application we can have custom navigation transitions using UIViewControllerAnimatedTransitioning protocol.
Is there an equivalent in SwiftUI?
I know we can already animate between removing and adding views.
But how do we do this when we push and pop to the navigation stack?
There isn't anything like this available in SwiftUI's APIs so far afaik. Make sure to open an enhancement request with Apple.
Implementing "navigation" on your own is a terrible idea, as you basically give all the accessibility and facility support afforded by UINavigationController.
Instead, here is a suggestion:
Either by means of a custom SwiftUI view or modifier, wrap the NavigationView with a UIViewControllerRepresentable wrapper, which in turn sets up a UIHostingController subclass, which waits for addChild(_:) to be called, which will be the navigation controller added as a child of the hosting controller. From here, if your animations are "static" (e.g. do not require any subview to subview transition), you can accomplish this here by implementing the navigation controller delegate, and providing the animation controller.
If you do need more evolved transitions (such as Photos.app's photo transition), you can create a custom UIViewRepresentable wrapper, which will serve as markers for "from" views and "to" views, you can then use those discover in UIKit, and e.g. can snapshot and animate in transitions.
Proposed API can look like this:
struct ContentView1: View {
var body: some View {
NavigationLink(destination: DetailView()) {
Image("small").customSourceTarget()
}.navigationBarTitle("Navigation")
}
}
struct ContentView2: View {
var body: some View {
Image("large").customTransitionTarget()
}
}
NavigationView {
ContentView1()
}.customAnimatable()