How to make a Horizontal List in SwiftUI? - ios

I can wrap all my views inside a List
List {
// contents
}
But this seems to be vertical scrolling. How do I make it horizontal?

You need to add .horizontal property to the scrollview. otherwise it won't scroll.
ScrollView (.horizontal, showsIndicators: false) {
HStack {
//contents
}
}.frame(height: 100)

Starting from iOS 14 beta1 & XCode 12 beta1 you will be able to wrap LazyHStack in a ScrollView to create a horizontal lazy list of items, i.e., each item will only be loaded on demand:
ScrollView(.horizontal) {
LazyHStack {
ForEach(0...50, id: \.self) { index in
Text(String(index))
.onAppear {
print(index)
}
}
}
}

To make a horizontal scrollable content, you can wrap a HStack inside a ScrollView:
ScrollView {
HStack {
ForEach(0..<10) { i in
Text("Item \(i)")
Divider()
}
}
}
.frame(height: 40)

You can use .horizontal property and use custom elements. For me I use cusomt CircleView
var body: some View {
VStack{
Divider()
ScrollView(.horizontal){
HStack(spacing:10){
ForEach(0..<10){
index in
CircleView(label: "\(index)")
}
}.padding()
}.frame(height:100)
Divider()
Spacer()
}
}
}
//struct CircleView:View
#State var label:String
var body:some View {
ZStack{
Circle()
.fill(Color.yellow)
.frame(width: 70, height: 70)
Text(label)
}
}
}

Related

Prevent SwiftUI scrollview scrolling when interacting with a specific component

I am trying to add a PKCanvasView to a scrollview with SwiftUI.
But when I try to draw in the PKCanvasView it scrolls the scrollview instead.
In other words, how could I deactivate the scrolling when the user interacts with a specific view inside the scrollview?
I parsed through a lot of examples and blog articles and almost similar things but they were either too old or not for SwiftUI...
EDIT
To give further details (and based on Guillermo answer), here is an additional sample:
struct ContentView: View {
#State private var scrollDisabled = false
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill((i & 1 == 0) ? .blue: .yellow)
.frame(width: 300, height: 50)
.overlay {
Text("\(i)")
}
}
}.frame(maxWidth: .infinity)
}
.scrollDisabled(scrollDisabled)
}
.frame(width: 300)
}
}
Note that you cannot scroll touching the leading/trailing white borders.
I'd like the same behavior for yellow items: if you try to move up/down while touching a blue cell it scrolls, but if you try the same with yellow the scrollview shouldn't scroll!
Sorry, I'm really trying my best to be clear... ^^
You can control how your elements respond to the drag gesture with the following:
.gesture(DragGesture(minimumDistance:))
For instance:
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill((i & 1 == 0) ? .blue: .yellow)
.frame(width: 300, height: 50)
.overlay {
Text("\(i)")
}
.gesture(DragGesture(minimumDistance: i & 1 == 0 ? 100 : 0))
}
}.frame(maxWidth: .infinity)
}
}
.frame(width: 300)
}
}
will block the scrolling for the yellow elements!
In iOS 16 was added a modifier scrollDisabled to achieve what you need here's an example:
struct Scroll: View {
#State private var scrollDisabled = false
var body: some View {
VStack {
Button("\(scrollDisabled ? "Enable" : "Disable") Scroll") {
scrollDisabled.toggle()
}
ScrollView {
VStack {
ForEach(1..<50) { i in
Rectangle()
.fill(.blue)
.frame(width: 50, height: 50)
.overlay {
Text("\(i)")
}
}
}.frame(maxWidth: .infinity)
}
.scrollDisabled(scrollDisabled)
}
}
}

Unable to remove space between two Horizontal Scroll views in SwiftUI?

I am trying to create two horizontal scrollable list of elements. Currently, this is what I am able to achieve:
As can be seen, I want to remove the space between the two Scroll Views. Here, is the code I am using to achieve this:
var body: some View {
NavigationView {
VStack (spacing: 0){
Text("Zara") .font(.custom(
"AmericanTypewriter",
fixedSize: 36))
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}.task {
await loadData()
}
ScrollView (.horizontal){
HorizontalScrollModel(searchResult: searchResults1)
}
}
}
}
HorizontalScrollModel() returns a LazyHGrid of the products. I have tried setting the spacing to 0 with VStack (spacing: 0) but this did not work. How do I remove the space between the ScrollViews?
HorizontalScrollModel:
struct HorizontalScrollModel: View {
var searchResults1: [homeResult]
let columns = [
GridItem(.adaptive(minimum: 200))]
#State private var isHearted = false
init( searchResult: [homeResult]) {
self.searchResults1 = searchResult
}
var body: some View {
LazyHGrid (rows: columns ){
ForEach(searchResults1, id: \.prodlink) { item in
NavigationLink{
ProductView(imageLink: item.image_src, productLink: item.prodlink ,
productCost: String(item.price))
} label: {
VStack (alignment: .leading, spacing: 0){
AsyncImage(url: URL(string: item.image_src)) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 10))
} else if phase.error != nil {
Text("There was an error loading the image.")
} else {
ProgressView()
}
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.gray.opacity(0.2), lineWidth: 4)
)
.padding(.top, 2)
.padding(.bottom, 2)
.padding(.leading, 2)
.padding(.trailing, 2)
.overlay (
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.3), lineWidth: 1))
}
}
}
// .padding([.horizontal, .bottom])
}
}
}
Each ScrollView takes all available space offered by VStack, which since it has 2 views will be half the screen for each view.
Inside each ScrollView you then place a LazyHGrid, and inside the cell you are placing a 150x150 image, but the ScrollView size will not change to fit the image.
So, it now depends on what you're aiming for. If you just want the two ScrollView to be joined together, you can have a .frame modified to the outer VStack with a height of 308 (150 for each image + the padding).
Try adding the .fixedSize() modifier to the ScrollView:
ScrollView(.horizontal) {
HorizontalScrollModel(searchResult: searchResults1)
}
.fixedSize(horizontal: false, vertical: true)
You might also be able to add it just to the HorizontalScrollModel.

