SwiftUI - How to initialize only one SectionView in ScrollView using onTapGesture - swiftui

My problem is that when user presses on the item in my ScrollView all of the SectionView items changes. What I want to do is to change only one item. I'm trying to change item image when user taps on it and then when user taps on another item to bring back previous item image and change the current item image.
Take a look at my ScrollView:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
ForEach(1..<41) { item in
GeometryReader { geometry in
SectionView(number: item, pressed: pressed, firstElement: (item == 1) ? true : false)
.rotation3DEffect(Angle(degrees:
Double(geometry.frame(in: .global).minX - 30) / -20
), axis: (x: 0, y: 10, z: 0))
.onTapGesture {
self.pressed.toggle()
SectionView(number: item, pressed: self.pressed, firstElement: (item == 1) ? true : false)
print("touched item \(item)")
}
})
}
}
}
Here's my SectionView:
struct SectionView: View {
var number: Int
var pressed: Bool
var firstElement: Bool
var body: some View {
ZStack(alignment: .bottom) {
Image(pressed ? "t1" : "t\(number)")
.resizable()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.aspectRatio(contentMode: .fill)
Spacer()
Text(firstElement ? "" : "Pride")
.font(.system(size: 12, weight: .black))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, 15)
}
.frame(width: 60, height: 80)
//.background(section.image)
.cornerRadius(7)
//.shadow(color: section.image.opacity(0.15), radius: 8, x: 0, y: 10)
}
}

I tried putting your code into some kind of buildable state, but I couldn't get it to compile or run, so I may be completely misinterpreting what you are trying to do. However, it seems to me you are using pressed as if you are capturing the state at the time of the press on that particular SectionView. However, it would have to be a State var to compile, so it will update all of the views, which is what I believe you are seeing. I think what you need to do is change pressed to be an Int that keeps track of the SectionView that was pressed, and compare it to number which lets the particular SectionView keep track of its identity.
Also, I don't think you want the SectionView in the tap gesture. I think you just want to change the state of the pressed variable and let it work its way through the hierarchy.

Related

SWIFTUI: app buttons logic - change label in specific scenario

