I have a horizontally scrolling LazyHStack. How do I set the initial scroll position?
In UIKit, I would create a horizontally scrolling UICollectionView and set the collectionView.contentOffset to set the initial scroll position, but I'm not sure how to do this in SwiftUI.
struct ContentView: View {
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(1...100, id: \.self) { i in
Text("\(i)")
}
}
}
}
}
You can use ScrollViewReader
Manual scroll
struct ContentView: View {
var body: some View {
ScrollViewReader { value in
Button("Go to #15") {
value.scrollTo(15, anchor: .center)
}
ScrollView(.horizontal) {
LazyHStack(alignment: .top, spacing: 10) {
ForEach(1...100, id: \.self) {
Text("Column \($0)")
}
}
}
}
}
}
Automatic scroll
struct ContentView: View {
var body: some View {
ScrollViewReader { value in
ScrollView(.horizontal) {
LazyHStack(alignment: .top, spacing: 10) {
ForEach(1...100, id: \.self) {
Text("Column \($0)")
}
}
}
.onAppear {
value.scrollTo(15, anchor: .center)
}
}
}
}
Related
//
// ContentView.swift
// DemoProject
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
private var gridItemLayout = [GridItem(.adaptive(minimum: 100))]
#StateObject private var viewModel = HomeViewModel()
var body: some View {
GeometryReader { geometry in
NavigationView {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach(viewModel.results, id: \.self) {
let viewModel = ResultVM(model: $0)
NavigationLink(destination: {
DetailView()
}, label: {
SearchResultRow(resultVM: viewModel)
})
}
}
}
}
.onAppear(perform: {
viewModel.performSearch()
})
}
}
}
struct SearchResultRow: View {
let resultVM: ResultVM
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 16).fill(.yellow)
.frame(maxWidth: .infinity).aspectRatio(1, contentMode: .fit)
.overlay(Text(resultVM.trackName))
.onTapGesture {
}
}.padding()
.background(Color.red)
}
}
I want to navigate to detailed view on click on each Cell, I tried navigation but its not clicking.
DetailView
import SwiftUI
struct DetailView: View {
var body: some View {
Text("Hello World")
}
}
To make your sub grid clickable and leads to another view try this:
ForEach(viewModel.results, id: \.self) {
let viewModel = ResultVM(model: $0)
NavigationLink {
//your DetailView()
} label: {
//this is the design for your navigation button
SearchResultRow(resultVM: viewModel)
}
}
The tap gesture blocks NavigationLink (which is actually a button, so also works onTap), so the fix is just remove
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 16).fill(.yellow)
.frame(maxWidth: .infinity).aspectRatio(1, contentMode: .fit)
.overlay(Text(resultVM.trackName))
// .onTapGesture { // << this conflict !!
// }
Tested with Xcode 13.4 / iOS 15.5
*Note: if you need both, then use .simultaneousGesture(TapGesture().onEnded {})
Context: I need to pin a view in a ScrollView to the top of the screen when scrolling, so I use a LazyVStack with pinnedViews, set the view I need as Section. All good.
Issue: The ScrollView other views might change the content while the view is scrolled to the bottom, when that happens the screen removes all views and doesn't display them back unless I scroll to the top.
Question: Is there another way to pin a view to the top? (I tried to use List, but not exactly what I need) Or is possible to make a custom Stack with pinned views?
This is my answer for another post, but it seems like you both are having a similar problem. Download and import TrackableScrollView, and try out the below code. While scrolling, there is a pinned View() which is displayed at the top of the screen.
Link package: https://github.com/maxnatchanon/trackable-scroll-view
Code:
import SwiftUI
import SwiftUITrackableScrollView //Added
import Combine
struct GameTabView: View {
#State private var scrollViewContentOffset = CGFloat(0) //Added
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
ZStack { //Added
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
VStack {
Color.gray.frame(height: 400)
sectionView
}
}
if(scrollViewContentOffset > 400) {
VStack {
headerView
Spacer()
}
}
}
}
var sectionView: some View {
Section {
tabContentView
.transition(.scale) // FIXED
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
From personal experience, I have done this using a VStack.
VStack{
PinnedItems()
LazyVStack {
OtherItems()
}
}
Then Pinned items will not scroll and are permanently attached to the top. And the lazyVStack will still be able to scroll underneath.
I've got the following view:
#if DEBUG
struct MyTestView_Previews: PreviewProvider {
static var previews: some View {
MyTestView()
}
}
#endif
struct MyTestView: View {
#State var selectedTab: Int = 0
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
ScrollView {
LazyHStack {
TabView(selection: $selectedTab) {
ForEach(0...3, id: \.self) { i in
Text(String(i))
}
}
.animation(.easeInOut)
.frame(width: UIScreen.main.bounds.width)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
Spacer().frame(height: geometry.safeAreaInsets.bottom)
}
.navigationBarTitle("Test")
.edgesIgnoringSafeArea(.bottom)
}
}
}
This used to work fine pre XCode 13 & iOS 15. Now with the latest versions, my TabView items are no longer shown. At all. If I remove ScrollView which gives me the neccesary vertical scrolling in my real scenario, I can see my items again.
How can I have vertical scrolling in LazyHStack -> TabView scenarios?
After a day, I figured I can just move my ScrollView down:
struct MyTestView: View {
#State var selectedTab: Int = 0
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
LazyHStack {
TabView(selection: $selectedTab) {
ForEach(0...3, id: \.self) { i in
ScrollView {
Text(String(i))
}
}
}
.animation(.easeInOut)
.frame(width: UIScreen.main.bounds.width)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
Spacer().frame(height: geometry.safeAreaInsets.bottom)
}
.navigationBarTitle("Test")
.edgesIgnoringSafeArea(.bottom)
}
}
}
Now everything works.
I've a SwiftUI ForEach loop which contains views that expand when clicked. Something similar to the view shown below.
struct ExpandingView: View {
#State var isExpanded = false
var body: some View {
VStack {
Text("Hello!")
if isExpanded {
Text("World")
}
}
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
In theory, I can use the ScrollViewReader and scrollTo a particular position.
ScrollViewReader { view in
ScrollView {
ForEach(0...100, id: \.self) { id in
ExpandingView()
.id(id)
.padding()
}
}
}
However, in practice, I'm not sure how to get the id from within the view (because onTapGesture is in the view) and propagate it up one level.
The other option is to have the onTapGesture in the ForEach loop, but then I'm not sure how to toggle the isExpanded flag for the correct view.
You can pass a ScrollViewProxy to row view and then you can now able to scroll.
struct ExpandingView: View {
#State var isExpanded = false
var id: Int
var proxy: ScrollViewProxy
var body: some View {
VStack {
Text("Hello!")
if isExpanded {
Text("World")
}
}
.onTapGesture {
withAnimation {
isExpanded.toggle()
proxy.scrollTo(id, anchor: .center)
}
}
}
}
struct TestScrollView: View {
var body: some View {
ScrollViewReader { view in
ScrollView {
ForEach(0...100, id: \.self) { id in
ExpandingView(id: id, proxy: view)
.id(id)
.padding()
}
}
}
}
}
if I understand your question correctly, you are asking how to pass the id in "ContentView ScrollView"
into ExpandingView. Try this:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ExpandingView: View {
#State var worldId: Int
#State var isExpanded = false
var body: some View {
VStack {
Text("Hello!")
if isExpanded { Text("World \(worldId)") }
}.onTapGesture {
withAnimation { isExpanded.toggle() }
}
}
}
struct ContentView: View {
var body: some View {
ScrollViewReader { view in
ScrollView {
ForEach(0...100, id: \.self) { id in
ExpandingView(worldId: id).id(id).padding()
}
}
}
}
}
I am trying to achieve Horizontal scroll of a scrollview inside a List. Which is working fine.
However, if I am adding NavigationLink to each ScrollView Element, it seems like NavigationView is unable to do his task.
I have added my code. Please have a look and let me know, what I am missing.
struct MovieHomeView: View {
#ObservedObject var moviesDataSource: MoviesHomeViewModel = MoviesHomeViewModel()
var body: some View {
NavigationView {
VStack {
HeaderView()
.frame(height: 50.0)
List {
MovieCategoryCell(category: "Now Playing", movies: moviesDataSource.nowPlayingMovies)
MovieCategoryCell(category: "Popular", movies: moviesDataSource.popularMovies)
}
.padding(.horizontal, -20)
}
.navigationBarTitle("Movies App")
.navigationBarHidden(true)
}
}
}
struct MovieCategoryCell: View {
var category: String
var movies: [MovieBaseProtocol]
init(category: String, movies: [MovieBaseProtocol]) {
self.category = category
self.movies = movies
}
var body: some View {
VStack(alignment: .leading) {
Text(category)
.font(.title)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(self.movies, id: \.id) { movie in
NavigationLink(destination: MovieUIView(movie: movie)) {
MovieCard(movie: movie)
}
}
}
}
.frame(height: 280)
}
.padding(.horizontal, 20)
}
}
I replace the Views with Text("1") and found everything was fine. So it must be something wrong inside your either/both of your views.
ForEach(self.movies, id: \.id) { movie in
NavigationLink(destination: Text("1")){// MovieUIView(movie: movie)) {
Text("1") // MovieCard(movie: movie)
}
}