Lazy loading SwiftUI Grid on both vertical and horizontal direction - ios

As the title says I try to build a Grid view that loads lazily on both vertical and horizontal, the main goal is building a calendar for a list of employee, both the list and the calendar interval can be very large so they have to be rendered lazily. I've tried LazyHGrid and LazyVGrid but they only render views lazily on one direction. The further approach uses one LazyVStack for the whole view and one LazyHStack for each row, the loading is lazily (check print in the console) but while scrolling the views loses their position and the grid get's broken
struct ContentView: View {
var body: some View {
ScrollView([.horizontal, .vertical]) {
LazyVStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: columnHeaders) {
ForEach(0..<20) { row in
LazyHStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: rowHeader(with: "Employee \(row)")) {
ForEach(0..<100) { column in
Text("Cell \(row), \(column)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 200, height: 100)
.background(Color.red)
.onAppear {
print("Cell \(row), \(column)")
}
}
}
}
}
}
}
}
}
var columnHeaders: some View {
LazyHStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: rowHeader(with: "")) {
ForEach(0..<100) { column in
Text("Header \(column)")
.frame(width: 200, height: 100)
.background(Color.white)
}
}
}
}
func rowHeader(with label: String) -> some View {
Text(label)
.frame(width: 100, height: 100)
.background(Color.white)
}
}
PS: I also need fixed headers, that is achieved in the code above using pinnedViews

You can fix the out-of-alignment problem by using a pair of nested single-axis ScrollViews instead of one multi-axis ScrollView.

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.

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

How to eliminate the space above Section in SwiftUI Form?

I wanna eliminate the space above the first Section in a Form
var body: some View {
VStack {
Text("Text 1")
Form {
Section {
Text("Text 2")
}
}
}
}
I tried to set the frame of the Section's header to 0, but it does not work
The solution is to use a Section with an EmptyView() and place the view you want to be at the top in the header of this Section
var body: some View {
VStack {
Text("Text 1")
Form {
Section(header: VStack(alignment: .center, spacing: 0) {
Text("Text 2").padding(.all, 16)
.frame(width: UIScreen.main.bounds.width, alignment: .leading)
.background(Color.white)
}) {
EmptyView()
}.padding([.top], -6)
}
}
}
I'd supply a dummy header view and set its size to be zero:
Section(header: Color.clear
.frame(width: 0, height: 0)
.accessibilityHidden(true)) {
Text("Text 2")
}
The best solution may depend on your requirements. What worked for me was an empty Text view as in:
Section(header: Text("")) {
MySectionView()
}
BTW, I tried EmptyView() but that is ignored completely; as in the space still remains.
This is much simpler:
struct ContentView: View
{
init()
{
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
var body: some View
{
}
}

How to make a Horizontal List in SwiftUI?

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

Resources