My app has a number of mainButton created by a single struct. The data is passed to these buttons from an array. Then I have a Submit button.
I am trying to have the Submit button to change label to a arrow up icon only when: a given instance of mainButton has been pressed once already and changed its color and size already (this works), and Submit was pressed once already. Now the Submit button's icon changes to a refresh symbol (arrow up), until a diffrent instance of mainButton has been pressed (and this new button in turn has changed the color and size already as expected).
Basically a button representing a value is selected(tapped) and then its content submitted by pressing the Submit button, and if you submit the same button again the submitter is a refresher (arrow up icon) rather than a submitter (Submit label) as you have submitted that given query before; hence if you want to submit the same value again, the button is submit button is labeled as a refresher. The code below should make this clearer.
I am new to Swift/SwiftUI and trying to do this in a simple way as I want to understand it fully. Hence, if possible, I would rather use if conditions and variables rather than advanced methods.
Here is a simplified version of my code with only place holders in it:
//
// AddView.swift
// WriteOn
//
// Created by Maurizio Zappettini on 2/11/23.
//
import SwiftUI
struct AddView: View {
#State private var typeofMessageGeneric: String = ""
#State private var lastButtonTapped: String = ""
#State private var buttonCount: Int = 0
let buttonData = [
(mainButtonText: "A", mainButtonValue: "a"),
(mainButtonText: "B", mainButtonValue: "b"),
(mainButtonText: "C", mainButtonValue: "c"),
(mainButtonText: "D", mainButtonValue: "d"),
]
var body: some View {
ZStack { // Using ZStack for background gradient
Rectangle()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading)
.foregroundColor(.clear)
.background(LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom))
VStack { //Master VStack
VStack { //Headline
Text("Test View")
.font(.headline)
.padding()
}
GeometryReader { geometry in // Debug alignment
VStack(alignment: .center) { //TextBox
Rectangle()
.fill(Color.cyan)
.cornerRadius(20)
.frame(width: geometry.size.width * 0.8, height: 250, alignment: .center)
.overlay(
HStack(alignment: .top){
VStack {
Spacer()
}
ScrollView {
VStack(alignment: .center) {
Text("getResponsePlaceHolder")
.foregroundColor(.black)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.layoutPriority(2)
}
}
VStack{
ShareLink(item: "nothing"){
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(.black)
}
}
VStack{
Spacer()
}
}
.frame(height: geometry.size.height * 0.6) // find way to make it refer to actual box and not whole screen
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} //Vstack Top
} //Geo Reader
Spacer(minLength: 50
)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
mainButton(typeofMessageGeneric: self.$typeofMessageGeneric, lastButtonTapped: self.$lastButtonTapped, buttonCount: self.$buttonCount, mainButtonText: data.mainButtonText, mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
VStack{ //Footer
submitButton()
}
}
}
}
func submitButton() -> some View {
return Button(action: getResponse) {
if self.typeofMessageGeneric == self.lastButtonTapped && self.buttonCount == 0 {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
}
// Create main buttons
struct mainButton: View {
#Binding var typeofMessageGeneric: String // Bind the variable inside this struct with the same one inside the ContentView struct
#Binding var lastButtonTapped: String // Not currently used -- implement later if needed
#Binding var buttonCount:Int
var mainButtonText:String
var mainButtonValue:String
var body: some View {
Button(action: {
self.typeofMessageGeneric = mainButtonValue
self.lastButtonTapped = mainButtonValue
buttonCount = 0
if self.typeofMessageGeneric == mainButtonValue{
buttonCount = 1
} else {
buttonCount = 0
}
}) {
Text(mainButtonText)
Text(String(buttonCount)) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(self.mainButtonValue == self.typeofMessageGeneric ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(self.mainButtonValue == self.typeofMessageGeneric ? 1.12 : 1)
.animation(.easeOut, value: 2)
}
}
func getResponse() {
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView()
}
}
EDIT:
Here is a better explanation of what the app does and what I am trying to achieve in terms of button logic.
What's on the screen when you start the app:
There is an empty text box.
There are 4 buttons (it will be more)
There is a Submit button at the bottom
What the app does:
The 4 main buttons are 4 different prompts. Each prompt/button is composed of a label and an associated value.
Tapping a button acts as a way to select that button's corresponding value.
Tapping the submit button will submit the value of the last selected main buttons to an external API. The latter will return some specific data that is visualized in the text box above (this part I left out as it is unrelated to my button label problem).
If the same prompt is submitted again the API will return different data (as there is a randomization mechanism). Hence, the user may want to submit again the same prompt they have submitted already.
How the buttons behave (practical example):
1- You tap button B
2- Button B changes the background color from purple to teal and becomes bigger.
3- You tap the "Submit" button.
4- The submit button changes its label from "Submit" to ARROW_UP.
5- The app submits value "b" to the API and this returns DATA_b_1.
6- DATA_b_1 is visualized in the text box.
7- Button B is still 'selected' (it is still teal and larger)
8- You tap ARROW_UP.
9- Value "b" is submitted again to the API and this now returns
DATA_b_2 (a different random instance of DATA_b).
10- DATA_b_2 is visualized in the text box.
11- You now tap button D.
12- Button D changes the background color from purple to teal and becomes bigger.
13- Button B reverts its color to purple and its size to smaller.
14- ARROW_UP button reverts its label to "Submit".
15- Back to point 3
Everything in the list above works as expected except the "Submit" to ARROW_UP and vice-versa label changes.
I hope I understood what you are trying to achieve, the. question is not very clear to me. What I got is:
Show the question and 4 possible answers to the user. Let the user select an answer. Submit button: disabled.
The user selects one answer; they can change their mind as much as they want until the answer is submitted. Submit button: enabled.
The user pressed "Submit": they can't press any other button, except for "Refresh". Submit button: "arrow up".
The user pressed "Refresh": go back to 1.
If that's the case, you just need to track two things:
what is the selected answer (when none is selected, nothing can be submitted)
when the answer was submitted - then, the only option is to refresh the question
By the way, your variable buttonCount is pointless: if you have only one variable for the whole view, all buttons will show the same number.
With a good set of ifs and elses, the "Submit" button can be disabled, enabled or show "arrow up", and perform the right action. Here's the code:
// Track which button was pressed - or none of them (starting case)
#State private var buttonPressed: String = ""
// Track if a choice was submitted - when submitted, the Submit button can only refresh
#State private var isWaitingToRefresh = false
// For testing only - tests the refresh
#State private var question = "0"
// (other code)
var body: some View {
// (other code)
Text("getResponsePlaceHolder \(question)")
// (other code)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
// Call a function, no need for binding vars
mainButton(mainButtonText: data.mainButtonText,
mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
submitButton()
}
}
}
func submitButton() -> some View {
Button {
// No action if no button was pressed
guard !buttonPressed.isEmpty else { return }
// Check if you need to refresh the question
if isWaitingToRefresh {
// If the view is waiting to refresh, then refresh the question
refresh()
isWaitingToRefresh = false
} else {
// If the view is not waiting to refresh, submit the user's choice
isWaitingToRefresh = true
getResponse()
}
} label: {
if !isWaitingToRefresh {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
// Gray-out the button if the user made no choice yet
.background(buttonPressed.isEmpty ? Color.secondary : Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
// Disable the button if the user made no choice yet
.disabled(buttonPressed.isEmpty)
}
// Create main buttons
// Call a function, no need for binding vars:
// you can use the same variables of the view
func mainButton(mainButtonText: String,
mainButtonValue: String) -> some View {
Button {
// You just need to change the choice when a button is pressed,
// let the Submit button do the rest
buttonPressed = mainButtonValue
} label: {
Text(mainButtonText)
Text(String("same number for all buttons")) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(mainButtonValue == buttonPressed ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(mainButtonValue == buttonPressed ? 1.12 : 1)
.animation(.easeOut, value: 2)
// Do not let the user change the choice if it was already submitted
.disabled(isWaitingToRefresh)
}
// Remember to reset the user's choice (buttonPressed = "")
func refresh() {
buttonPressed = ""
question = String(Int.random(in: 1...100))
}

SwiftUI - Drag Gesture doesn't activate on HStack with Buttons in it

I have an HStack with buttons in it defined like this:
GeometryReader { proxy in
HStack {
Button("test")
.frame(width: 100, height: 50)
Button("test")
.frame(width: 100, height: 50)
Button("test")
.frame(width: 100, height: 50)
}
.frame(width: proxy.size.width)
.gesture(
.onChanged { gesture in
// do something here
}
)
}
As you can see, I am trying to perform some function when the HStack is dragged. This works if the user drags starting from an empty space in the HStack, however, if the drag starts from the button, the gesture is not activated. How can I fix this?
You can use a View with an onTapGesture instead of a button.
Try something like this:
#State private var count = 0 // To check if dragging works
GeometryReader { proxy in
HStack {
Text("test")
.onTapGesture { // The action gesture
//
}
.foregroundColor(.blue) // Colour it like the default button
.frame(width: 100, height: 50)
Text("\(count)")
}
.gesture(DragGesture(minimumDistance: 1) // The dragging gesture
.onChanged { gesture in
count += 1
})
}
You could possibly also try and disabling the button when dragging and then enable when you're done dragging.
Good luck!

SwiftUI - onTapGesture on HStack works, until it stops working

We have a custom view, that we use in forms (its main feature is to be able to show a nice border on if there is a validation error on the form):
struct ButtonNavigationLink: View {
var label: String
var markingType: MarkingType = .none
var action: () -> Void
var body: some View {
GeometryReader { geometry in
HStack {
Text(label)
.offset(x: 5, y: 0)
Spacer()
Image("PfeilRechts")
}
.frame(width: geometry.size.width + 10, height: geometry.size.height, alignment: .leading)
.padding(.trailing, 5)
.border(markingType.getMarkingColor())
.offset(x: -5, y: 0)
// to allow click on the whole width
.contentShape(Rectangle())
.onTapGesture {
print("in ButtonNavigationLink")
self.action()
}
}
}
}
The onTapGesture works well, until it doesn't work anymore.
It happens if we open other sheets and then eventually come to this one.
The funny thing is, that - when is not working anymore - if you scroll, even so lightly, the form containing it, it starts working again. The form is not disabled

How to get a horizontal picker for Swift UI?

Is there an existing solution to get a horizontal picker in Swift UI?
Example for Swift: https://github.com/akkyie/AKPickerView-Swift
I ended up solving this using a horizontal scrollview, tap gestures, and state to track the selection.
#State private var index
var body: some View {
        return ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(0..<self.items.count, id: \.self) { i in
                    Text("\(i)")
                        .foregroundColor(self.index == i ? .red : .black)
                        .frame(width: 20, height: 20)
                        .gesture(TapGesture().onEnded({ self.index = i }))
                }
            }
        }
        .frame(width: self.width, alignment: .leading)
}
You can use the Rotation Effect on a Picker and on the Picker's content. Make sure to rotate the content 90 degrees opposite from the picker or else the content will be sideways. If the picker is too big, you can manually set a height of the picker by using frame(height: _). In my case, I used frame(maxHeight: _). You might need to adjust the row indicators by using clipped() after the resize to stop them from flowing out of the picker.
I'm using images as an example but it should work with most if not all basic views.
Code:
Picker(selection: $data, label: Text("Data")) {
ForEach(dataArray, id: \.self) { imageName in
Image(imageName)
.resizable()
.scaledToFit()
.rotationEffect(Angle(degrees: 90))
}
}
.labelsHidden()
.rotationEffect(Angle(degrees: -90))
.frame(maxHeight: 100)
.clipped()

SwuftUI Gesture location detect clicks out of View border

If I put several Views in a row with no spaces (or very little space between them), and attach some Gesture action, I can't correctly detect, which one is tapped. It looks like there is some padding inside gesture that I can't remove.
here's the example code:
struct ContentView: View {
#State var tap = 0
#State var lastClick = CGPoint.zero
var body: some View {
VStack{
Text("last tap: \(tap)")
Text("coordinates: (x: \(Int(lastClick.x)), y: \(Int(lastClick.y)))")
HStack(spacing: 0){
ForEach(0...4, id: \.self){ind in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.gray)
.overlay(Text("\(ind)"))
.overlay(Circle()
.frame(width: 4, height: 4)
.foregroundColor(self.tap == ind ? Color.red : Color.clear)
.position(self.lastClick)
)
.frame(width: 40, height: 50)
//.border(Color.black, width: 0.5)
.gesture(DragGesture(minimumDistance: 0)
.onEnded(){value in
self.tap = ind
self.lastClick = value.startLocation
}
)
}
}
}
}
}
and behavior:
I want Gesture to detect 0 button clicked when click location goes negative. Is there a way to do that?
I spent few hours with that problem, and just after I post this question, I found solution.
it was so simple - just to add a contentShape modifier
...
.frame(width: 40, height: 50)
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0)
...