SwiftUI: matchedGeometryEffect not transitioning smoothly - swift

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:

Related

SwiftUI: using MatchedGeometryEffect for a scrollView?

I am trying to understand MatchGeometryEffect a little better. I am trying to put it on each cell of a list in a scrollView. I want it to be where if showAlternate is false, the main cell is show but if showAlternate is true , then the individual cell will show the else of the conditional which would be a larger cell ( not part of the code yet). I have the matchedGeometryEffect on the ForEach right now but how can I do each cell indvidually?
struct ContentView: View {
#Binding var countries: [Country]
#State var showAlternate = false;
#Namespace var namespace;
var body: some View {
NavigationView {
ScrollView {
ForEach(Array(countries.enumerated()), id: \.1.id) { (index,country) in
LazyVStack {
HStack {
NavigationLink(
destination: CountryView(country: country),
label: {
HStack {
Image(country.image)
.resizable()
.scaledToFit()
Text(country.display_name)
.foregroundColor(Color.black)
.padding(.leading)
Spacer()
}
.padding(.top, 12.0)
.frame(height: 80)
}
).buttonStyle(FlatLinkStyle())
}
.padding(.horizontal, 16.0)
.overlay(Divider(), alignment: .top)
}
}
.background(Rectangle().opacity(0.2).matchedGeometryEffect(id: "shape", in: namespace))
}
}
.font(Font.custom("Avenir", size: 22))
}
}

Using TextField hides the ScrollView beneath it in VStack

This view hold a list of pdf names which when tapped open webviews of pdf links.
The view has a search bar above the list which when tapped causes the scrollview to disappear.
struct AllPdfListView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var pdfsFetcher = PDFsFetcher()
#State var searchString = ""
#State var backButtonHidden: Bool = false
#State private var width: CGFloat?
var body: some View {
GeometryReader { geo in
VStack(alignment: .leading, spacing: 1) {
HStack(alignment: .center) {
Image(systemName: "chevron.left")
Text("All PDFs")
.font(.largeTitle)
Spacer()
}
.padding(.leading)
.frame(width: geo.size.width, height: geo.size.height / 10, alignment: .leading)
.background(Color(uiColor: UIColor.systemGray4))
.onTapGesture {
self.mode.wrappedValue.dismiss()
}
HStack(alignment: .center) {
Image(systemName: "magnifyingglass")
.padding([.leading, .top, .bottom])
TextField ("Search All Documents", text: $searchString)
.textFieldStyle(PlainTextFieldStyle())
.autocapitalization(.none)
Image(systemName: "slider.horizontal.3")
.padding(.trailing)
}
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.black, lineWidth: 1))
.padding([.leading, .top, .bottom])
.frame(width: geo.size.width / 1.05 )
ScrollView {
ForEach($searchString.wrappedValue == "" ? pdfsFetcher.pdfs :
pdfsFetcher.pdfs.filter({ pdf in
pdf.internalName.contains($searchString.wrappedValue.lowercased())
})
, id: \._id) { pdf in
if let parsedString = pdf.file?.split(separator: "-") {
let request = URLRequest(url: URL(string: "https://mylink/\(parsedString[1]).pdf")!)
NavigationLink(destination: WebView(request: request)
.navigationBarBackButtonHidden(backButtonHidden)
.navigationBarHidden(backButtonHidden)
.onTapGesture(perform: {
backButtonHidden.toggle()
})) {
HStack(alignment: .center) {
Image(systemName: "doc")
.padding()
.frame(width: width, alignment: .leading)
.lineLimit(1)
.alignmentGuide(.leading, computeValue: { dimension in
self.width = max(self.width ?? 0, dimension.width)
return dimension[.leading]
})
Text(pdf.internalName)
.padding()
.multilineTextAlignment(.leading)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
.padding(.leading)
}
}
}
.navigationBarHidden(true)
}
.accentColor(Color.black)
.onAppear{
pdfsFetcher.pdfs == [] ? pdfsFetcher.fetchPDFs() : nil
}
}
}
}
}
Pdf list and Searchbar.
The same view on Searchbar focus.
I would like the search string to filter the list of pdfs while maintaining the visibility of the list.
I was able to fix this by making my #ObservableObject an #EnvironmentObject in my App :
#main
struct MyApp: App {
#ObservedObject var pdfsFetcher = PDFsFetcher()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(pdfsFetcher)
}
}
}
struct AllPdfListView: View {
#EnvironmentObject var pdfsFetcher: PDFsFetcher
}

