SwiftUI onTapGesture called only once - swift

I am working on this audio player with multiple view components in it.
I added a way to hide/show the top view and the bottom view when we click anywhere in the middle view.
Before it was working fine, but recently when I tried again, it only dismiss it and doesn't trigger the onTapGesture again.
I believe the only difference with before is that the view is presented instead of pushed in a view controller.
I have tried to use a custom gesture with a TapGesture() on onEnded() but the same result.
I also tried to add a Rectangle shape like said [here][1].
struct PlayerView: View {
#ObservedObject private var playerState = PlayerState()
#Binding var isPlayerReduced: Bool
private let interfaceColor: Color = .gray//.black
private let interfaceOpacity: Double = 0.9
private let interfaceAnimationDuration: Double = 0.4
var body: some View {
ZStack(content: {
GeometryReader(content: { geometry in
VStack(content: {
if !self.playerState.isInterfaceHidden {
TopPlayerView(playerState: self.playerState,
isPlayerReduced: self.$isPlayerReduced)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
MiddlePlayerView(skipIntro: self.$playerState.skipIntro)
// Allow to spread the background zone for click purposes
.background(Color.clear)
// I want to have the middle under my TopPlayer and my BottomPlayer
.zIndex(-1)
.onTapGesture(perform: {
withAnimation(.easeInOut(duration: self.interfaceAnimationDuration)) {
self.playerState.isInterfaceHidden.toggle()
}
})
// .gesture(TapGesture()
// .onEnded({ _ in
// }))
if !self.playerState.isInterfaceHidden {
BottomPlayerView(playerState: self.playerState)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
})
})
})
.background(Color.black)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
I am kind of out of ideas here, any help is welcomed! thank you!

Alright, so after touching everything possible in this code. I ended up making it work.
The difference is where I put the padding to my views.
I switch the paddings to the VStack instead of my views in the VStack.
It seems to work now.
I post below the working code.
var body: some View {
ZStack(alignment: .center, content: {
GeometryReader(content: { geometry in
VStack(content: {
self.topMarker()
if !self.playerState.isInterfaceHidden {
TopPlayerView(playerState: self.playerState,
isPlayerReduced: self.$isPlayerReduced)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
MiddlePlayerView(skipIntro: self.$playerState.skipIntro)
// Allow to spread the background zone for click purposes
.background(Color.white.opacity(0.00000001))
// I want to have the middle under my TopPlayer and my BottomPlayer
.zIndex(-1)
.onTapGesture(perform: {
withAnimation(.easeInOut(duration: self.interfaceAnimationDuration)) {
self.playerState.isInterfaceHidden.toggle()
}
})
if !self.playerState.isInterfaceHidden {
BottomPlayerView(playerState: self.playerState)
.transition(.opacity)
.background(self.interfaceColor.opacity(self.interfaceOpacity))
}
})
.padding(.top, 8)
.padding(.bottom, geometry.safeAreaInsets.bottom)
})
})
.background(Color.black)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("")
.navigationBarHidden(true)
}
To be honest, I have no idea what would be the difference here...
Even in the view debugger there is no difference..

Related

Can you control the progress of a SwiftUI animation

Is it possible to control the progress of withAnimation
Ideal API would look something like this
withAnimation(progress: percentDragged) { showSecondView.toggle() }
Here's a full code example of the above line in context.
The goal is to smoothly slide between different app states with a user controlled gesture.
struct ContentView: View {
#State var showSecondView = false
var body: some View {
VStack {
ZStack {
if !showSecondView {
Text("First view")
.padding()
.background(Color.blue)
.transition(.slide)
} else {
Text("Second view")
.padding()
.background(Color.red)
.transition(.slide)
}
}
}
.gesture(
DragGesture()
.onChanged { value in
let percentDragged = value.translation.width / UIScreen.main.bounds.width
// Here I want to gradually slide (interpolate) between the two states
// Ideal API would look like this:
withAnimation(progress: percentDragged) { showSecondView.toggle() }
}
)
}
}
Yes.. I know I can trigger an animation whenever I want (like this). but that's not what I'm looking for! I want to control the progress
if percentDragged > 0.5 {
withAnimation { showSecondView.toggle() }
}
Yes.. I also know I could manually store a #State var offset: Double and control my own slide.
This just gets nasty when my app state is tied to tons of different visual changes. Views appearing/disappearing, alignments shifting, etc.

NavigationView frame glitch with safearea

I try to create a custom ViewModifier similar to SwiftUI's .sheet modifier.
When I try to make a NavigationView spring from bottom, the frame of the view just glitched over safearea. The frame looks as if adjust to the safearea when the view moves from bottom to top.
Anyone knows maybe how to constrain the view frame inside the navigation view to avoid this?
Here is what happened. When click the plus button, the SwiftUI .sheet modifier shows up. Custom popup shows up when pressing the gear button.
Problem gif recording here
Here is code of the custom popup view.
struct SettingsView: View {
#Binding var showingSelf: Bool
#Binding var retryWrongCards: Bool
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
Section {
Toggle(isOn: $retryWrongCards) {
Text("Retry Wrong Cards")
}
}
}
.animation(nil)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationBarTitle("Settings")
.navigationBarItems(trailing: Button("Done") {
self.showingSelf = false
})
}
}
}
}
Here's the code of custom modifier
struct Popup<T: View>: ViewModifier {
let popup: T
let isPresented: Bool
init(isPresented: Bool, #ViewBuilder content: () -> T) {
self.isPresented = isPresented
popup = content()
}
func body(content: Content) -> some View {
content
.overlay(popupContent())
}
#ViewBuilder private func popupContent() -> some View {
GeometryReader { geometry in
if isPresented {
popup
.animation(.spring())
.transition(.offset(x: 0, y: geometry.belowScreenEdge))
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
private extension GeometryProxy {
var belowScreenEdge: CGFloat {
UIScreen.main.bounds.height - frame(in: .global).minY
}
}
After debugging and trying many changes, I was already preparing a mini-project to file a SwiftUI bug for Apple. In the end, it was not necessary and we have a simple solution 🥳!!
TLDR; your SettingsView does not correctly initialise the NavigationView.
The modifier .navigationViewStyle(StackNavigationViewStyle()) has to be applied onto the NavigationView and not inside it (in contrast to navigationBarTitle or navigationBarItems which only work when put inside the NavigationView):
NavigationView {
List {
Section {
Toggle(isOn: $retryWrongCards) {
Text("Retry Wrong Cards")
}
}
}
.animation(nil)
.listStyle(GroupedListStyle())
.navigationBarTitle("Settings")
.navigationBarItems(trailing: Button("Done") {
self.showingSelf = false
})
}
.navigationViewStyle(StackNavigationViewStyle())
After this change your custom sheet modifier behaves for me identically as the regular .sheet modifier! Great work!
Philipp
It seems your animation does exactly what a sheet does. I would just replace it with that. You can easily present a sheet on top of a sheet.
Edit
After you posted your ViewModifier code, it seems the only difference is the animation type and the size of the popup.

Disable Scrolling in SwiftUI List/Form

Lately, I have been working on creating a complex view that allows me to use a Picker below a Form. In every case, the Form will only have two options, thus not enough data to scroll downwards for more data. Being able to scroll this form but not Picker below makes the view feel bad. I can't place the picker inside of the form or else SwiftUI changes the styling on the Picker. And I can't find anywhere whether it is possible to disable scrolling on a List/Form without using:
.disable(condition)
Is there any way to disable scrolling on a List or Form without using the above statement?
Here is my code for reference
VStack{
Form {
Section{
Toggle(isOn: $uNotifs.notificationsEnabled) {
Text("Notifications")
}
}
if(uNotifs.notificationsEnabled){
Section {
Toggle(isOn: $uNotifs.smartNotifications) {
Text("Enable Smart Notifications")
}
}.animation(.easeInOut)
}
} // End Form
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
if(!uNotifs.smartNotifications){
GeometryReader{geometry in
HStack{
Picker("",selection: self.$hours){
ForEach(0..<24){
Text("\($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:geometry.size.width / CGFloat(5))
.clipped()
Text("hours")
Picker("",selection: self.$min){
ForEach(0..<61){
Text("\($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:geometry.size.width / CGFloat(5))
.clipped()
Text("min")
}
Here it is
Using approach from my post SwiftUI: How to scroll List programmatically [solution]?, it is possible to add the following extension
extension ListScrollingProxy {
func disableScrolling(_ flag: Bool) {
scrollView?.isScrollEnabled = !flag
}
}
and the use it as in example for above demo
struct DemoDisablingScrolling: View {
private let scrollingProxy = ListScrollingProxy()
#State var scrollingDisabled = false
var body: some View {
VStack {
Button("Scrolling \(scrollingDisabled ? "Off" : "On")") {
self.scrollingDisabled.toggle()
self.scrollingProxy.disableScrolling(self.scrollingDisabled)
}
Divider()
List(0..<50, id: \.self) { i in
Text("Item \(i)")
.background(ListScrollingHelper(proxy: self.scrollingProxy))
}
}
}
}
You can use the .scrollDisabled(true) modifier on the component (Form or List) to accomplish this behavior.

SwiftUI: Animate changes in List without animating content changes

I have a simple app in SwiftUI that shows a List, and each item is a VStack with two Text elements:
var body: some View {
List(elements) { item in
NavigationLink(destination: DetailView(item: item)) {
VStack(alignment: .leading) {
Text(item.name)
Text(self.distanceString(for: item.distance))
}
}
}
.animation(.default)
}
The .animate() is in there because I want to animate changes to the list when the elements array changes. Unfortunately, SwiftUI also animates any changes to content, leading to weird behaviour. For example, the second Text in each item updates quite frequently, and an update will now shortly show the label truncated (with ... at the end) before updating to the new content.
So how can I prevent this weird behaviour when I update the list's content, but keep animations when the elements in the list change?
In case it's relevant, I'm creating a watchOS app.
The following should disable animations for row internals
VStack(alignment: .leading) {
Text(item.name)
Text(self.distanceString(for: item.distance))
}
.animation(nil)
The answer by #Asperi fixed the issue I was having also (Upvoted his answer as always).
I had an issue where I was animating the whole screen in using the below: AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))
And all the Text() and Button() sub views where also animating in weird and not so wonderful ways. I used animation(nil) to fix the issue after seeing Asperi's answer. However the issue was that my Buttons no longer animated on selection, along with other animations I wanted.
So I added a new State variable to turn on and off the animations of the VStack. They are off by default and after the view has been animated on screen I enable them after a small delay:
struct QuestionView : View {
#State private var allowAnimations : Bool = false
var body : some View {
VStack(alignment: .leading, spacing: 6.0) {
Text("Some Text")
Button(action: {}, label:Text("A Button")
}
.animation(self.allowAnimations ? .default : nil)
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.allowAnimations = true
}
}
}
}
Just adding this for anyone who has a similar issue to me and needed to build on Asperi's excellent answer.
Thanks to #Brett for the delay solution. My code needed it in several places, so I wrapped it up in a ViewModifier.
Just add .delayedAnimation() to your view.
You can pass parameters for defaults other than one second and the default animation.
import SwiftUI
struct DelayedAnimation: ViewModifier {
var delay: Double
var animation: Animation
#State private var animating = false
func delayAnimation() {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.animating = true
}
}
func body(content: Content) -> some View {
content
.animation(animating ? animation : nil)
.onAppear(perform: delayAnimation)
}
}
extension View {
func delayedAnimation(delay: Double = 1.0, animation: Animation = .default) -> some View {
self.modifier(DelayedAnimation(delay: delay, animation: animation))
}
}
In my case any of the above resulted in strange behaviours. The solution was to animate the action that triggered the change in the elements array instead of the list. For example:
#State private var sortOrderAscending = true
// Your list of elements with some sorting/filtering that depends on a state
// In this case depends on sortOrderAscending
var elements: [ElementType] {
let sortedElements = Model.elements
if (sortOrderAscending) {
return sortedElements.sorted { $0.name < $1.name }
} else {
return sortedElements.sorted { $0.name > $1.name }
}
}
var body: some View {
// Your button or whatever that triggers the sorting/filtering
// Here is where we use withAnimation
Button("Sort by name") {
withAnimation {
sortOrderAscending.toggle()
}
}
List(elements) { item in
NavigationLink(destination: DetailView(item: item)) {
VStack(alignment: .leading) {
Text(item.name)
}
}
}
}

SwiftUI conditional view will not animate/transition

I’m trying to get my views to animate/transition using .transition() on views. I use similar code from here and put .transition() to both conditional views.
struct Base: View {
#State private var isSignedIn = false
var body: some View {
Group {
if(isSignedIn){
Home().transition(.slide)
}else{
AuthSignin(isSignedIn: self.$isSignedIn).transition(.slide)
}
}
}
}
struct AuthSignin: View {
#Binding var isSignedIn: Bool
var body: some View {
VStack {
Button(action: {
self.isSignedIn = true
}) {
Text("Sign In")
.bold()
.frame(minWidth: CGFloat(0), maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(CGFloat(10))
}.padding()
}
}
}
However, whenever I click on the "Sign In" button (with or without .transition()), the app will freeze for a second and then the Home() view will suddenly appear without any animation/transition. I've also tried to wrap self.isSignedIn = true in withAnimation but it still won't work. Any ideas or is there a better way to do this?
Place your .transition on the container of the views that will switch, not each conditional view. Here's a trivial example from some code I have done (which works).
In the main View that needs to transition conditionally:
import SwiftUI
struct AppWrapperView: View {
#State var showFirstRun:Bool = true
var body: some View {
ZStack {
if (showFirstRun) {
FirstRunView(showFirstRun: $showFirstRun)
} else {
Text("Some other view")
}
}
.transition(.slide)
}
}
Then, somewhere in the view that triggers the change in condition:
import SwiftUI
struct FirstRunView: View {
#Binding var showFirstRun:Bool
var body: some View {
Button(action: {
withAnimation {
self.showFirstRun = false
}
}) {
Text("Done")
}
}
}
I had to put my if..else statement inside ZStack container instead of Group. Seems that Group was the main reason for broken animation in my case. Also, I applied .transition in combination with .animation to container instead of views.
ZStack {
if(isSignedIn){
Home()
} else {
AuthSignin(isSignedIn: self.$isSignedIn)
}
}
.transition(.slide)
.animation(.easeInOut)
Put
WithAnimation before self.isSignedIn = true