SwiftUI scroll to horizontal list item - swiftui

I'm able to scroll to a vertical List item using ScrollViewReader proxy.
I tried doing something similar with Horizontal list, but it totally messed up the view.
Any suggestion, how this can be done ?
tia!
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators:false) {
ForEach(0..<items.count) { index in
Text(items[index].title)
.id(index)
.onTapGesture {
print("tapped \(index)")
}
}
.onAppear{
proxy.scrollTo(selectedIndex, anchor: .center)
}
}
}

ScrollView will not layout your views in a row just because you've specified .horizontal scrolling.
You need to specify HStack/LazyHStack explicitly inside ScrollView.
Another problem you may have encountered is that the scrolling position is not accurate, this can be fixed with DispatchQueue.main.async:
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack { // or LazyHStack
ForEach(0..<items.count) { index in
Text(items[index].title)
.id(index)
.onTapGesture {
print("tapped \(index)")
}
}
}
.onAppear {
DispatchQueue.main.async {
proxy.scrollTo(selectedIndex, anchor: .center)
}
}
}
}

Related

SwiftUI problems: Decreasing the clickable area of buttons inside the scrollview if the view is adjacent to the list

I have a scrollable panel with buttons above the list. And in this layout the lower part of buttons is unclicable.
import SwiftUI
struct TestListScreen: View {
var body: some View {
NavigationView {
VStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 8) {
ForEach(1..<10) { i in
Button { print(i) } label: {
Text("Button \(i)")
.padding(24)
.background(Color.yellow)
}
}
}
}
List {
ForEach(1..<100) { i in
Text(String(i))
}
}
.listStyle(.plain)
.listRowInsets(EdgeInsets())
.frame(maxHeight: .infinity)
}
.navigationBarTitle("Buttons above the list", displayMode: .inline)
}
}
}
struct TestListScreen_Previews: PreviewProvider {
static var previews: some View {
TestListScreen()
}
}
If I remove ScrollView wrapped buttons HStack, or if I remove the list under buttons, everything works well. Also I cannot replace list with a ScrollView + VStack because of poor performance.
So... the "problem" is that you have a scrolling view of button. That 16 pixels is to show the scrollbar when you drag the view.
Easy fix is to leave space for it by adding padding.
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 8) {
ForEach(1..<10) { i in
Button { print(i) } label: {
Text("Button \(i)")
.padding(24)
.background(Color.yellow)
}
}
}
.padding(.bottom, 16)
note: while adding the space does make the whole button clickable, if you just finished dragging the buttons, the 16 pixels is increased for a couple seconds after you finish dragging. During that time, part of the button sill won't be clickable, instead it makes the scrollbar tab get bigger.

How can implement a navigationLink inside of a LazyVGrid and ForEach statement?

How can I implement play buttons that navigate to different views in the corner of each music category item? Here is an image of what I am looking for:
Here is my code:
struct ScrollCategories: View {
var body: some View {
ZStack {
ScrollView {
LazyVGrid (columns: [GridItem(.fixed(200)), GridItem(.fixed(200))]) {
ForEach(musics) { sub in
ZStack {
VStack(alignment: .leading) {
//Hidden Code
} // END OF VSTACK
NavigationLink {
// Naviagte to a different view!
Onboarding()
} label: {
Image(systemName: "play.circle")
.font(.system(size: 30))
.padding(.leading, -72.0)
.padding(.bottom, 130.0)
}
} // END OF ZSTACK
}// END OF FOR EACH
} // END OF GRID ITEM
} // END OF SCROLL VIEW
} //END OF ZSTACK
}
}
As you can see, I have a navigation link, yet it does not show in the preview or simulator.
Here is an image of what the preview looks like:
Your navigation link is there. You just use padding too high that it was out of the frame. You can use ZStack with alignment to put on the left-top corner and add padding to make it nice.
ZStack(alignment: .topLeading) { <- positioning for each View
VStack(alignment: .leading) {
//Hidden Code
} // END OF VSTACK// END OF VSTACk
NavigationLink {
// Navigate to a different view!
Onboarding()
} label: {
Image(systemName: "play.circle")
.font(.system(size: 30))
.padding( {{ put your desire padding }})
}
} // END OF ZSTACK

NavigationLink touch area

I'm trying to use NavigationLink inside List. For a specific reason, it is in .background with EmptyView().
var body: some View {
List {
Section {
HStack {
Image(systemName: "checkmark")
.frame(width: 30, height: 30, alignment: .center)
.opacity(selected ? 1 : 0)
Text("TEST")
Spacer()
}
.onTapGesture {
selected.toggle()
}
.background(
NavigationLink(destination: WifiView()) { EmptyView() }
.disabled(isPad)
)
}
}
}
The problem is the touch area for the NavigationLink is not as expected.
.background's area is as above.
but NavigationLink and EmptyView's area is as above. I tried to force the frame with .frame but it won't change.
What am I missing?
try something like this, works well for me:
var body: some View {
List {
Section {
HStack {
Image(systemName: "checkmark")
.frame(width: 30, height: 30, alignment: .center)
.opacity(selected ? 1 : 0)
Text("TEST")
Spacer()
}
.contentShape(Rectangle()) // <--- here
.onTapGesture {
selected.toggle()
}
.background(
NavigationLink(destination: WifiView()) { EmptyView() }
.disabled(isPad)
)
}
}
}
To distill the accepted answer: all you need to do is to add .contentShape(Rectangle()) to the same element which has .onTapGesture. This solution works for any container, not just List.
E.g. if you have:
VStack {
// ...
}
.onTapGesture {
// ...
}
Add contentShape to the same element:
VStack {
// ...
}
.contentShape(Rectangle())
.onTapGesture {
// ...
}
Rationale:
If you add a tap gesture to a container SwiftUI view, such as VStack or HStack, then SwiftUI only adds the gesture to the parts of the container that have something inside – large parts of the stack are likely to be untappable.
If this is what you want then the default behavior is fine. However, if you want to change the shape of hit tests – the area that responds to taps – then you should use the contentShape() modifier with the shape you want.
(source)

