SwiftUI .onTapGesture not being called from inside a foreach loop - swift

Here's the code that I am using:
var array = ["Q","W","E","R","T","Y"]
HStack(){
ForEach(0..<topRow.count, id: \.self){i in
MyView(letter: self.array[i])
.onTapGesture {
print("Test")
}
}
}
All the "MyView"s are displaying correctly, but when I tap any of them nothing is printed. Also the MyViews are just a text().

I found the problem and solution. As New Dev pointed out, the error was being called by something not included in the code shown. I'll post what what wrong here anyways incase someone stumbles upon this.
What wasn't included was the code for MyView which I'll put here:
struct MyView: View {
var letter:String
#State var pressed = false
var body: some View {
ZStack{
if pressed{
Text(letter)
.font(Font.custom("ComicNeue-Bold", size: 30))
.foregroundColor(.white)
.opacity(0.23)
} else if !pressed{
Text(letter)
.font(Font.custom("ComicNeue-Bold", size: 30))
.foregroundColor(.white)
}
}.padding(5)
.onTapGesture {
self.pressed = true
}
}
}
I solved the issue by removing the .onTapGesture from the ZStack. So I'm guessing that the issue is that views cannot have two .onTapGesture modifiers.

Related

iOS16 Bug Keyboard breaks layout on sheet dismissal SwiftUI

In iOS16 faced a bug with keyboard inside sheet, when sheet is dismissing keyboard disappears(what is ok), but layout is not updated. I saw only 1 question on same problem and wondering maybe somebody found a temporary workaround until Apple don't fix this.
Code to reproduce :
struct Test: View {
#State var isPresented: Bool = false
#State var text: String = ""
var body: some View {
VStack{
Button {
isPresented.toggle()
} label: {
Text("PRESENT")
}
}
.sheet(isPresented: $isPresented) {
ZStack {
Color.red
VStack{
TextField("Test", text: $text)
.frame(height: 50, alignment: .center)
Spacer()
Rectangle()
.fill(Color.blue)
.frame(width:300, height: 50)
}
}
}
}
}
Video:
https://vimeo.com/758845068
The .ignoresSafeArea() fixes the issue, but...
This will have as result of keyboard overlap in your UI and not be able to scroll to see all your elements.
I use the .adaptsToKeyboard() custom modifier taken from this answer
and then using it where needed with this particular order.
VStack {...}
.adaptsToKeyboard()
.ignoresSafeArea()

When pressing button and toggling a Bool var all of my Bool vars toggle?

