SwiftUI - onTapGesture on HStack works, until it stops working - swiftui

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

Related

How does ScrollView work when wrapping a ZStack?

In the code below, firstScrollProxy does not work, while secondScrollViewProxy does. I don't understand why.
The only solution I found, was to give some id to the overlay, and scroll to that. However that causes other issues for my code, and I'd rather avoid such workarounds.
I played with fixedSize() for the ZStack items, but that didn't help either.
Laying out the items vertically has the same issue, while a VStack works.
The anchor is optional, but trying different anchors does reveal the fact that the scroll view behaves as if the width of the items are the same as the entire scrollable area.
import SwiftUI
struct ContentView: View {
var body: some View {
let numItems: Int = 100
let itemWidth = 60.0
let itemHeight = 100.0
VStack(spacing: 4) {
Spacer()
ScrollViewReader { firstScrollProxy in
ScrollView(.horizontal) {
ZStack {
ForEach(0..<numItems, id:\.self) { x in
Rectangle()
.fill(Color.purple)
.frame(width: itemWidth - 2, height: itemHeight)
.overlay {
Text("\(x)")
}
.position(x: Double(x) * itemWidth + itemWidth / 2.0, y: itemHeight / 2.0)
}
}
.frame(width: Double(numItems) * itemWidth, height: itemHeight)
}
.onTapGesture {
withAnimation {
firstScrollProxy.scrollTo(17, anchor: .center)
}
}
}
.padding(8)
.background(Color(white: 0.2))
Color.clear.frame(height: 10)
ScrollViewReader { secondScrollProxy in
ScrollView(.horizontal) {
HStack(spacing: 2) {
ForEach(0..<numItems, id:\.self) { x in
Rectangle()
.fill(Color.blue)
.frame(width: itemWidth - 2, height: itemHeight)
.overlay {
Text("\(x)")
}
}
}
.frame(height: itemHeight)
}
.onTapGesture {
withAnimation {
secondScrollProxy.scrollTo(17, anchor: .center)
}
}
}
.padding(8)
.background(Color(white: 0.25))
Spacer()
}
.background(.black)
.foregroundColor(.white)
}
}
ZStack doesn't know about the effect of the position modifier. ZStack just assumes all of its children are piled up on top of each other, since that's how it lays them out. So when the ZStack's parent ScrollView asks the ZStack for its size, the ZStack reports a size that is the maximum width and height of any of its children, without accounting for the side-by-side layout you have manually implemented.
Given Rob's answer, the solution is actually to use the anchor to calculate the the position to scroll to. E.g.
ZStack {
// ...
}
.id("zstack")
.onTapGesture {
.withAnimation {
let x = position_of_17 / width_of_zstack
firstScrollProxy.scrollTo("zstack", anchor: UnitPoint(x: x, y: 1.0))
}
}
}
I found a better workaround, if anyone should have this problem. Simply embed a HStack with a width equal to the ZStack in which you can add invisible elements with the ids and the location that you need to scroll to.
In my case this is acceptable, I only ever need to programmatically scroll to 1 element, and switching to an actual HStack view for my content would not make sense for my use case.
ZStack {
// Content
HStack(spacing: 0) {
Color.clear
.frame(width: targetX, height: 1)
Color.clear
.frame(width: itemWidth, height: 1)
.id("target")
Color.clear
.frame(width: totalWidth - targetX - itemWidth, height: 1)
}
}
func scrollToTarget() {
scrollProxy.scrollTo("target", anchor: anchor)
}

Transparent background SwiftUI

I have a list in swiftUI and I want the background to be somewhat see through. However there seems to be a white background applied to both scrollview and List. I was wondering if anyone had a work around or way to change it so when .background(Color.white.opacity(0.7)) is applied to the list, it can be translucent and not have the same affect as .background(Color.white).
Here is my code for the view that I am trying to implement the desired effect. The wordPosts is simply just a custom data struct used to populate the list item data. I've already trying changing the cell and tableview appearance using UIkit on init which did not work.
struct ProfileWordsView: View {
init(){
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
}
var body: some View {
List {
ForEach(wordPosts) { post in
Group {
VStack(alignment: .leading) {
HStack(alignment: .top) {
RoundedRectangle(cornerRadius: 5)
.fill()
.frame(width: 4)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 5) {
Text(post.title).font(.custom("Gilroy-SemiBold", size: 20)).foregroundColor(.black).lineLimit(1)
Text("• \(post.timestamp)").font(.custom("Gilroy-SemiBold", size: 19)).foregroundColor(.gray)
}
HStack {
Text(post.text).font(.custom("Gilroy-Regular", size: 16)).foregroundColor(Color.black.opacity(0.7)).padding(.top, 5)
}
}
Spacer()
VStack {
Button(action: {}) {
Image(systemName: "chevron.up")
.resizable()
.frame(width: 20, height: 15)
.font(Font.title3)
.foregroundColor(.gray)
.padding(.bottom, 6)
}
Text("\(post.rizz)").font(.custom("Gilroy-SemiBold", size: 18)).foregroundColor(.gray)
Button(action: {}) {
Image(systemName: "chevron.down")
.resizable()
.frame(width: 20, height: 15)
.font(Font.title3)
.foregroundColor(.gray)
}
}
}
}.listRowInsets(EdgeInsets(top: 10, leading: 5, bottom: 10, trailing: 10))
}.listRowBackground(Color.clear)
}
.listRowSeparator(.hidden)
}.edgesIgnoringSafeArea(.all).listStyle(.plain)
}
}
I'm not sure if it is possible for your code but I'd recommend using a scrollview instead. They are transparent by default, so that should help. You can always add a frame with a semi-transparent color if you want to have a semi-transparent background instead.

Using a SwiftUI menu in the top-trailing corner

When placing a SwiftUI Menu button in the top-trailing corner, using ignoresSafeArea(), it works correctly in the SwiftUI preview:
But not on the actual device/in simulator:
When I use ignoresSafeArea() again on the menu itself, a strange behavior occurs (iOS 14.5): without padding the menu looses function, with padding, the button moves down when the Menu appears and the Menu will have an odd spacing between button and Menu:
Is there a way to get a Menu to appear correctly in the top-trailing corner?
Example code:
struct MenuButtonExampleView: View {
var body: some View {
Color.yellow
.ignoresSafeArea()
.overlay(self.menuButton, alignment: .topTrailing)
}
#ViewBuilder var menuButton: some View {
Menu(
content: {
Button(
action: {
debugPrint("Action")
},
label: {
Label("Action", systemImage: "xmark")
}
)
},
label: {
Button(
action: {},
label: {
ZStack {
Circle()
.foregroundColor(.green)
.frame(width: 30, height: 30)
Image(systemName: "ellipsis")
.foregroundColor(.white)
}
}
)
.padding(20)
}
)
}
}
This may be due to the navigation bar.
You can try to add this code.
.navigationBarHidden(true)
Create a let constant and get the complete screen height and assign it to that constant and after that change the body code as you can see in the code below and it will work for every device
let customHeight = UIScreen.main.bounds.height
var body: some View {
Color.yellow
.overlay(self.menuButton, alignment: .topTrailing)
.statusBar(hidden: true)
.frame(height: customHeight + (customHeight/18))
.ignoresSafeArea()
}

SwiftUI height animation of a text frame when #ObservedObject change

I try to make a smooth animation when the NetStatus change but it's not working like i want.
I want to get the same effect as when i press the button with the toggle animation. The commented button animation is working great and i try to replicate it with the scaling of the height of the text frame.
The commented button code is just for a working example of the animation effect that i want (expand and close gracefully), i don't need this code.
How can i do that?
import SwiftUI
struct NoNetwork: View {
let screenSize: CGRect = UIScreen.main.bounds
#ObservedObject var online = NetStatus()
var body: some View {
VStack{
Text("NoNetworkTitle")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: self.online.connected ? 0 : 40, alignment: .center)
// .animation(.easeIn(duration: 5))
.background(Color.red)
// Button(action: {
// withAnimation {
// self.online.connected.toggle()
// }
// }, label: {
// Text("Animate")
// })
}
}
}
struct NoNetwork_Previews: PreviewProvider {
static var previews: some View {
NoNetwork()
}
}
To animate when online.connected changes, put the .animation modifier on the VStack:
VStack{
Text("NoNetworkTitle")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: self.online.connected ? 0 : 40, alignment: .center)
.background(Color.red)
Button(action: {
self.online.connected.toggle()
}, label: {
Text("Animate")
})
}
.animation(.easeInOut(duration: 0.5))
This will animate the other views in the VStack as the Text appears and disappears.

Race condition when using List with async-loaded rows and laid out with GeometryReader

This is a follow-up question from Content hugging priority behaviour in SwiftUI.
I have a List with async-loaded images for each row, which has its height set using a GeometryReader. Full code here:
struct CountryCell: View {
let country: Country
#State var childSize: CGSize = .init(width: 0, height: 50)
var body: some View {
HStack {
AsyncImage(url: Endpoints.flag(countryCode: country.flagCode).url, placeholder: Image("flag"))
.aspectRatio(contentMode: .fit)
.frame(width: DeviceMetrics.size.width * 0.25, height: self.childSize.height)
VStack(alignment: .leading, spacing: 5) {
Text("Country: ").bold() + Text(self.country.name)
Text("Capital: ").bold() + Text(self.country.capital)
Text("Currency: ").bold() + Text(self.country.currency)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.background(
GeometryReader { proxy -> AnyView in
DispatchQueue.main.async {
self.childSize = proxy.size
}
return AnyView(Color.clear)
})
}
}
}
Run it, the images won't replace the placeholder (maybe 1 in 10 will randomly show up), although the network requests are made. I can't figure it out, but have a hunch it's a race condition during triggering layout by the GeometryReader and the AsyncImage. If you replace:
.frame(width: DeviceMetrics.size.width * 0.25, height: self.childSize.height)
with:
.frame(width: DeviceMetrics.size.width * 0.25)
then the images will show up correctly. Similarly, if you comment out the GeometryReader, things will start to work too.
Any hints would be much appreciated.