ScrollView causes buggy buttons in SwiftUI - swift

I've been trying to create a form in SwiftUI but because of the limitations using "Form()" I instead chose to create a ScrollView with a ForEach loop that contains a button. When I run the project and I try to click on the buttons it clicks the incorrect one, unless I scroll the view. I am new in SwiftUI and I have not been able to figure it out.
I tried making the ScrollView in different sizes and it doesn't seem to be related
struct DropDown: View {
var datos: [String]
var categoria: String
#State var titulo: String = "Seleccione"
#State var expand = false
var body: some View {
VStack {
Text(categoria).fontWeight(.heavy).foregroundColor(.white)
HStack {
Text(titulo).fontWeight(.light).foregroundColor(.white)
Image(systemName: expand ? "chevron.up" : "chevron.down").resizable().frame(width: 10, height: 6).foregroundColor(.white)
}.onTapGesture {
self.expand.toggle()
}
if expand {
ScrollView(showsIndicators: true) {
ForEach(0..<self.datos.count){ nombre in
Button(action: {
print(self.datos[nombre])
self.titulo = self.datos[nombre]
self.expand.toggle()
diccionarioDatos[self.categoria] = self.titulo
print(diccionarioDatos)
}) {
Text(self.datos[nombre]).foregroundColor(.white)
}
}
}
.frame(maxHeight: 150)
.fixedSize()
}
}
.padding()
.background(LinearGradient(gradient: .init(colors: [.blue, .green]), startPoint: .top, endPoint: .bottom))
.cornerRadius(20)
.animation(.spring())
}
}
I clicked on "2018" under "Modelo" and "2015" got selected for some reason
This is how the dropdown menu looks like

As I tested the observed behaviour is due to order of animatable properties. In your case moving rounding corners into background itself solves the problem.
So instead of
.background(
LinearGradient(gradient: .init(colors: [.blue, .green]), startPoint: .top, endPoint: .bottom)
)
.cornerRadius(20)
Use
.background(
LinearGradient(gradient: .init(colors: [.blue, .green]), startPoint: .top, endPoint: .bottom)
.cornerRadius(20)
)

Consider using .onTapGesture instead of action for the buttons.
Button(action: {}, label: {
Text(self.datos[nombre]).foregroundColor(.white)
}).onTapGesture{
print(self.datos[nombre])
self.titulo = self.datos[nombre]
self.expand.toggle()
diccionarioDatos[self.categoria] = self.titulo
print(diccionarioDatos)
}

Related

SwiftUI: matchedGeometryEffect not transitioning smoothly

I have a list of countries. Each row has an image of a country as the background. I used to have it where clicking on a country row would navigate to a new view that displayed info on the country but I am testing out using the same view to accomplish this. I am setting the state of selectedCountry when tapping a country item and trying to use matchedGeometryEffect to make the image transition. Each country has hardcoded JSON data so the id field is a hardcoded ID coming from a JSON. I ensured the id and namespace match but I am still seeing a "flash" transition rather than a smooth one I would expect.
struct ContentView: View {
#Namespace var namespace;
#Binding var countries: [Country(id: 0, display_name: "USA", image: "image_1"), Country(id: 1, display_name: "Canada", image: "image_2")]
#State var selectedCountry: Country?;
var body: some View {
if (selectedCountry != nil) {
VStack {
Text("mainImage-\(self.selectedCountry!.id)")
Image("\(selectedCountry!.image)-bg")
.resizable()
.scaledToFit()
.edgesIgnoringSafeArea(.all)
.overlay(Rectangle().opacity(0.2))
.matchedGeometryEffect(id: "main\(self.selectedCountry!.id)", in: namespace)
.mask(
LinearGradient(gradient: Gradient(colors: [Color.black, Color.black.opacity(0)]), startPoint: .center, endPoint: .bottom)
)
Spacer()
Text("test close")
.onTapGesture {
selectedCountry = nil
}
}
}
if (selectedCountry == nil) {
GeometryReader { bounds in
ScrollView {
ForEach(Array(filteredCountries.enumerated()), id: \.1.id) { (index,country) in
LazyVStack {
ZStack(alignment: .bottom) {
Text("mainImage-\(country.id)")
Image("\(country.image)-bg")
.resizable()
.scaledToFit()
.edgesIgnoringSafeArea(.all)
.overlay(Rectangle().opacity(0.2))
.matchedGeometryEffect(id: "main\(country.id)", in: namespace)
.mask(
LinearGradient(gradient: Gradient(colors: [Color.black, Color.black.opacity(0)]), startPoint: .center, endPoint: .bottom)
)
}
}
.onTapGesture {
withAnimation() {
self.selectedCountry = country;
}
}
}
}
}
}
}
}
EDIT: Adding gif below to show the broken behavior:

Animating background gradient on view

Attempting to fool around with animating a background gradient. When animation is toggled, it would change the end radius. It is creating weird behavior whenever I incorporate the animation (gif below) where it moves the whole VStack.
I tried to put the stack in a ZStack thinking that would be a solution, but end result still the same.
Curious what exactly is causing the behavior
struct LandingPage: View {
#AppStorage("signedIn") var signedIn = false
#Environment (\.dismiss) var dismiss
#StateObject var vm = DashboardLogic()
#State private var animateGradient = false
#ViewBuilder
var body: some View {
if(signedIn){
// Text("Random Page")
}
else{
NavigationView{
VStack{
Image("bodybuilding-1") // << main image
.resizable()
.scaledToFit()
.frame(width:150, height:150)
//.renderingMode(.template)
.foregroundColor(.black)
.padding(.top, 200)
Text("Welcome to Meal Journal")
.font(.title)
.padding()
.offset(y:-25) // << adjusts title
VStack{
NavigationLink(destination:dummyPage() .navigationBarHidden(true),
label:{
Text("Get Started").fontWeight(.bold)
.frame(minWidth: 0, maxWidth: 200)
.padding(10)
.foregroundColor(.white)
//draw rectange around buttons
.background(
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [.orange, .yellow],
startPoint: .topLeading,
endPoint: .bottomTrailing
)))
})
NavigationLink(destination: DummyPage().navigationBarHidden(true), label: {
Text("Login").fontWeight(.semibold)
.frame(minWidth:0, maxWidth: 200)
.padding(10)
.foregroundColor(.black)
.overlay( RoundedRectangle(cornerRadius: 25)
.stroke(Color.gray, lineWidth: 3)
)
})
.padding()
}
Rectangle()
.frame(height: 0)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
}
//.background(Color.purple)
.background(RadialGradient(gradient: Gradient(colors: [.yellow, .green]), center: .center, startRadius: 312, endRadius: animateGradient ? 100 : 450))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
animateGradient.toggle()
}
}
}
}
}
}
}
I didn't carefully check your entire code, but one thing that may help is remembering that background is a View on its own, so you can (and often have to) apply modifiers to the background view itself. So instead of .onAppear on the parent view, add it to a background view, i.e.:
var backgroundView: some View {
RadialGradient(gradient: Gradient(colors: [.yellow, .green]), center: .center, startRadius: 312, endRadius: animateGradient ? 100 : 450)
.onAppear {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
animateGradient.toggle()
}
}
}
}
and then parent view simply uses background view:
VStack {
Image("bodybuilding-1")
...
}
.background(backgroundView)
Playground example:
struct AnimatedBackgroundView: View {
#State private var animateGradient = false
#State var scale = 1.0
var body: some View {
VStack {
Text("Hello")
Button("Do it", action: {})
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.scaleEffect(scale)
.background(backgroundView)
}
var backgroundView: some View {
RadialGradient(gradient: Gradient(colors: [.yellow, .green]), center: .center, startRadius: 312, endRadius: animateGradient ? 100 : 450)
.onAppear {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
animateGradient.toggle()
}
}
}
}
struct ContentView: View {
var body: some View {
AnimatedBackgroundView()
.frame(
width: 500,
height: 600
)
}
}
PlaygroundPage.current.setLiveView(ContentView())
But like I said: I didn't investigate your entire code, so maybe some other issues prevent it from working

Ignore safe area in safe area