How to create sticky bottom in SwiftUI?

I am trying to create sticky footer in swiftUI where other part of screen is scrollable but in footer there is one view with buttons and other element which should be fixed.
Thank You for help.
If I understand correctly, what you want to do is stack vertically (VStack)
a Scrollview
another VStack (with the Toggle and the Button), aligned at the bottom :
VStack {
ScrollView {...} // 1
VStack { // 2
Toggle(...)
Button(...)
}
.frame(alignment: .bottom)
}
To take your example :
struct SwiftUIView: View {
#State private var checked: Bool = false
let text = String(repeating: "blabla ", count: 20)
var body: some View {
VStack {
ScrollView {
ForEach((1...100), id: \.self) {_ in
Text(text)
}
}
VStack {
Toggle(isOn: $checked, label: {
Text("I have read...")
})
Button("Enter") {
// action
}
.frame(maxWidth: .infinity)
.padding(.vertical)
.background(Color.red)
}
.padding()
.border(Color.black)
.frame(alignment: .bottom)
}
}
}

SwiftUI Horizontal ScrollView onTapGesture registering taps on vertical scrollview behind it

I have a horizontal scrollview(ForEach) and vertical scrollview(ForEach) in a VStack when I scroll items from vertical scrollview(ForEach) and tap on the items from horizontal view then the tap gesture from horizontal view gets executed instead of tap gesture from vertical view.
here is my code :
//
// TestView.swift
// iOS
//
//
import SwiftUI
struct TestView: View {
#State var searchTerm = ""
var body: some View {
VStack {
TextField("Search",text: $searchTerm)
.padding()
.background(
RoundedRectangle(cornerRadius:15).fill(Color.yellow)
)
.padding()
ScrollView(.horizontal){
HStack{
ForEach(1..<100){ index in
VStack {
Image(systemName: "swift")
.resizable()
.scaledToFit()
.padding()
.frame(width: 100, height: 100, alignment: .center)
.background(Color.gray)
.clipShape(Circle())
Text("item \(index)")
}
.onTapGesture{
print("horizontal list item tapped")
}
}
}
}
ScrollView {
VStack {
ForEach(1..<100) { index in
HStack {
Image(systemName: "swift")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50, alignment: .center)
Text("vertical list item : \(index)")
Spacer()
}
.contentShape(Rectangle())
.onTapGesture{
print("vertical list item \(index) tapped")
}
}
.padding(.all,5)
.background(
RoundedRectangle(cornerRadius:15).fill(Color.gray)
)
.padding(.all,5)
}
}
}
.animation(.easeIn(duration: 10))
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
before scrolling
After scrolling
FYI: having list in the place of vertical scroll view fixed the tap gestures but I would like to have scrollview because I want to use ScrollViewReader to scroll vertical list items as next step
The solution is to add clipped content shape for both scrollviews, like
ScrollView(.horizontal){
// .. content here
}.contentShape(Rectangle())
.clipped()
ScrollView {
// .. content here
}.contentShape(Rectangle())
.clipped()
Tested with Xcode 12 / iOS 14

SwiftUI Jump Menu (sectionIndexTitles)

I have an alphabetically sorted SwiftUI List with sections. I try to set a jump menu / sidebar for scrolling through the sections like here:
In UIKit it was possible by setting the sectionIndexTitles. But I couldn't find a solution for SwiftUI.
What you can do in SwiftUI 2.0 is to use the ScrollViewReader.
First, in your list, each element has to have a unique ID which can be any hashable object. Simply use the .id modifier. For example:
List(0..<10) { i in
Text("Row \(i)")
.id(i)
}
After that you can use the ScrollViewReader as following:
ScrollViewReader { scroll in
VStack {
Button("Jump to row #10") {
scroll.scrollTo(10)
}
List(0..<10) { i in
Text("Row \(i)")
.id(i)
}
}
}
So in your case you could give the each alphabetical section an id, so section "a" would have .id(a) etc. After that you could use the sidebar you've implemented and jump to the desired alphabetical section inside ScrollViewReader.
Edit: So I have tried to make a very simple solution quickly. It is not perfect but it serves your purpose. Feel free to copy and modify the code:
struct AlphaScroller: View {
let alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]
let sampleItems = ["Apple", "Banana", "Orange"]
var body: some View {
ScrollView {
ScrollViewReader{ scroll in
VStack {
//top character picker with horizontal Scrolling
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(alphabet, id: \.self) { char in
Button(char){
withAnimation {
scroll.scrollTo(char, anchor: .top)
}
}
}
}
}.frame(width: UIScreen.main.bounds.width * 0.8, height: 20)
//list of sections
ForEach(alphabet, id: \.self){ char in
HStack {
VStack {
Text(char).id(char)
.font(.system(size: 30))
.padding()
ForEach(sampleItems, id: \.self){ item in
Text(item)
.padding()
}
Rectangle()
.foregroundColor(.gray)
.frame(width: UIScreen.main.bounds.width, height: 1)
}
Spacer()
}
}
}
}
}
}
}

Resources