I have a form with several Boolean variables that change images on toggle, but when I click any of the buttons, all my variables change images? I have pulled all of them into separate subviews with the same result. Doesn't make any sense to me. Any help would be appreciated. Probably something simple I'm overlooking, but I'll be damned if I can see it.
Here is the code.
#State var total: String = ""
#State var date: Date = Date()
#State var bathroom: Bool = false
#State var steps: Bool = false
#State var furniture: Bool = false
#State var travel: Bool = false
#State var woodFloor: Bool = false
#State var concreteFloor: Bool = false
#State var takeUp: Bool = false
var body: some View {
VStack {
Form {
Section(header: Text("Job Info")
.font(.title2)
.foregroundColor(.blue)
.fontWeight(.bold)
.padding(.bottom, 10)
) {
VStack(alignment: .leading) {
DatePicker("Date", selection: $date)
.font(.title3)
.foregroundColor(.black)
.padding(.bottom, 10)
HStack(alignment: .center) {
Text("Total: $")
.font(.title3)
.foregroundColor(.black)
.padding(.trailing, 0)
TextField("", text: $total)
.font(.title3)
.frame(width: 100, height: 20, alignment: .leading)
.background(Color.yellow)
.foregroundColor(.black)
.cornerRadius(5)
Spacer()
}
.padding(.bottom, 20)
}
Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) {
Spacer()
HStack {
Text("Bathroom")
Spacer()
Button {
self.bathroom.toggle()
} label: {
Image(systemName: bathroom ? "checkmark.square" : "square")
}
}
HStack {
Text("Steps")
Spacer()
Button {
self.steps.toggle()
} label: {
Image(systemName: steps ? "checkmark.square" : "square")
}
}
HStack {
Text("Furniture")
Spacer()
Button {
self.furniture.toggle()
} label: {
Image(systemName: furniture ? "checkmark.square" : "square")
}
}
Spacer()
}
}
}
}
You put all buttons in one row (VStack with HStacks creates one view, so one row), and Form (being a List) sends all actions whenever any button is clicked in a row (it is designed to have one active element in a row).
So the solution would be either to remove VStack
Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) { // << this !!
Spacer()
and let every HStack with button live in own row...
... or instead of buttons use Image with tap gesture, like
HStack {
Text("Steps")
Spacer()
Image(systemName: steps ? "checkmark.square" : "square")
.padding()
.onTapGesture {
self.steps.toggle()
}
}
If you have a button inside a Section of Form the whole section will act as a button. So when you tap on it, all 3 button actions get executed.
instead, You can use Image with .onTapGesture{} modifier. In that way, you'll get what you want.
Sample code,
Image(systemName: bathroom ? "checkmark.square" : "square")
.foregroundColor(.blue)
.onTapGesture {
self.bathroom.toggle()
}
I think you are expecting the wrong thing from Form. Forms are not to make flexible lists with fancy stuff. If you want to be able to do anything you want and be able to customize your list at will, you should probably use Grids and other normal components like VStack...
How to use Form and what to expect:
The easiest way to see what you can do or not do with Forms without working around the limitations, is to go to the settings on your iPhone and see how different part of the settings are made. You can achieve almost all those stuff very easily without much work. However, if you want something even a little bit different than what you see in the settings, then you probably need a workaround to implement that in your app because Forms are not flexible.
Example of what you can do with Forms, taken from the settings:
For clarification, i am not saying you can't achieve what you want using forms. I'm saying thats most likely not a good idea to try to force forms to be something that they are not supposed to be.
The Answer:
So what should you do now? You should probably replace your button with Toggles so you get something similar to what you see in the settings.
[Other people have already said how to fix your current problem, so i'll just say the better way]
Consider using something like this:
struct ContentView: View {
#State var date = Date()
#State var total = ""
#State var bathroom = false
#State var steps = false
#State var furniture = false
var body: some View {
NavigationView { // DELETE this if the view before this view
// already has a navigation view
Form {
Section(header: Text("Job Info")) {
DatePicker("Date", selection: $date)
NavigationLink.init(
destination: FormTextFieldView(name: "Total $", value: $total),
label: {
Text("Total")
Spacer()
Text("$ " + total).foregroundColor(.secondary)
})
}
Section(header: Text("Addons")) {
Toggle("Bathroom", isOn: $bathroom)
Toggle("Steps", isOn: $steps)
Toggle("Furniture", isOn: $furniture)
}
}
.navigationBarTitle("Form", displayMode: .inline)
}
}
}
struct FormTextFieldView: View {
let name: String
#Binding var value: String
var body: some View {
Form {
TextField(name, text: $value)
}
.navigationBarTitle(name)
}
}
Why to use this?
First, because it is built with No Effort. You barely need to use any modifiers. So simple!
Second, this will work well on Any apple device. When you use modifiers, specially setting a frame for the view, you'll need to consider what will happen if you use e.g. an iPad. You don't need to worry about that when you are using this approach.

SwiftUI onTapGesture called only once

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..

SwiftUI - triggered breakpoints twice

I am trying to figure out why a breakpoint triggered just once for the first time I used my app and when I did the same steps one more time the breakpoints triggered twice for the same line of code, nothing changed on runtime I just used navigation forward and back. I demo this issue with a comment on my video below. It's 5 minutes video with explanation, but you will see the issue in 2 minutes:
here is a link to a video I made
https://youtu.be/s3fqJ4YhQEc
If you don't want to watch a video I will add a code at the bottom of the question:
here is a link to a repo I used to record a video: https://github.com/matrosovDev/SwiftUINavigatioPopBackIssue
I am also use Apple Feedback Assistant, so guys help me with this as well. I assumed that there is something with deallocating view that was not deallocated in a right way but they answered with this:
There is no notion of “deallocating” structs in Swift. SwiftUI manages
view lifetimes. If you’re writing code that somehow depends on the
existance or non-existance of a particular View at a particular point
in time, that code is wrong.
What leads you to believe that the SecondView isn’t “deallocated”?
The code that leads you to that conclusion is making incorrect
assumptions about the SwiftUI view lifecycle.
Anyway you may see on the video breakpoints triggered twice and even before I actually showing SecondView.
Code example:
struct FirstView: View {
#EnvironmentObject var simpleService: SimpleService
#State var showSecondView = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
self.downloadImageFromURL()
}) {
Text("Next")
.fontWeight(.bold)
.font(.title)
.padding(EdgeInsets(top: 20, leading: 40, bottom: 20, trailing: 40))
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
}
NavigationLink(destination: SecondView(), isActive: $showSecondView) { EmptyView() }
}
}
}
func downloadImageFromURL() {
simpleService.image = nil
if let url = URL(string: "https://placebear.com/200/300") {
simpleService.downloadImage(from: url) { (image) in
self.showSecondView = true
}
}
}
}
struct CircleImage: View {
var image: Image
var body: some View {
image
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.aspectRatio(contentMode: .fill)
}
}
struct SecondView: View {
#EnvironmentObject var simpleService: SimpleService
var body: some View {
NavigationView {
CircleImage(image: simpleService.image!) // Called twice next time I visit SecondView
.frame(height: 140)
.frame(width: 140)
}.onAppear(perform: fetch)
}
private func fetch() {
print(Date())
}
}

SwiftUI List disable cell press

I am using xCode 11 beta 7 with SwiftUI.
I have a simple list which each list element has several buttons. Currently when the user presses the cell(not the buttons) it is highlighting the back of the list cell(probably not the correct terminology for SwiftUI).
How do i disable this behaviour? I could not locate an obvious api to disable it.
List {
HStack {
Group {
Button(action: {}) {
Text("Read").padding(5)
}.onTapGesture {
print("1")
}
.padding()
.background(Color.blue)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Notify").padding(5)
}.onTapGesture {
print("2")
}
.padding()
.background(Color.purple)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Write").padding(5)
}.onTapGesture {
print("3")
}
.padding()
.background(Color.yellow)
.cornerRadius(5)
}
}
}
Same answer as in How to remove highlight on tap of List with SwiftUI?
I know I'm a bit late, but it's for those of you who are searching (like me 😇)
What I found
I guess you should take a look at the short article How to disable the overlay color for images inside Button and NavigationLink from #TwoStraws
Just add the .buttonStyle(PlainButtonStyle()) modifier to your item in the List and you'll have what you wanted. It also makes the Buttons work again in the List, which is another problem I encountered.
A working example for Swift 5.1 :
import Combine
import SwiftUI
struct YourItem: Identifiable {
let id = UUID()
let text: String
}
class YourDataSource: ObservableObject {
let willChange = PassthroughSubject<Void, Never>()
var items = [YourItem]()
init() {
items = [
YourItem(text: "Some text"),
YourItem(text: "Some other text")
]
}
}
struct YourItemView: View {
var item: YourItem
var body: some View {
VStack(alignment: .leading) {
Text(item.text)
HStack {
Button(action: {
print("Like")
}) {
Image(systemName: "heart.fill")
}
Button(action: {
print("Star")
}) {
Image(systemName: "star.fill")
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
struct YourListView: View {
#ObservedObject var dataSource = YourDataSource()
var body: some View {
List(dataSource.items) { item in
YourItemView(item: item)
}
.navigationBarTitle("List example", displayMode: .inline)
.edgesIgnoringSafeArea(.bottom)
}
}
#if DEBUG
struct YourListView_Previews: PreviewProvider {
static var previews: some View {
YourListView()
}
}
#endif
As said in the article, it also works with NavigationLinks. I hope it helped some of you 🤞🏻
This is my simplest solution that is working for me (lo and behold, I'm building my first app in Swift and SwiftUI as well as this being my first post on SO):
Wherever you have buttons, add .buttonStyle(BorderlessButtonStyle())
Button(action:{}){
Text("Hello")
}.buttonStyle(BorderlessButtonStyle())
Then on your list, add .onTapGesture {return}
List{
Text("Hello")
}.onTapGesture {return}
Instead of using a button try it with a gesture instead
Group {
Text("Notify").padding(5).gesture(TapGesture().onEnded() {
print("action2")
})
.padding()
.background(Color.purple)
.cornerRadius(5)
}