How do I fix buttons not being tappable in a ForEach loop?

I have a view which contains a button. The view with the button is created from a forEach loop. For some reason only some buttons are tappable and others are not.
The parent view contains a NavigationView, a scroll view inside the NavigationView, a lazyVStack inside of the scroll view, a forEachloop in that lazyVStack and in that for loop is the child view that contains the button.
struct ContentView: View {
let peoples:[Person] = Bundle.main.decode("data.json")
var body: some View {
let columns = [
GridItem(.flexible(minimum: 300), spacing: 10)
]
NavigationView {
ScrollView(.vertical) {
LazyVStack {
ForEach(peoples, id: \.self) { person in
PersonView(name: person.Name, age: person.Age)
}
}
.navigationTitle("A list of people")
.navigationViewStyle(DefaultNavigationViewStyle())
.padding()
}
}
}
}
The child view is bellow. I suspect the scroll view is stealing the user input, but I am not sure why or how to overcome it. Some buttons are tapeable and some are not.
struct PersonView: View {
#Environment(\.colorScheme) var colorScheme
var name: String
var age: Int
var body: some View {
VStack(alignment:.leading) {
Image("randoPerson")
.resizable()
.scaledToFill()
.frame(minWidth: nil, idealWidth: nil,
maxWidth: UIScreen.main.bounds.width, minHeight: nil,
idealHeight: nil, maxHeight: 300, alignment: .center)
.clipped()
VStack(alignment: .leading, spacing: 6) {
Text("name")
.fontWeight(.heavy)
.padding(.leading)
Text("Age \(age)")
.foregroundColor(Color.gray)
.padding([.leading,.bottom])
Button(action: { print("I was tapped") }) {
HStack {
Image(systemName: "message.fill")
.font(.title)
.foregroundColor(.white)
.padding(.leading)
Text("Message them")
.font(.subheadline)
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
}
.padding()
}
.background(Color(UIColor.systemBackground).cornerRadius(15))
.shadow(color:colorScheme == .dark
? Color.white.opacity(0.2)
: Color.black.opacity(0.2),
radius: 7, x: 0, y: 2)
}
}
}
To fix the issue, you can add an id: UUID associated to a Person and iterate between the Person, inside the ForEach using their ID.
You will also noticed that I added the value as lowercases to respect the Swift convention.
struct Person {
let id: UUID // Add this value
var name: String
var age: Int
}
So here is the ContentView with the id replacing \.self:
struct ContentView: View {
let peoples: [Person] = Bundle.main.decode("data.json")
var body: some View {
NavigationView {
ScrollView(.vertical) {
LazyVStack {
ForEach(peoples, id: \.id) { person in // id replace \.self here
PersonView(name: person.name, age: person.age) // removed uppercase
}
}
.navigationTitle("A list of people")
.navigationViewStyle(DefaultNavigationViewStyle())
.padding()
}
}
}
}

Error: Argument passed to call that takes no arguments SwiftUI

In my code I get this error and I cannot figure out how to resolve it. The error is in the Trailing block of code
struct Map: View {
#ObservedObject public var dataManager = DataManager.shared
#Binding public var seqId: String
// rz- to pass seqId to second view
// #State public var seqId2: String = ""
var body: some View {
ZStack {
VStack {
Text("Results").foregroundColor(Color.white).onAppear{self.dataManager.downloadSeqData(seqId: self.seqId)} .onAppear(perform: playSound)/*rz want to add this sound func to contentview somehow, otherwise it will constantly play sound when page loads */ .edgesIgnoringSafeArea(.all)
//rz- if strand is linear draw line
if dataManager.dataSet?.INSDSeq.topology == "linear" {
Rectangle()
.fill(Color.black)
.frame(width: 500, height: 20)
}
//rz- if strand is circular draw o
if dataManager.dataSet?.INSDSeq.topology == "circular" {
Circle()
.stroke(Color.black, style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.frame(width: 500, height: 500)
}
if dataManager.dataSet?.INSDSeq.topology != "circular" && dataManager.dataSet?.INSDSeq.topology != "linear"{
Text("Data Unavailable, try a new accession number.")
}
}
.navigationBarItems(
//rz - added home and genbank view to navigation bars
leading:
NavigationLink(destination: ContentView()) {
Text("New Query")
.font(.title)
.foregroundColor(Color.black)
},
trailing:
NavigationLink(destination: Screen(seqId: self.$seqId)) {
Text("GenBank View")
.font(.title)
.foregroundColor(Color.black)
Error is in $seqId
NavigationLink(destination: Screen(seqId: self.$seqId)) {
Here is the Screen view
import SwiftUI
struct Screen: View {
#ObservedObject public var dataManager = DataManager.shared
#State private var seqId: String = ""
var body: some View {
VStack {
TextField("Enter Accession Number", text: $seqId)
.padding()
Button("Search")
{
dataManager.downloadSeqData(seqId: seqId)
}
ScrollView {
VStack(alignment: .leading) {
Text(dataManager.dataSet?.INSDSeq.locus ?? "")
.padding()
Text(dataManager.dataSet?.INSDSeq.organism ?? "").padding()
Text(dataManager.dataSet?.INSDSeq.source ?? "").padding()
Text(dataManager.dataSet?.INSDSeq.taxonomy ?? "").padding()
Text(dataManager.dataSet?.INSDSeq.topology ?? "").padding()
Text(dataManager.dataSet?.INSDSeq.length == nil ? "" : "\(dataManager.dataSet!.INSDSeq.length)") .padding()
Text(dataManager.dataSet?.INSDSeq.strandedness ?? "").padding()
Text(dataManager.dataSet?.INSDSeq.moltype ?? "").padding()
Text(dataManager.dataSet?.INSDSeq.sequence ?? "").padding()
}
.cornerRadius(10)
VStack(alignment: .leading, spacing: 10) {
ForEach(dataManager.featureList, id:\.self) { feature in
VStack (alignment: .leading){
VStack (alignment: .leading) {
Text(feature.INSDFeature_key).bold()
Text(feature.INSDFeature_location)
}.padding()
IntervalSection(feature: feature)
QualsSection(feature: feature)
}
.cornerRadius(10)
}
}
.cornerRadius(20)
}
.padding(.horizontal)
}
}
}
struct IntervalSection: View {
var feature: INSDFeature
var body: some View {
VStack (alignment: .leading){
ForEach(0..<feature.INSDFeature_intervals.count, id: \.self) { i in
ForEach(0..<feature.INSDFeature_intervals[i].INSDInterval.count, id: \.self) { j in
if let from = feature.INSDFeature_intervals[i].INSDInterval[j].INSDInterval_from {
VStack (alignment: .leading){
Text("\(from)").bold()
Text("\(feature.INSDFeature_intervals[i].INSDInterval[j].INSDInterval_to ?? -1)")
}
.padding()
.cornerRadius(10)
}
}
}
}
}
}
struct QualsSection: View{
var feature: INSDFeature
var body: some View {
VStack (alignment: .leading){
ForEach(0..<feature.INSDFeature_quals.count , id: \.self) { i in
ForEach(0..<feature.INSDFeature_quals[i].INSDQualifier.count , id: \.self) { j in
VStack (alignment: .leading){
Text(feature.INSDFeature_quals[i].INSDQualifier[j].INSDQualifier_name).bold()
Text(feature.INSDFeature_quals[i].INSDQualifier[j].INSDQualifier_value)
}
.padding()
.cornerRadius(10)
}
}
}
}
}
In SwiftUI you should use Binding instead of State in cases, where the data is coming from another view. Furthermore both attributes of Screen are private, therefore they can't be accessed in this case set from outside of the struct itself.
I would suggest to make seqID in Screen public like you did it with dataManager and change it to State

ScrollView causes buggy buttons in SwiftUI

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)
}