appletv how to react on focus change event - swiftui

i tried to change the textcolor of a button on AppleTV on focus change and this is my code (see below).
Unfortunately the code with if focus .. is never called. What am i doing wrong?
Thank you for any help!
struct ContentView: View {
#State var textColor : Color = .white
var body: some View {
VStack {
Button(action: {
self.textColor = .black
}) {
Text("tap me")
}
.focusable(true) { (focus) in
if focus {
self.textColor = .blue
} else {
self.textColor = .green
}
}
Button(action: {
self.textColor = .black
}) {
Text("another tap me")
}
.focusable(true) { (focus) in
if focus {
self.textColor = .blue
} else {
self.textColor = .green
}
}
}
}
}

The .focusable adds capability for elements non-focusable by nature, like Text (or Image), but Button is focusable as-is, so nothing happens.
The following modifications of your example works (tested with Xcode 11.2):
var body: some View {
VStack {
Text("Focusable").foregroundColor(textColor)
.focusable(true) { (focus) in
if focus {
self.textColor = .blue
} else {
self.textColor = .green
}
}
Button(action: {
self.textColor = .black
}) {
Text("Button")
}
}
}

Related

`.transition(.move(edge: .bottom))` for element inside `ZStack` not working with `withAnimation` on button

I'm trying to build a simple animated overlay. Ideally, the dark background fades in (which it's doing now) and the white card slides up from the bottom edge (using .transition(.move(edge: .bottom).
Here's my ContentView.swift file:
struct Overlays: View {
#State var showOverlay = false
var body: some View {
NavigationView {
Button {
withAnimation(.spring()) {
showOverlay.toggle()
}
} label: {
Text("Open overlay")
}
.navigationTitle("Overlay demo")
}
.overlay {
if showOverlay {
CustomOverlay(
overlayPresented: $showOverlay,
overlayContent: "This is a real basic overlay, and it should be sliding in from the bottom."
)
}
}
}
}
And here's my CustomOverlay.swift file:
struct CustomOverlay: View {
#Binding var overlayPresented: Bool
let overlayContent: String
var body: some View {
ZStack(alignment: .bottom) {
overlayBackground
overlayCard
}
}
}
extension CustomOverlay {
var overlayBackground: some View {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
.onTapGesture {
withAnimation(.spring()) {
overlayPresented = false
}
}
}
var overlayCard: some View {
VStack(spacing: 16) {
overlayText
overlayCloseButton
}
.padding()
.frame(maxWidth: .infinity)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.padding()
.transition(.move(edge: .bottom))
}
var overlayText: some View {
Text(overlayContent)
}
var overlayCloseButton: some View {
Button {
withAnimation(.spring()) {
overlayPresented = false
}
} label: {
Text("Close")
}
}
}
This doesn't appear to work. The entire overlay is fading in/out.
https://imgur.com/a/iRzJCsw
If I move the .transition(.move(edge: .bottom) to the CustomOverlay ZStack the entire overlay slides in from the bottom which looks super goofy.
What am I doing wrong?
After some more experimentation, I've found something pretty cool.
Our main ContentView.swift file:
struct Overlays: View {
#State var showOverlay = false
var body: some View {
NavigationView {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
showOverlay.toggle()
}
} label: {
Text("Open overlay")
}
.navigationTitle("Overlay demo")
}
.overlay {
if showOverlay {
// Here's the overlay background, which we can animate independently
OverlayBackground(
overlayPresented: $showOverlay
)
.transition(.opacity)
// Explicit z-index as per https://stackoverflow.com/a/58512696/1912818
.zIndex(0)
// Here's the overlay content card, which we can animate independently too!
OverlayContent(
overlayPresented: $showOverlay,
overlayContent: "This is a real basic overlay, and it should be sliding in from the bottom."
)
.transition(.move(edge: .bottom).combined(with: .opacity))
// Explicit z-index as per https://stackoverflow.com/a/58512696/1912818
.zIndex(1)
}
}
}
}
And here's OverlayBackground.swift (the background):
struct OverlayBackground: View {
#Binding var overlayPresented: Bool
var body: some View {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.25)) {
overlayPresented = false
}
}
}
}
And lastly OverlayContent.swift:
struct OverlayContent: View {
#Binding var overlayPresented: Bool
let overlayContent: String
var body: some View {
VStack {
Spacer()
overlayCard
}
}
}
extension OverlayContent {
var overlayCard: some View {
VStack(spacing: 16) {
overlayText
overlayCloseButton
}
.padding()
.frame(maxWidth: .infinity)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.padding()
}
var overlayText: some View {
Text(overlayContent)
}
var overlayCloseButton: some View {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
overlayPresented = false
}
} label: {
Text("Close")
}
}
}
The result: https://imgur.com/a/1JoMWcs