SwiftUI: animating scroll on ScrollView programmatically?

I need to programmatically animate the scroll of a scrollview. The scrollview contains either an HStack or a VStack. Code I tested with is this:
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: spacing) {
ForEach(cids, id: \.self) { cid in
....
}
}
.onAppear {
withAnimation(Animation.easeInOut(duration: 4).delay(3)) {
proxy.scrollTo(testCid)
}
}
}
.frame(maxWidth: w, maxHeight: w / 2)
}
The scrollview does land on the item with the testCid, however, it does not animate.
As soon as the view comes on screen the scrollview is already on testCid...
How can I animate the scroll?
The interactive scroll works if you start it from somewhere else (f.e. Button action) but not from the onAppear modifier. I'd guess this is intentional behavior to prevent the user seeing the scrolling when the view appears (or a bug in SwiftUI...). An ugly workaround is to defer the animation with an DispatchQueue.main.async:
import SwiftUI
struct ContentView: View {
let words = ["planet", "kidnap", "harbor", "legislation", "soap", "management", "prejudice", "an", "trunk", "divide", "critic", "area", "affair"]
#State var selectedWord: String?
var body: some View {
ScrollViewReader { proxy in
VStack(alignment: .leading) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(words, id: \.self) { word in
Text(word)
.background(self.selectedWord == word ? Color.yellow : nil)
.id(word)
}
}
}
Button("Scroll to random word") {
withAnimation(Animation.easeInOut(duration: 1)) {
let word = words.randomElement()
self.selectedWord = word
proxy.scrollTo(word)
}
}
}
.onAppear {
DispatchQueue.main.async { // <--- workaround
withAnimation(Animation.easeInOut(duration: 1).delay(1)) {
let word = self.words.last
self.selectedWord = word
proxy.scrollTo(word)
}
}
}
}
.padding(10)
}
}

How do I access a VStack while UI testing my SwiftUI app

I have a VStack with code relying on the .onTapGesture method. Something like this:
VStack {
if imageShow {
Image("image1")
}
else {
Image("image2")
}
}
.onTapGesture {
imageShow.toggle()
}
I'd like to test this behavior within a UI Test using XCTest. The problem is, I don't know how to access the VStack in order to apply a .tap() to it. I can't seem to find the method attached to app. A button is found using app.buttons[] but there doesn't seem to be an equivalent for app.VStack or app.HStack.
Also, I've tried converting this code to wrap the VStack in a Button, but for some reason, this overlays my image, distorting the preferred behavior.
Updating with full VStack code snippet that I am working with:
VStack(alignment: .leading, spacing: 0) {
ZStack {
if self.create_event_vm.everyone_toggle == true {
Image("loginBackground")
.resizable()
.accessibility(identifier: "everyone_toggle_background")
}
HStack {
VStack(alignment: .leading, spacing: 0) {
Text("Visible to everyone on Hithr")
.kerning(0.8)
.scaledFont(name: "Gotham Medium", size: 18) .foregroundColor(self.create_event_vm.everyone_toggle ? Color.white : Color.black)
.frame(alignment: .leading)
Text("Public event")
.kerning(0.5)
.scaledFont(name: "Gotham Light", size: 16)
.foregroundColor(self.create_event_vm.everyone_toggle ? Color.white : Color.black)
.frame(alignment: .leading)
}
.padding(.leading)
Spacer()
}
}
}
.frame(maxWidth: .infinity, maxHeight: 74)
.onTapGesture {
self.create_event_vm.everyone_toggle.toggle()
}
.accessibility(identifier: "public_event_toggle")
.accessibility(addTraits: .isButton)
Try the following approach
VStack {
if imageShow {
Image("image1")
}
else {
Image("image2")
}
}
.onTapGesture {
imageShow.toggle()
}
.accessibility(addTraits: .isButton)
.accessibility(identifier: "customButton")
and test
XCTAssertTrue(app.buttons["customButton"].exists)
The solution for me was to create an un-styled GroupBox to "wrap" any content inside.
After that we can assign an accessibilityIdentifier to it.
It doesn't disrupt the screen readers or change the layout.
Code:
/// Groupbox "no-op" container style without label
/// to combine the elements for accessibilityId
struct ContainerGroupBoxStyle: GroupBoxStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.content
}
}
extension GroupBoxStyle where Self == ContainerGroupBoxStyle {
static var contain: Self { Self() }
}
extension View {
/// This method wraps the view inside a GroupBox
/// and adds accessibilityIdentifier to it
func groupBoxAccessibilityIdentifier(_ identifier: String) -> some View {
GroupBox {
self
}
.groupBoxStyle(.contain)
.accessibilityIdentifier(identifier)
}
}
Usage:
struct TestView: View {
var body: some View {
HStack {
Image(systemName: "person")
TextField("Name", text: .constant("Name"))
}
.padding()
.onTapGesture {
print("Stack Tap")
}
.groupBoxAccessibilityIdentifier("NAME_TEXTFIELD")
}
}
I find that otherElements["x"] doesn't work for ZStack, VStack or HStack. Instead I identify something else, like a Text, and try to get using staticTexts["x"].