I'm trying to use safeAreaInset(edge:alignment:spacing:content:), to create a floating view at the bottom. The green view should ignore the bottom safe area, but this isn't working.
Code:
struct ContentView: View {
var body: some View {
ScrollView {
LinearGradient(
colors: [.red, .blue],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
}
.safeAreaInset(edge: .bottom) {
Color.green
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
}
}
}
Current result:
How do I make the green view ignore the bottom safe area so it touches the bottom?
Once you spoke about keyboard you add another difficulty to your original question! I just made simple code with overlay method it can easily converted to ZStack method as well! You do which way you like! Also I just add some fun on green View just for show and need refactor for sure, but that is not the part of issue or question!
struct ContentView: View {
#State private var string: String = String()
var body: some View {
let myDivider = Spacer().overlay(Color.secondary.frame(width: 1, height: 40))
Color.clear
.ignoresSafeArea(.all)
.overlay(
ScrollView {
ZStack {
LinearGradient(gradient: .init(colors: [.red, .blue]), startPoint: .top, endPoint: .bottom)
.frame(height: 1000)
VStack {
Spacer()
TextField("Enter youer text here ...", text: $string)
Spacer()
}
}
}
)
.overlay(
Color.green
.overlay(HStack { Spacer(); Text("🙈").padding(20); myDivider; Text("🙉").padding(20); myDivider; Text("🙊").padding(); Spacer() }.font(Font.system(.largeTitle)))
.frame(height: 80)
, alignment: .bottom)
.ignoresSafeArea(.container)
}
}
Creating an overlay(alignment:content:) after ignoring the safe area works. A view such as Color.clear can be used as the base.
Code:
struct ContentView: View {
var body: some View {
ScrollView {
LinearGradient(
colors: [.red, .blue],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
}
.safeAreaInset(edge: .bottom) {
Color.clear
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
.overlay(
Color.green
)
}
}
}
Result:
And if you have content and want a background in the safe area:
Color.red
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
.background(
Color.green
)
Result:
Note: be careful with the 2nd version. If you use modifiers such as clipShape on the background, it can cause it to break again. Fix this by using .ignoresSafeArea(edges: .bottom) after you may clip or anything else which breaks this.

Animation transition of one gradient to another SwiftUI

I was trying to change the gradient of a background depending on the value of a state value which should be possible, and it is. However, I wanted to animate this change of gradient/background (as you often do when a property depends on some transition). So I tried setting up a ternary operator that would change the background to a different gradient, with an animation/transition. The background does change, but there is no smooth transitions, simply a harsh change. Here is a minimal working example I created to illustrate the issue:
import SwiftUI
// Based on https://nerdyak.tech/development/2019/09/30/animating-gradients-swiftui.html
struct ContentView: View {
#State var animCheck: Bool = false
var body: some View {
VStack {
Button(action: {
self.animCheck = true
}){
Text("Change gradient")
}
Button(action: {
}){
Text("random button")
.background(self.animCheck == true ? LinearGradient(gradient: Gradient(colors: [.white, .green]), startPoint: .leading, endPoint: .trailing).transition(.slide).animation(.spring()) : LinearGradient(gradient: Gradient(colors: [.black, .orange]), startPoint: .leading, endPoint: .trailing).transition(.slide).animation(.spring()) )
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Is there any way to get around this/create the intended effect of an animated background change here?
Thanks.
We need to animate container to make transition work, so here is a solution:
Text("random button")
.background(
VStack {
if self.animCheck {
LinearGradient(gradient: Gradient(colors: [.white, .green]), startPoint: .leading, endPoint: .trailing)
.transition(.slide)
} else {
LinearGradient(gradient: Gradient(colors: [.black, .orange]), startPoint: .leading, endPoint: .trailing)
.transition(.slide)
}
}.animation(.spring()))

ContextMenu on a rounded LinearGradient produces sharp edges in SwiftUI

I have the following view:
struct ContentView: View {
var body: some View {
LinearGradient(gradient: Gradient(colors: [.blue, .red]), startPoint: .topTrailing, endPoint: .bottomLeading)
.cornerRadius(16)
.frame(width: 140, height: 140)
.contextMenu {
Button("", action: {})
}
}
}
However, when the ContextMenu is invoked, the edges are not rounded:
I've tried several things, such as:
Applying the clipShape modifier to clip it to a RoundedRectangle
Wrapping the gradient as the background of a RoundedRectangle view
Using a Color instead of a LinearGradient (which works as expected, but not what I need)
However none work. Any advice would be appreciated, thanks!
Add the following code after .frame(...):
.contentShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
Updated for Swift 5.7.2
TLDR:
// Use the method that takes in ContentShapeKinds
.contentShape(.contextMenuPreview, RoundedRectangle(cornerRadius: 30))
Placed after .frame(...), and before .contextMenu { ... }
Detailed example:
var body: some View {
VStack(alignment: .leading, spacing: .zero) {
Text("Hello")
.foregroundColor(.white)
.font(.title)
}
.frame(maxWidth: .infinity, minHeight: 120)
// These corner radii should match
.background(RoundedRectangle(cornerRadius: 30).foregroundColor(.blue))
.contentShape(.contextMenuPreview, RoundedRectangle(cornerRadius: 30))
.contextMenu {
Button("Look!") { print("We did it!") }
}
}
The above code produces a contextMenu that looks like this:
... instead of this: