How not to let Home button quit your tvOS app in SwiftUI - 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
}
}
}

Related

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.

iOS 14 AssistiveTouch Dwell Control sometimes not working on SwiftUI Buttons

I have a SwiftUI App which uses regular Button views and our customers use AssistiveTouch's DwellControl feature quite often. On iOS 13 DwellControl was working fine but on iOS 14 beta 8 DwellControl fails to be able click on those buttons.
The Buttons can be tapped by finger and clicked with the normal mouse but not with the DwellControl feature.
I have tried adding accessibility traits like isButton, but nothing works.
Has anyone encountered the same problem and has a solution or any insight at all? I know this is quite niche but any help is appreciated!
EDIT:
I have tested it with a very simple example, including one a Button and it works fine. It seems to be a side effect of some sorts in my specific App.
I have several ZStacks which show and hide some views like modals and popovers. Could this be the source of failure?
What I don't get is that the Buttons can be tapped and clicked... If a view was blocking the Button then this shouldn't be possible right?
I have tried Release and Debug builds which does not make a difference.
When DwellControl is activated and the cursor is on the Button that shall be clicked, tapping doesn't work either. But when the cursor is nowhere near the Button, tapping works fine.
I have send a report via Feedback to Apple.
EDIT:
I found the cause. ScrollView somehow prevents all DwellControl clicks from happening. That happens whenever a ScrollView is somewhere present in one of the Views.
Minimal example:
struct ContentView: View {
#State var show = false
var body: some View {
ZStack {
ScrollView {
Button(action: {
print("This should be printed but isn't....")
}, label: {
Text("Button in ScrollView")
})
}
}
}
}
I know its niche but maybe someone needs this.
There are other components which are affected and cause the same behaviour:
ScrollView
TextField
Slider
Stepper
UIViewRepresentable
For me this is a big issue, since our customers use DwellControl quite often.
EDIT:
This can be replicated in the newest iOS 14.2 beta 1:
import SwiftUI
#main
struct TempSwitUIApp: App {
#State private var text: String = "I am a text"
var body: some Scene {
WindowGroup {
Button(action: {
print("\(UUID()) \(self.text)")
}, label: {
Text("Print text to the console")
})
TextField("Hello", text: self.$text)
}
}
}
I updated my issue with Apple but haven't heard from them yet.

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()

Hide statusbar in swift ui, when .statusbar(hidden: true) does not work or react

I have an app that loads a tabbar with a view as initial screen, RecipeList(). Inside of RecipeList I call another view to show a recipe full screen. In RecipeList file I have code to show or hide the status bar checking if the recipe detail fullscreen view is loaded or not. It works perfectly if I preview it in xcode, BUT when I preview the code below, which is my Home() view file, and what I want to load as initial screen due to tabbar need, THEN the code inside of RecipeList to show or hide statusbar doesnt work anymore, and status bar is always on.
If i try to hide the statusbar in the code below, it works, but then is always off, something i dont want. Only wanna hide it for the fullscreen view.
I actually used this Introspect package from Github to hide the tabbar when the child view is loaded full screen, and i made it work!
SwiftUI hide TabBar in subview
https://github.com/siteline/SwiftUI-Introspect
Actually, I wonder if anyone has used Introspect to hide the statusbar like the tabbar. I tried to use it, but I am a rockie, I only know a bit of SwiftUI, no Swift, no view controller experience, nothing.
But I have a totally functional app with only this issue, and I am super frustrated not to have the skills to know why the tabbar view is forcing a persistent status bar.
Any help, please?
var body: some View {
ZStack {
Color("background2")
.edgesIgnoringSafeArea(.all)
TabView {
RecipeList().tabItem {
Image(systemName: "book.fill")
.font(.system(size: 24, weight: .bold))
Text("Galería")
}
PostList(section: sectionData[0]).tabItem {
Image(systemName: "list.bullet")
.font(.system(size: 22, weight: .bold))
Text("Listado")
}
}
.accentColor(Color("accent"))
.introspectTabBarController { tabBarController in
// customize here the UITabBarViewController if you like
self.viewModel.tabBarController = tabBarController
}
}
}
I can suggest you using the #EnvironmentObject wrapper which basically allows you to use an object as global state. You can find fair amount of tutorials explaining how to do that and inject it in your initial view so that this object is accessible in the whole view hierarchy.
Once you have that global state set up, you can hide your status bar conditionally like this:
MyOutterWrapper {
Text("Some text")
}
.statusBar(hidden: myGlobalState.statusBarHidden)
If you are using NavigationView note that hiding the status bar works best if you set it up there (also assuming navigation view is your first view that appears).
Now all you got to do is inside your view set the variable to true when entering full screen and set it back to false when exiting. Hope that helps!
EDIT: Forgot to mention that hiding status bar as of June 26, 2020 only works if it's set on the initial view. You cannot change it later and that's the reason we set up this variable in order to go back and change the value dynamically.

What is the equivalent of WKInterfaceController.didAppear() in SwiftUI on watchOS?

I'm building an Apple Watch app, and there is code I want to run every time the app is brought to the foreground.
Previously, if I wanted to do this in a watchOS with a WKInterfaceController, I would put this code in didAppear().
In SwiftUI, there is onAppear(), but when I call that on watchOS it only seems to be called the first time the app loads up, so it behaves like WKInterfaceController.willActivate() instead. The app has just a single view.
If onAppear() is the equivalent of WKInterfaceController.willActivate(), is there a different SwiftUI function that is the equivalent of WKInterfaceController.didAppear()?
Here's my current example code:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello World").font(.footnote)
.onAppear {
print("onAppear called")
}
}
}
In the meantime, I am going to experiment with triggering what I need to do within the ExtensionDelegate, but I'm just trying to learn my way around SwiftUI on WatchOS, so knowing the answer to this would be helpful in the future.