SwiftUI: fullScreenCover with no animation?

I have this view:
struct TheFullCover: View {
#State var showModal = false
var body: some View {
Button(action: {
showModal.toggle()
}) {
Text("Show Modal")
.padding()
.foregroundColor(.blue)
}
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth:1)
)
.fullScreenCover(isPresented: $showModal, onDismiss: {
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
})
}
}
Every time I press the Button, the modal screen comes up fullscreen. All works great.
Question:
How do I disable the slide up animation? I want the view to be presented immediately fullscreen without animating to it.
Is there a way to do that?
A possible solution is to disable views animation completely (and then, if needed, enable again in .onAppear of presenting content), like
Button(action: {
UIView.setAnimationsEnabled(false) // << here !!
showModal.toggle()
}) {
and then
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
.onAppear {
UIView.setAnimationsEnabled(true) // << here !!
}
})
Tested with Xcode 13 / iOS 15
AFAIK the proper to do it as of today is using transaction https://developer.apple.com/documentation/swiftui/transaction
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
showModal.toggle()
}
I also created a handy extension for this:
extension View {
func withoutAnimation(action: #escaping () -> Void) {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
action()
}
}
}
which can be used like this:
withoutAnimation {
// do your thing
}
.fullScreenCover(isPresented: isPresented) {
content()
.background(TransparentBackground())
}
.transaction({ transaction in
transaction.disablesAnimations = true
})
this should work, based on #asamoylenko's answer
At the moment, I find it easier to use UIKit for presentation in SwiftUI.
someView
.onChange(of: isPresented) { _ in
if isPresented {
let vc = UIHostingController(rootView: MyView())
vc.modalPresentationStyle = .overFullScreen
UIApplication.shared.rootVC?.present(vc, animated: false)
} else {
UIApplication.shared.rootVC?.dismiss(animated: false)
}
}
Why not use an overlay instead?
.overlay {
if isLoading {
ZStack {
ProgressView()
}
.background(BackgroundCleanerView())
}
}

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

SwiftUI no hide animation

I have noticed that when I'm coloring the background I will not get animations when removing Views.
If I remove Color(.orange).edgesIgnoringSafeArea(.all) then hide animation will work, otherwise Modal will disappear abruptly. Any solutions?
struct ContentView: View {
#State var show = false
func toggle() {
withAnimation {
show = true
}
}
var body: some View {
ZStack {
Color(.orange).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Modal")
}
if show {
Modal(show: $show)
}
}
}
}
struct Modal: View {
#Binding var show: Bool
func toggle() {
withAnimation {
show = false
}
}
var body: some View {
ZStack {
Color(.systemGray4).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Close")
}
}
}
}
You need to make animatable container holding removed view (and this makes possible to keep animation in one place). Here is possible solution.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var show = false
func toggle() {
show = true // animation not requried
}
var body: some View {
ZStack {
Color(.orange).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Modal")
}
VStack { // << major changes
if show {
Modal(show: $show)
}
}.animation(.default) // << !!
}
}
}
struct Modal: View {
#Binding var show: Bool
func toggle() {
show = false // animation not requried
}
var body: some View {
ZStack {
Color(.systemGray4).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Close")
}
}
}
}

Color Change Animation

I'm trying to animate a color change on some text but I can't seem to get it to change gradually. I've tried both an implicit and explicit animation as seen in the code below, but no dice....
struct Example: View {
#State var showing = false
var body: some View {
VStack {
Text("test text").foregroundColor(showing ? .red : .blue)
.animation(.easeIn(duration: 2))
Button(action: toggle) {
Text("Toggle")
}
}
}
func toggle() {
withAnimation(.easeIn(duration: 2)) {self.showing.toggle()}
}
}
Can anyone give me some pointers?
Unfortunately, you can't animate .foregroundColor. But you can animate .colorMultiply. So in your case this will work:
struct ColorChangeAnimation: View {
#State private var multiplyColor: Color = .blue
var body: some View {
VStack {
Text("test text")
.foregroundColor(.white)
.colorMultiply(multiplyColor)
Button(action: toggle) {
Text("Toggle")
}
}
}
func toggle() {
withAnimation(.easeIn(duration: 2)) {
self.multiplyColor = (self.multiplyColor == .red) ? .blue : .red
}
}
}